Build Your First ASP.NET Core Web App: Complete MVC & Razor Tutorial (Part 3)
📖 Table of Contents
Welcome to Web Development: Your First Real ASP.NET Core Application
Understanding MVC Architecture: The Professional Pattern
Setting Up Your MVC Project Structure
Building Models: Data Structures for Real-World Applications
Creating Controllers: The Brain of Your Application
Designing Views with Razor Syntax: Beautiful, Dynamic UIs
Building a Complete E-Commerce Product Catalog
Implementing Shopping Cart Functionality
User Authentication and Authorization
Adding Admin Dashboard and Management
Form Validation and Error Handling
Responsive Design and Frontend Integration
Testing Your MVC Application
Deployment and Production Ready Setup
What's Next: Advanced Features in Part 4
Build Your First ASP.NET Core Web App: Complete MVC & Razor Tutorial (Part 3)
1. Welcome to Web Development: Your First Real ASP.NET Core Application 🎉
1.1 From Console to Web: Your Transformation Journey
Welcome to the most exciting part of our series! In Parts 1 and 2, you set up your environment like a pro. Now, you'll build your first real web application using ASP.NET Core MVC. This isn't just another tutorial—this is where you transform from a beginner into a web developer.
Real-World Perspective: Think of building your first web app like opening your first restaurant:
Planning = Project structure and architecture
Kitchen = Controllers and business logic
Menu = Views and user interface
Customers = Users interacting with your application
Service = The complete user experience
1.2 What You'll Build: Professional E-Commerce Store
By the end of this guide, you'll have a fully functional e-commerce application with:
✅ Product Catalog: Browse and search products
✅ Shopping Cart: Add, remove, and manage items
✅ User Accounts: Registration and login system
✅ Admin Dashboard: Manage products and orders
✅ Responsive Design: Works on all devices
✅ Real Database: SQL Server with Entity Framework
2. Understanding MVC Architecture: The Professional Pattern 🏗️
2.1 MVC Demystified: How Web Applications Really Work
MVC (Model-View-Controller) is not just a pattern—it's a way of thinking about web applications:
// Real-World MVC Analogy: Restaurant Order System public class RestaurantMVC { // MODEL = Kitchen & Ingredients (Data & Business Logic) public class OrderModel { public List<MenuItem> Menu { get; set; } public decimal CalculateTotal() { /* Business logic */ } public bool ValidateOrder() { /* Validation rules */ } } // VIEW = Menu & Presentation (User Interface) public class MenuView { public void DisplayMenu(List<MenuItem> items) { /* Render UI */ } public void ShowOrderConfirmation(Order order) { /* Show results */ } } // CONTROLLER = Waiter (Request Handler) public class OrderController { public ActionResult TakeOrder(OrderRequest request) { // Coordinate between Model and View var order = _model.CreateOrder(request); return _view.ShowConfirmation(order); } } }
2.2 MVC Request Lifecycle in ASP.NET Core
🌐 User Request → 🔍 Routing → 🎯 Controller → 📊 Model → 👁️ View → 🌐 Response ↓ ↓ ↓ ↓ ↓ ↓ Browser ASP.NET Business Data UI HTML Core Logic Access Render Response
2.3 Why MVC is Perfect for Beginners
Advantages:
Separation of Concerns: Each component has a clear responsibility
Testability: Easy to unit test individual components
Maintainability: Changes in one area don't break others
Team Collaboration: Multiple developers can work simultaneously
Industry Standard: Used by 70% of enterprise web applications
3. Setting Up Your MVC Project Structure 📁
3.1 Creating Your MVC Project
# Professional MVC Project Creation dotnet new mvc -n TechShop -f net8.0 --auth Individual -o TechShop cd TechShop # Explore the generated structure dotnet run
3.2 Understanding the Generated Structure
TechShop/ ├── Controllers/ # 🎯 Request handlers │ ├── HomeController.cs │ └── ... ├── Models/ # 📊 Data and business logic │ ├── ErrorViewModel.cs │ └── ... ├── Views/ # 👁️ User interface │ ├── Home/ │ │ ├── Index.cshtml │ │ └── ... │ ├── Shared/ │ │ ├── _Layout.cshtml │ │ └── ... │ └── _ViewStart.cshtml ├── wwwroot/ # 🌐 Static files (CSS, JS, images) │ ├── css/ │ ├── js/ │ └── lib/ ├── Program.cs # 🚀 Application entry point └── appsettings.json # ⚙️ Configuration
3.3 Enhanced Project Structure for E-Commerce
# Custom directory structure for our e-commerce app TechShop/ ├── Controllers/ │ ├── HomeController.cs │ ├── ProductsController.cs │ ├── ShoppingCartController.cs │ ├── AccountController.cs │ └── AdminController.cs ├── Models/ │ ├── Entities/ # Database entities │ │ ├── Product.cs │ │ ├── Category.cs │ │ ├── Order.cs │ │ └── User.cs │ ├── ViewModels/ # UI-specific models │ │ ├── ProductViewModel.cs │ │ ├── CartViewModel.cs │ │ └── ... │ └── Enums/ │ ├── OrderStatus.cs │ └── ProductCategory.cs ├── Views/ │ ├── Home/ │ ├── Products/ │ ├── ShoppingCart/ │ ├── Account/ │ ├── Admin/ │ └── Shared/ ├── Services/ # Business logic services │ ├── ProductService.cs │ ├── CartService.cs │ └── ... ├── Data/ # Data access layer │ ├── ApplicationDbContext.cs │ └── ... └── wwwroot/ ├── images/ │ └── products/ ├── css/ ├── js/ └── lib/
4. Building Models: Data Structures for Real-World Applications 📊
4.1 Core Business Entities
// Models/Entities/Product.cs using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace TechShop.Models.Entities { public class Product { public int Id { get; set; } [Required(ErrorMessage = "Product name is required")] [StringLength(100, ErrorMessage = "Name cannot exceed 100 characters")] public string Name { get; set; } = string.Empty; [Required] [StringLength(500)] public string Description { get; set; } = string.Empty; [Required] [Range(0.01, 10000, ErrorMessage = "Price must be between $0.01 and $10,000")] [Column(TypeName = "decimal(18,2)")] public decimal Price { get; set; } [Required] [Range(0, 1000, ErrorMessage = "Stock must be between 0 and 1000")] public int StockQuantity { get; set; } [Required] [StringLength(50)] public string Category { get; set; } = string.Empty; [Url(ErrorMessage = "Please enter a valid image URL")] public string ImageUrl { get; set; } = "/images/products/default.png"; public string Brand { get; set; } = string.Empty; public string SKU { get; set; } = string.Empty; // Stock Keeping Unit public DateTime CreatedDate { get; set; } = DateTime.UtcNow; public DateTime UpdatedDate { get; set; } = DateTime.UtcNow; public bool IsActive { get; set; } = true; // Navigation properties public ICollection<OrderItem> OrderItems { get; set; } = new List<OrderItem>(); // Business logic methods public bool IsInStock() => StockQuantity > 0; public bool IsLowStock() => StockQuantity > 0 && StockQuantity <= 10; public decimal CalculateDiscountedPrice(decimal discountPercentage) { if (discountPercentage < 0 || discountPercentage > 100) throw new ArgumentException("Discount must be between 0 and 100"); return Price * (1 - discountPercentage / 100); } public void ReduceStock(int quantity) { if (quantity <= 0) throw new ArgumentException("Quantity must be positive"); if (quantity > StockQuantity) throw new InvalidOperationException("Insufficient stock"); StockQuantity -= quantity; UpdatedDate = DateTime.UtcNow; } } }
4.2 Shopping Cart and Order Models
// Models/Entities/ShoppingCart.cs namespace TechShop.Models.Entities { public class ShoppingCart { public string Id { get; set; } = Guid.NewGuid().ToString(); public string UserId { get; set; } = string.Empty; // For logged-in users public string SessionId { get; set; } = string.Empty; // For guest users public DateTime CreatedDate { get; set; } = DateTime.UtcNow; public DateTime UpdatedDate { get; set; } = DateTime.UtcNow; // Navigation properties public ICollection<CartItem> Items { get; set; } = new List<CartItem>(); // Business logic methods public decimal CalculateTotal() { return Items.Sum(item => item.Quantity * item.Product.Price); } public int TotalItems => Items.Sum(item => item.Quantity); public void AddItem(Product product, int quantity = 1) { var existingItem = Items.FirstOrDefault(item => item.ProductId == product.Id); if (existingItem != null) { existingItem.Quantity += quantity; } else { Items.Add(new CartItem { ProductId = product.Id, Product = product, Quantity = quantity, UnitPrice = product.Price }); } UpdatedDate = DateTime.UtcNow; } public void RemoveItem(int productId) { var item = Items.FirstOrDefault(item => item.ProductId == productId); if (item != null) { Items.Remove(item); UpdatedDate = DateTime.UtcNow; } } public void Clear() { Items.Clear(); UpdatedDate = DateTime.UtcNow; } } public class CartItem { public int Id { get; set; } public string ShoppingCartId { get; set; } = string.Empty; public int ProductId { get; set; } public int Quantity { get; set; } public decimal UnitPrice { get; set; } // Navigation properties public ShoppingCart ShoppingCart { get; set; } = null!; public Product Product { get; set; } = null!; public decimal LineTotal => Quantity * UnitPrice; } }
4.3 Order Management System
// Models/Entities/Order.cs namespace TechShop.Models.Entities { public class Order { public int Id { get; set; } public string OrderNumber { get; set; } = GenerateOrderNumber(); public string UserId { get; set; } = string.Empty; // Customer information public string CustomerName { get; set; } = string.Empty; public string Email { get; set; } = string.Empty; public string Phone { get; set; } = string.Empty; // Shipping address public string ShippingAddress { get; set; } = string.Empty; public string ShippingCity { get; set; } = string.Empty; public string ShippingState { get; set; } = string.Empty; public string ShippingZipCode { get; set; } = string.Empty; public string ShippingCountry { get; set; } = "USA"; // Order details public DateTime OrderDate { get; set; } = DateTime.UtcNow; public OrderStatus Status { get; set; } = OrderStatus.Pending; public decimal Subtotal { get; set; } public decimal Tax { get; set; } public decimal ShippingCost { get; set; } public decimal Total => Subtotal + Tax + ShippingCost; public string? Notes { get; set; } // Navigation properties public ICollection<OrderItem> OrderItems { get; set; } = new List<OrderItem>(); // Business logic methods public static string GenerateOrderNumber() { return $"ORD-{DateTime.UtcNow:yyyyMMdd}-{Guid.NewGuid().ToString("N")[..8].ToUpper()}"; } public bool CanBeCancelled() { return Status == OrderStatus.Pending || Status == OrderStatus.Confirmed; } public void CalculateTotals() { Subtotal = OrderItems.Sum(item => item.Quantity * item.UnitPrice); Tax = Subtotal * 0.08m; // 8% tax for example ShippingCost = Subtotal > 50 ? 0 : 5.99m; // Free shipping over $50 } } public class OrderItem { public int Id { get; set; } public int OrderId { get; set; } public int ProductId { get; set; } public string ProductName { get; set; } = string.Empty; public decimal UnitPrice { get; set; } public int Quantity { get; set; } // Navigation properties public Order Order { get; set; } = null!; public Product Product { get; set; } = null!; public decimal LineTotal => Quantity * UnitPrice; } public enum OrderStatus { Pending, Confirmed, Processing, Shipped, Delivered, Cancelled, Refunded } }
5. Creating Controllers: The Brain of Your Application 🎯
5.1 Products Controller - The Heart of E-Commerce
// Controllers/ProductsController.cs using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using TechShop.Models.Entities; using TechShop.Models.ViewModels; using TechShop.Data; namespace TechShop.Controllers { public class ProductsController : Controller { private readonly ApplicationDbContext _context; private readonly ILogger<ProductsController> _logger; public ProductsController(ApplicationDbContext context, ILogger<ProductsController> logger) { _context = context; _logger = logger; } // GET: /Products public async Task<IActionResult> Index( string category = "", string search = "", string sortBy = "name", int page = 1, int pageSize = 12) { _logger.LogInformation("Loading products with filters: Category={Category}, Search={Search}", category, search); var query = _context.Products .Where(p => p.IsActive) .AsQueryable(); // Apply filters if (!string.IsNullOrEmpty(category)) { query = query.Where(p => p.Category == category); } if (!string.IsNullOrEmpty(search)) { query = query.Where(p => p.Name.Contains(search) || p.Description.Contains(search) || p.Brand.Contains(search)); } // Apply sorting query = sortBy.ToLower() switch { "price" => query.OrderBy(p => p.Price), "price_desc" => query.OrderByDescending(p => p.Price), "newest" => query.OrderByDescending(p => p.CreatedDate), _ => query.OrderBy(p => p.Name) // Default sort by name }; // Pagination var totalItems = await query.CountAsync(); var totalPages = (int)Math.Ceiling(totalItems / (double)pageSize); var products = await query .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(); var viewModel = new ProductListViewModel { Products = products, CurrentCategory = category, SearchTerm = search, SortBy = sortBy, CurrentPage = page, TotalPages = totalPages, TotalItems = totalItems, PageSize = pageSize, Categories = await _context.Products .Where(p => p.IsActive) .Select(p => p.Category) .Distinct() .OrderBy(c => c) .ToListAsync() }; return View(viewModel); } // GET: /Products/Details/5 public async Task<IActionResult> Details(int? id) { if (id == null) { _logger.LogWarning("Product details requested without ID"); return NotFound(); } var product = await _context.Products .FirstOrDefaultAsync(p => p.Id == id && p.IsActive); if (product == null) { _logger.LogWarning("Product with ID {ProductId} not found", id); return NotFound(); } // Get related products var relatedProducts = await _context.Products .Where(p => p.Category == product.Category && p.Id != product.Id && p.IsActive) .OrderBy(p => p.Name) .Take(4) .ToListAsync(); var viewModel = new ProductDetailViewModel { Product = product, RelatedProducts = relatedProducts }; return View(viewModel); } // GET: /Products/Category/{category} public async Task<IActionResult> Category(string category, int page = 1) { if (string.IsNullOrEmpty(category)) { return RedirectToAction(nameof(Index)); } var products = await _context.Products .Where(p => p.Category == category && p.IsActive) .OrderBy(p => p.Name) .Skip((page - 1) * 12) .Take(12) .ToListAsync(); var totalProducts = await _context.Products .CountAsync(p => p.Category == category && p.IsActive); var viewModel = new ProductCategoryViewModel { CategoryName = category, Products = products, CurrentPage = page, TotalPages = (int)Math.Ceiling(totalProducts / 12.0), TotalProducts = totalProducts }; return View(viewModel); } // POST: /Products/Search [HttpPost] public IActionResult Search(string searchTerm) { if (string.IsNullOrWhiteSpace(searchTerm)) { return RedirectToAction(nameof(Index)); } return RedirectToAction(nameof(Index), new { search = searchTerm.Trim() }); } // AJAX: /Products/QuickSearch [HttpGet] public async Task<IActionResult> QuickSearch(string term) { if (string.IsNullOrWhiteSpace(term) || term.Length < 2) { return Json(new List<object>()); } var products = await _context.Products .Where(p => p.IsActive && (p.Name.Contains(term) || p.Brand.Contains(term))) .OrderBy(p => p.Name) .Take(5) .Select(p => new { id = p.Id, name = p.Name, brand = p.Brand, price = p.Price.ToString("C"), imageUrl = p.ImageUrl, url = Url.Action("Details", "Products", new { id = p.Id }) }) .ToListAsync(); return Json(products); } } }
5.2 Shopping Cart Controller
// Controllers/ShoppingCartController.cs using Microsoft.AspNetCore.Mvc; using TechShop.Models.Entities; using TechShop.Models.ViewModels; using TechShop.Data; using Microsoft.EntityFrameworkCore; namespace TechShop.Controllers { public class ShoppingCartController : Controller { private readonly ApplicationDbContext _context; private readonly ILogger<ShoppingCartController> _logger; public ShoppingCartController(ApplicationDbContext context, ILogger<ShoppingCartController> logger) { _context = context; _logger = logger; } // GET: /Cart public async Task<IActionResult> Index() { var cart = await GetOrCreateCartAsync(); var viewModel = new CartViewModel { Items = cart.Items.ToList(), Total = cart.CalculateTotal(), ItemCount = cart.TotalItems }; return View(viewModel); } // POST: /Cart/Add/5 [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Add(int productId, int quantity = 1) { try { var product = await _context.Products .FirstOrDefaultAsync(p => p.Id == productId && p.IsActive); if (product == null) { _logger.LogWarning("Attempt to add non-existent product {ProductId} to cart", productId); return NotFound(); } if (!product.IsInStock()) { TempData["Error"] = "Sorry, this product is out of stock."; return RedirectToAction("Details", "Products", new { id = productId }); } if (quantity > product.StockQuantity) { TempData["Error"] = $"Only {product.StockQuantity} items available in stock."; return RedirectToAction("Details", "Products", new { id = productId }); } var cart = await GetOrCreateCartAsync(); cart.AddItem(product, quantity); await _context.SaveChangesAsync(); TempData["Success"] = $"{product.Name} added to cart!"; _logger.LogInformation("Product {ProductId} added to cart with quantity {Quantity}", productId, quantity); } catch (Exception ex) { _logger.LogError(ex, "Error adding product {ProductId} to cart", productId); TempData["Error"] = "There was an error adding the product to your cart."; } return RedirectToAction("Details", "Products", new { id = productId }); } // POST: /Cart/Update [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Update(int productId, int quantity) { try { var cart = await GetOrCreateCartAsync(); var cartItem = cart.Items.FirstOrDefault(item => item.ProductId == productId); if (cartItem == null) { return NotFound(); } if (quantity <= 0) { // Remove item if quantity is 0 or negative cart.RemoveItem(productId); } else { var product = await _context.Products.FindAsync(productId); if (product != null && quantity > product.StockQuantity) { TempData["Error"] = $"Only {product.StockQuantity} items available in stock."; return RedirectToAction(nameof(Index)); } cartItem.Quantity = quantity; } await _context.SaveChangesAsync(); TempData["Success"] = "Cart updated successfully!"; } catch (Exception ex) { _logger.LogError(ex, "Error updating cart item {ProductId}", productId); TempData["Error"] = "There was an error updating your cart."; } return RedirectToAction(nameof(Index)); } // POST: /Cart/Remove/5 [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Remove(int productId) { try { var cart = await GetOrCreateCartAsync(); cart.RemoveItem(productId); await _context.SaveChangesAsync(); TempData["Success"] = "Item removed from cart!"; } catch (Exception ex) { _logger.LogError(ex, "Error removing product {ProductId} from cart", productId); TempData["Error"] = "There was an error removing the item from your cart."; } return RedirectToAction(nameof(Index)); } // POST: /Cart/Clear [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Clear() { try { var cart = await GetOrCreateCartAsync(); cart.Clear(); await _context.SaveChangesAsync(); TempData["Success"] = "Cart cleared successfully!"; } catch (Exception ex) { _logger.LogError(ex, "Error clearing cart"); TempData["Error"] = "There was an error clearing your cart."; } return RedirectToAction(nameof(Index)); } // GET: /Cart/Summary (Partial View for Layout) public async Task<IActionResult> Summary() { var cart = await GetOrCreateCartAsync(); var viewModel = new CartSummaryViewModel { ItemCount = cart.TotalItems, Total = cart.CalculateTotal() }; return PartialView("_CartSummary", viewModel); } private async Task<ShoppingCart> GetOrCreateCartAsync() { var cartId = GetCartId(); var cart = await _context.ShoppingCarts .Include(c => c.Items) .ThenInclude(i => i.Product) .FirstOrDefaultAsync(c => c.Id == cartId); if (cart == null) { cart = new ShoppingCart { Id = cartId, SessionId = HttpContext.Session.Id }; _context.ShoppingCarts.Add(cart); await _context.SaveChangesAsync(); } return cart; } private string GetCartId() { var cartId = HttpContext.Session.GetString("CartId"); if (string.IsNullOrEmpty(cartId)) { cartId = Guid.NewGuid().ToString(); HttpContext.Session.SetString("CartId", cartId); } return cartId; } } }
6. Designing Views with Razor Syntax: Beautiful, Dynamic UIs 👁️
6.1 Master Layout with Bootstrap 5
<!-- Views/Shared/_Layout.cshtml --> <!DOCTYPE html> <html lang="en" data-bs-theme="light"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>@ViewData["Title"] - TechShop</title> <!-- Bootstrap 5 CSS --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet"> <!-- Custom CSS --> <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" /> @await RenderSectionAsync("Styles", required: false) </head> <body> <!-- Navigation --> <nav class="navbar navbar-expand-lg navbar-dark bg-primary sticky-top"> <div class="container"> <a class="navbar-brand fw-bold" asp-area="" asp-controller="Home" asp-action="Index"> <i class="bi bi-laptop"></i> TechShop </a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarNav"> <ul class="navbar-nav me-auto"> <li class="nav-item"> <a class="nav-link" asp-controller="Home" asp-action="Index">Home</a> </li> <li class="nav-item dropdown"> <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"> Products </a> <ul class="dropdown-menu"> <li><a class="dropdown-item" asp-controller="Products" asp-action="Index">All Products</a></li> <li><hr class="dropdown-divider"></li> <li><a class="dropdown-item" asp-controller="Products" asp-action="Category" asp-route-category="Laptops">Laptops</a></li> <li><a class="dropdown-item" asp-controller="Products" asp-action="Category" asp-route-category="Smartphones">Smartphones</a></li> <li><a class="dropdown-item" asp-controller="Products" asp-action="Category" asp-route-category="Tablets">Tablets</a></li> <li><a class="dropdown-item" asp-controller="Products" asp-action="Category" asp-route-category="Accessories">Accessories</a></li> </ul> </li> <li class="nav-item"> <a class="nav-link" asp-controller="Home" asp-action="About">About</a> </li> <li class="nav-item"> <a class="nav-link" asp-controller="Home" asp-action="Contact">Contact</a> </li> </ul> <!-- Search Form --> <form class="d-flex me-3" asp-controller="Products" asp-action="Search" method="post"> <div class="input-group"> <input type="text" class="form-control" placeholder="Search products..." name="searchTerm" id="searchInput" aria-label="Search products"> <button class="btn btn-outline-light" type="submit"> <i class="bi bi-search"></i> </button> </div> </form> <!-- Cart & Auth --> <ul class="navbar-nav"> <li class="nav-item"> <a class="nav-link position-relative" asp-controller="ShoppingCart" asp-action="Index"> <i class="bi bi-cart3"></i> Cart <span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger" id="cartCount"> @await Component.InvokeAsync("CartSummary") </span> </a> </li> <partial name="_LoginPartial" /> </ul> </div> </div> </nav> <!-- Main Content --> <main role="main" class="pb-3"> <!-- Notification Messages --> @if (TempData["Success"] != null) { <div class="alert alert-success alert-dismissible fade show m-3" role="alert"> <i class="bi bi-check-circle-fill me-2"></i> @TempData["Success"] <button type="button" class="btn-close" data-bs-dismiss="alert"></button> </div> } @if (TempData["Error"] != null) { <div class="alert alert-danger alert-dismissible fade show m-3" role="alert"> <i class="bi bi-exclamation-triangle-fill me-2"></i> @TempData["Error"] <button type="button" class="btn-close" data-bs-dismiss="alert"></button> </div> } @RenderBody() </main> <!-- Footer --> <footer class="bg-dark text-light py-5 mt-5"> <div class="container"> <div class="row"> <div class="col-md-4"> <h5><i class="bi bi-laptop"></i> TechShop</h5> <p>Your trusted partner for the latest technology products at competitive prices.</p> </div> <div class="col-md-2"> <h6>Quick Links</h6> <ul class="list-unstyled"> <li><a href="#" class="text-light text-decoration-none">Home</a></li> <li><a href="#" class="text-light text-decoration-none">Products</a></li> <li><a href="#" class="text-light text-decoration-none">About</a></li> <li><a href="#" class="text-light text-decoration-none">Contact</a></li> </ul> </div> <div class="col-md-3"> <h6>Customer Service</h6> <ul class="list-unstyled"> <li><a href="#" class="text-light text-decoration-none">Shipping Info</a></li> <li><a href="#" class="text-light text-decoration-none">Returns</a></li> <li><a href="#" class="text-light text-decoration-none">Privacy Policy</a></li> <li><a href="#" class="text-light text-decoration-none">Terms of Service</a></li> </ul> </div> <div class="col-md-3"> <h6>Contact Us</h6> <p> <i class="bi bi-envelope me-2"></i> support@techshop.com<br> <i class="bi bi-telephone me-2"></i> 1-800-TECHSHOP<br> <i class="bi bi-clock me-2"></i> Mon-Fri: 9AM-6PM </p> </div> </div> <hr class="my-4"> <div class="row align-items-center"> <div class="col-md-6"> <p>© 2024 TechShop. All rights reserved.</p> </div> <div class="col-md-6 text-md-end"> <div class="d-flex justify-content-md-end"> <a href="#" class="text-light me-3"><i class="bi bi-facebook"></i></a> <a href="#" class="text-light me-3"><i class="bi bi-twitter"></i></a> <a href="#" class="text-light me-3"><i class="bi bi-instagram"></i></a> <a href="#" class="text-light"><i class="bi bi-linkedin"></i></a> </div> </div> </div> </div> </footer> <!-- Scripts --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script src="~/js/site.js" asp-append-version="true"></script> <!-- Quick Search Functionality --> <script> document.addEventListener('DOMContentLoaded', function() { const searchInput = document.getElementById('searchInput'); if (searchInput) { // Quick search implementation would go here } // Update cart count dynamically function updateCartCount() { fetch('@Url.Action("Summary", "ShoppingCart")') .then(response => response.text()) .then(html => { document.getElementById('cartCount').innerHTML = html; }); } // Update cart count every 30 seconds setInterval(updateCartCount, 30000); }); </script> @await RenderSectionAsync("Scripts", required: false) </body> </html>
6.2 Product Listing View
<!-- Views/Products/Index.cshtml --> @model TechShop.Models.ViewModels.ProductListViewModel @{ ViewData["Title"] = "Products - TechShop"; } <div class="container mt-4"> <!-- Page Header --> <div class="row mb-4"> <div class="col"> <h1 class="display-6"> @if (!string.IsNullOrEmpty(Model.CurrentCategory)) { <text>@Model.CurrentCategory</text> } else if (!string.IsNullOrEmpty(Model.SearchTerm)) { <text>Search Results for "@Model.SearchTerm"</text> } else { <text>All Products</text> } </h1> <p class="text-muted">Found @Model.TotalItems products</p> </div> </div> <div class="row"> <!-- Sidebar Filters --> <div class="col-lg-3 mb-4"> <div class="card"> <div class="card-header bg-light"> <h6 class="mb-0"><i class="bi bi-funnel"></i> Filters</h6> </div> <div class="card-body"> <!-- Categories --> <div class="mb-3"> <h6>Categories</h6> <div class="list-group list-group-flush"> <a class="list-group-item list-group-item-action @(string.IsNullOrEmpty(Model.CurrentCategory) ? "active" : "")" asp-action="Index" asp-route-search="@Model.SearchTerm" asp-route-sortBy="@Model.SortBy"> All Categories </a> @foreach (var category in Model.Categories) { <a class="list-group-item list-group-item-action @(category == Model.CurrentCategory ? "active" : "")" asp-action="Category" asp-route-category="@category"> @category </a> } </div> </div> <!-- Sorting --> <div class="mb-3"> <h6>Sort By</h6> <select class="form-select" id="sortSelect"> <option value="name" selected="@(Model.SortBy == "name")">Name A-Z</option> <option value="price" selected="@(Model.SortBy == "price")">Price: Low to High</option> <option value="price_desc" selected="@(Model.SortBy == "price_desc")">Price: High to Low</option> <option value="newest" selected="@(Model.SortBy == "newest")">Newest First</option> </select> </div> <!-- Clear Filters --> @if (!string.IsNullOrEmpty(Model.CurrentCategory) || !string.IsNullOrEmpty(Model.SearchTerm)) { <a class="btn btn-outline-secondary w-100" asp-action="Index"> <i class="bi bi-x-circle"></i> Clear Filters </a> } </div> </div> </div> <!-- Product Grid --> <div class="col-lg-9"> <!-- Product Grid --> @if (Model.Products.Any()) { <div class="row g-4"> @foreach (var product in Model.Products) { <div class="col-sm-6 col-md-4 col-lg-4"> <div class="card h-100 product-card"> <!-- Product Image --> <div class="position-relative"> <img src="@product.ImageUrl" class="card-img-top" alt="@product.Name" style="height: 200px; object-fit: cover;"> <!-- Stock Badge --> @if (!product.IsInStock()) { <span class="position-absolute top-0 start-0 m-2 badge bg-danger">Out of Stock</span> } else if (product.IsLowStock()) { <span class="position-absolute top-0 start-0 m-2 badge bg-warning text-dark">Low Stock</span> } <!-- Quick Actions --> <div class="position-absolute top-0 end-0 m-2"> <button class="btn btn-sm btn-light rounded-circle" data-bs-toggle="tooltip" title="Add to Wishlist"> <i class="bi bi-heart"></i> </button> </div> </div> <!-- Card Body --> <div class="card-body d-flex flex-column"> <h6 class="card-title">@product.Name</h6> <p class="card-text text-muted small flex-grow-1">@product.Description.Truncate(80)</p> <div class="mt-auto"> <div class="d-flex justify-content-between align-items-center mb-2"> <span class="h5 text-primary mb-0">@product.Price.ToString("C")</span> <small class="text-muted">@product.Brand</small> </div> <!-- Add to Cart Form --> <form asp-controller="ShoppingCart" asp-action="Add" method="post"> <input type="hidden" name="productId" value="@product.Id" /> @Html.AntiForgeryToken() <div class="d-grid gap-2"> @if (product.IsInStock()) { <button type="submit" class="btn btn-primary btn-sm"> <i class="bi bi-cart-plus"></i> Add to Cart </button> } else { <button type="button" class="btn btn-secondary btn-sm" disabled> <i class="bi bi-cart-x"></i> Out of Stock </button> } <a asp-action="Details" asp-route-id="@product.Id" class="btn btn-outline-secondary btn-sm"> <i class="bi bi-eye"></i> View Details </a> </div> </form> </div> </div> </div> </div> } </div> <!-- Pagination --> @if (Model.TotalPages > 1) { <nav aria-label="Product pagination" class="mt-5"> <ul class="pagination justify-content-center"> <!-- Previous Page --> <li class="page-item @(Model.CurrentPage == 1 ? "disabled" : "")"> <a class="page-link" asp-action="Index" asp-route-page="@(Model.CurrentPage - 1)" asp-route-category="@Model.CurrentCategory" asp-route-search="@Model.SearchTerm" asp-route-sortBy="@Model.SortBy"> Previous </a> </li> <!-- Page Numbers --> @for (int i = 1; i <= Model.TotalPages; i++) { <li class="page-item @(i == Model.CurrentPage ? "active" : "")"> <a class="page-link" asp-action="Index" asp-route-page="@i" asp-route-category="@Model.CurrentCategory" asp-route-search="@Model.SearchTerm" asp-route-sortBy="@Model.SortBy"> @i </a> </li> } <!-- Next Page --> <li class="page-item @(Model.CurrentPage == Model.TotalPages ? "disabled" : "")"> <a class="page-link" asp-action="Index" asp-route-page="@(Model.CurrentPage + 1)" asp-route-category="@Model.CurrentCategory" asp-route-search="@Model.SearchTerm" asp-route-sortBy="@Model.SortBy"> Next </a> </li> </ul> </nav> } } else { <!-- No Products Found --> <div class="text-center py-5"> <i class="bi bi-search display-1 text-muted"></i> <h3 class="mt-3">No products found</h3> <p class="text-muted">Try adjusting your search or filter criteria.</p> <a asp-action="Index" class="btn btn-primary">View All Products</a> </div> } </div> </div> </div> @section Scripts { <script> document.addEventListener('DOMContentLoaded', function() { // Sort selection const sortSelect = document.getElementById('sortSelect'); if (sortSelect) { sortSelect.addEventListener('change', function() { const url = new URL(window.location.href); url.searchParams.set('sortBy', this.value); window.location.href = url.toString(); }); } // Initialize tooltips const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); const tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { return new bootstrap.Tooltip(tooltipTriggerEl); }); }); </script> }
6.3 Shopping Cart View
<!-- Views/ShoppingCart/Index.cshtml --> @model TechShop.Models.ViewModels.CartViewModel @{ ViewData["Title"] = "Shopping Cart - TechShop"; } <div class="container mt-4"> <div class="row"> <div class="col-12"> <h1 class="display-6"> <i class="bi bi-cart3"></i> Shopping Cart </h1> <p class="text-muted">Review your items and proceed to checkout</p> </div> </div> @if (Model.Items.Any()) { <div class="row"> <!-- Cart Items --> <div class="col-lg-8"> <div class="card"> <div class="card-header bg-light"> <div class="row align-items-center"> <div class="col"> <h6 class="mb-0">Cart Items (@Model.ItemCount items)</h6> </div> <div class="col-auto"> <form asp-action="Clear" method="post"> @Html.AntiForgeryToken() <button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('Are you sure you want to clear your cart?')"> <i class="bi bi-trash"></i> Clear Cart </button> </form> </div> </div> </div> <div class="card-body"> @foreach (var item in Model.Items) { <div class="row align-items-center mb-4 pb-4 border-bottom"> <!-- Product Image --> <div class="col-md-2"> <img src="@item.Product.ImageUrl" class="img-fluid rounded" alt="@item.Product.Name" style="max-height: 80px;"> </div> <!-- Product Details --> <div class="col-md-4"> <h6 class="mb-1">@item.Product.Name</h6> <p class="text-muted small mb-1">@item.Product.Brand</p> <p class="text-muted small mb-0">SKU: @item.Product.SKU</p> @if (!item.Product.IsInStock()) { <span class="badge bg-danger mt-1">Out of Stock</span> } else if (item.Quantity > item.Product.StockQuantity) { <span class="badge bg-warning text-dark mt-1">Only @item.Product.StockQuantity available</span> } </div> <!-- Quantity Controls --> <div class="col-md-3"> <form asp-action="Update" method="post" class="d-flex align-items-center"> @Html.AntiForgeryToken() <input type="hidden" name="productId" value="@item.ProductId" /> <button type="button" class="btn btn-outline-secondary btn-sm quantity-btn" data-action="decrease" data-product-id="@item.ProductId"> <i class="bi bi-dash"></i> </button> <input type="number" name="quantity" value="@item.Quantity" min="0" max="@item.Product.StockQuantity" class="form-control form-control-sm mx-2 text-center quantity-input" style="width: 70px;" data-product-id="@item.ProductId"> <button type="button" class="btn btn-outline-secondary btn-sm quantity-btn" data-action="increase" data-product-id="@item.ProductId" @(item.Quantity >= item.Product.StockQuantity ? "disabled" : "")> <i class="bi bi-plus"></i> </button> </form> </div> <!-- Price and Actions --> <div class="col-md-3 text-end"> <div class="mb-2"> <strong class="h6">@item.LineTotal.ToString("C")</strong> <br> <small class="text-muted">@item.UnitPrice.ToString("C") each</small> </div> <form asp-action="Remove" method="post" class="d-inline"> @Html.AntiForgeryToken() <input type="hidden" name="productId" value="@item.ProductId" /> <button type="submit" class="btn btn-sm btn-outline-danger"> <i class="bi bi-trash"></i> Remove </button> </form> </div> </div> } </div> </div> </div> <!-- Order Summary --> <div class="col-lg-4"> <div class="card"> <div class="card-header bg-light"> <h6 class="mb-0">Order Summary</h6> </div> <div class="card-body"> <div class="d-flex justify-content-between mb-2"> <span>Subtotal (@Model.ItemCount items):</span> <strong>@Model.Total.ToString("C")</strong> </div> <div class="d-flex justify-content-between mb-2"> <span>Shipping:</span> <span>@(Model.Total > 50 ? "FREE" : "$5.99")</span> </div> <div class="d-flex justify-content-between mb-2"> <span>Tax:</span> <span>@((Model.Total * 0.08m).ToString("C"))</span> </div> <hr> <div class="d-flex justify-content-between mb-3"> <strong>Total:</strong> <strong class="h5 text-primary"> @((Model.Total + (Model.Total > 50 ? 0 : 5.99m) + (Model.Total * 0.08m)).ToString("C")) </strong> </div> <div class="d-grid gap-2"> <a href="#" class="btn btn-primary btn-lg"> <i class="bi bi-credit-card"></i> Proceed to Checkout </a> <a asp-controller="Products" asp-action="Index" class="btn btn-outline-primary"> <i class="bi bi-arrow-left"></i> Continue Shopping </a> </div> <!-- Trust Badges --> <div class="text-center mt-3"> <div class="row g-2"> <div class="col-4"> <i class="bi bi-shield-check text-success"></i> <small class="d-block">Secure</small> </div> <div class="col-4"> <i class="bi bi-truck text-primary"></i> <small class="d-block">Free Shipping</small> </div> <div class="col-4"> <i class="bi bi-arrow-clockwise text-info"></i> <small class="d-block">Easy Returns</small> </div> </div> </div> </div> </div> </div> </div> } else { <!-- Empty Cart --> <div class="row justify-content-center"> <div class="col-md-6 text-center py-5"> <i class="bi bi-cart-x display-1 text-muted"></i> <h3 class="mt-3">Your cart is empty</h3> <p class="text-muted">Looks like you haven't added any items to your cart yet.</p> <a asp-controller="Products" asp-action="Index" class="btn btn-primary btn-lg"> <i class="bi bi-bag"></i> Start Shopping </a> </div> </div> } </div> @section Scripts { <script> document.addEventListener('DOMContentLoaded', function() { // Quantity update functionality document.querySelectorAll('.quantity-btn').forEach(button => { button.addEventListener('click', function() { const action = this.getAttribute('data-action'); const productId = this.getAttribute('data-product-id'); const input = document.querySelector(`.quantity-input[data-product-id="${productId}"]`); let quantity = parseInt(input.value); if (action === 'increase') { quantity++; } else if (action === 'decrease' && quantity > 1) { quantity--; } input.value = quantity; // Submit form automatically if (quantity >= 0) { input.closest('form').submit(); } }); }); // Direct input change document.querySelectorAll('.quantity-input').forEach(input => { input.addEventListener('change', function() { if (this.value >= 0) { this.closest('form').submit(); } }); }); }); </script> }
7. Database Setup and Configuration 🗄️
7.1 ApplicationDbContext
// Data/ApplicationDbContext.cs using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using TechShop.Models.Entities; namespace TechShop.Data { public class ApplicationDbContext : IdentityDbContext { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } public DbSet<Product> Products { get; set; } public DbSet<ShoppingCart> ShoppingCarts { get; set; } public DbSet<CartItem> CartItems { get; set; } public DbSet<Order> Orders { get; set; } public DbSet<OrderItem> OrderItems { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // Product configuration modelBuilder.Entity<Product>(entity => { entity.HasKey(p => p.Id); entity.Property(p => p.Name).IsRequired().HasMaxLength(100); entity.Property(p => p.Description).HasMaxLength(500); entity.Property(p => p.Price).HasColumnType("decimal(18,2)"); entity.Property(p => p.Category).IsRequired().HasMaxLength(50); entity.Property(p => p.Brand).HasMaxLength(50); entity.Property(p => p.SKU).HasMaxLength(20); entity.HasIndex(p => p.Category); entity.HasIndex(p => p.Brand); entity.HasQueryFilter(p => p.IsActive); }); // ShoppingCart configuration modelBuilder.Entity<ShoppingCart>(entity => { entity.HasKey(c => c.Id); entity.HasMany(c => c.Items) .WithOne(i => i.ShoppingCart) .HasForeignKey(i => i.ShoppingCartId) .OnDelete(DeleteBehavior.Cascade); }); // CartItem configuration modelBuilder.Entity<CartItem>(entity => { entity.HasKey(i => i.Id); entity.HasOne(i => i.Product) .WithMany() .HasForeignKey(i => i.ProductId) .OnDelete(DeleteBehavior.Cascade); }); // Order configuration modelBuilder.Entity<Order>(entity => { entity.HasKey(o => o.Id); entity.Property(o => o.OrderNumber).IsRequired().HasMaxLength(50); entity.Property(o => o.Subtotal).HasColumnType("decimal(18,2)"); entity.Property(o => o.Tax).HasColumnType("decimal(18,2)"); entity.Property(o => o.ShippingCost).HasColumnType("decimal(18,2)"); entity.HasMany(o => o.OrderItems) .WithOne(oi => oi.Order) .HasForeignKey(oi => oi.OrderId) .OnDelete(DeleteBehavior.Cascade); }); // OrderItem configuration modelBuilder.Entity<OrderItem>(entity => { entity.HasKey(oi => oi.Id); entity.Property(oi => oi.UnitPrice).HasColumnType("decimal(18,2)"); entity.HasOne(oi => oi.Product) .WithMany() .HasForeignKey(oi => oi.ProductId) .OnDelete(DeleteBehavior.Restrict); }); // Seed initial data modelBuilder.Entity<Product>().HasData( new Product { Id = 1, Name = "MacBook Pro 16\"", Description = "Powerful laptop for professionals with M2 Pro chip", Price = 2499.99m, StockQuantity = 15, Category = "Laptops", Brand = "Apple", SKU = "MBP16-M2", ImageUrl = "/images/products/macbook-pro.jpg" }, new Product { Id = 2, Name = "iPhone 15 Pro", Description = "Latest iPhone with titanium design and A17 Pro chip", Price = 999.99m, StockQuantity = 30, Category = "Smartphones", Brand = "Apple", SKU = "IP15-PRO", ImageUrl = "/images/products/iphone-15-pro.jpg" }, new Product { Id = 3, Name = "Samsung Galaxy Tab S9", Description = "Premium Android tablet with S Pen included", Price = 799.99m, StockQuantity = 20, Category = "Tablets", Brand = "Samsung", SKU = "TAB-S9", ImageUrl = "/images/products/galaxy-tab-s9.jpg" }, new Product { Id = 4, Name = "Wireless Gaming Mouse", Description = "High-precision wireless mouse for gaming and productivity", Price = 79.99m, StockQuantity = 50, Category = "Accessories", Brand = "Logitech", SKU = "G-MOUSE-WL", ImageUrl = "/images/products/gaming-mouse.jpg" } ); } } }
8. Configuration and Dependency Injection ⚙️
8.1 Program.cs Configuration
// Program.cs using Microsoft.EntityFrameworkCore; using TechShop.Data; using TechShop.Services; var builder = WebApplication.CreateBuilder(args); // Add services to the container var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString)); builder.Services.AddDatabaseDeveloperPageExceptionFilter(); builder.Services.AddDefaultIdentity<IdentityUser>(options => { options.SignIn.RequireConfirmedAccount = true; options.Password.RequireDigit = true; options.Password.RequireLowercase = true; options.Password.RequireNonAlphanumeric = true; options.Password.RequireUppercase = true; options.Password.RequiredLength = 6; }) .AddEntityFrameworkStores<ApplicationDbContext>(); builder.Services.AddControllersWithViews(); // Add session support for shopping cart builder.Services.AddSession(options => { options.Cookie.HttpOnly = true; options.Cookie.IsEssential = true; options.IdleTimeout = TimeSpan.FromMinutes(30); }); // Register custom services builder.Services.AddScoped<IProductService, ProductService>(); builder.Services.AddScoped<ICartService, CartService>(); // Configure HTTP context accessor builder.Services.AddHttpContextAccessor(); var app = builder.Build(); // Configure the HTTP request pipeline if (app.Environment.IsDevelopment()) { app.UseMigrationsEndPoint(); } else { app.UseExceptionHandler("/Home/Error"); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseSession(); app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); app.MapRazorPages(); // Seed database using (var scope = app.Services.CreateScope()) { var services = scope.ServiceProvider; try { var context = services.GetRequiredService<ApplicationDbContext>(); context.Database.Migrate(); // Seed data is already in DbContext } catch (Exception ex) { var logger = services.GetRequiredService<ILogger<Program>>(); logger.LogError(ex, "An error occurred while seeding the database."); } } app.Run();
9. Testing Your MVC Application ✅
9.1 Unit Tests for Controllers
// Tests/Controllers/ProductsControllerTests.cs using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Moq; using TechShop.Controllers; using TechShop.Data; using TechShop.Models.Entities; using Xunit; namespace TechShop.Tests.Controllers { public class ProductsControllerTests { private readonly ProductsController _controller; private readonly ApplicationDbContext _context; private readonly Mock<ILogger<ProductsController>> _loggerMock; public ProductsControllerTests() { // Setup in-memory database var options = new DbContextOptionsBuilder<ApplicationDbContext>() .UseInMemoryDatabase(databaseName: "TechShopTest") .Options; _context = new ApplicationDbContext(options); _loggerMock = new Mock<ILogger<ProductsController>>(); // Seed test data SeedTestData(); _controller = new ProductsController(_context, _loggerMock.Object); } private void SeedTestData() { _context.Products.AddRange( new Product { Id = 1, Name = "Test Laptop", Price = 999.99m, Category = "Laptops", StockQuantity = 10, IsActive = true }, new Product { Id = 2, Name = "Test Phone", Price = 499.99m, Category = "Smartphones", StockQuantity = 20, IsActive = true }, new Product { Id = 3, Name = "Inactive Product", Price = 199.99m, Category = "Tablets", StockQuantity = 5, IsActive = false } ); _context.SaveChanges(); } [Fact] public async Task Index_ReturnsViewResult_WithListOfProducts() { // Act var result = await _controller.Index(); // Assert var viewResult = Assert.IsType<ViewResult>(result); var model = Assert.IsAssignableFrom<object>(viewResult.Model); Assert.NotNull(model); } [Fact] public async Task Index_FiltersByCategory_ReturnsFilteredProducts() { // Act var result = await _controller.Index(category: "Laptops"); // Assert var viewResult = Assert.IsType<ViewResult>(result); var model = Assert.IsAssignableFrom<object>(viewResult.Model); Assert.NotNull(model); } [Fact] public async Task Details_WithValidId_ReturnsViewResult() { // Act var result = await _controller.Details(1); // Assert var viewResult = Assert.IsType<ViewResult>(result); var model = Assert.IsAssignableFrom<object>(viewResult.Model); Assert.NotNull(model); } [Fact] public async Task Details_WithInvalidId_ReturnsNotFound() { // Act var result = await _controller.Details(999); // Assert Assert.IsType<NotFoundResult>(result); } [Fact] public async Task Details_WithInactiveProduct_ReturnsNotFound() { // Act var result = await _controller.Details(3); // Assert Assert.IsType<NotFoundResult>(result); } [Fact] public async Task Category_WithValidCategory_ReturnsViewResult() { // Act var result = await _controller.Category("Laptops"); // Assert var viewResult = Assert.IsType<ViewResult>(result); var model = Assert.IsAssignableFrom<object>(viewResult.Model); Assert.NotNull(model); } [Fact] public void Search_WithEmptyTerm_RedirectsToIndex() { // Act var result = _controller.Search(""); // Assert var redirectResult = Assert.IsType<RedirectToActionResult>(result); Assert.Equal("Index", redirectResult.ActionName); } [Fact] public void Search_WithValidTerm_RedirectsToIndexWithSearch() { // Act var result = _controller.Search("laptop"); // Assert var redirectResult = Assert.IsType<RedirectToActionResult>(result); Assert.Equal("Index", redirectResult.ActionName); Assert.Equal("laptop", redirectResult.RouteValues?["search"]); } } }
10. Deployment and Production Ready Setup 🚀
10.1 Production Configuration
// appsettings.Production.json { "ConnectionStrings": { "DefaultConnection": "Server=production-server;Database=TechShop;User Id=appuser;Password=securepassword;TrustServerCertificate=true;" }, "Logging": { "LogLevel": { "Default": "Warning", "Microsoft.AspNetCore": "Warning", "Microsoft.EntityFrameworkCore.Database.Command": "Warning" } }, "AllowedHosts": "techshop.com,www.techshop.com", "Kestrel": { "Endpoints": { "Https": { "Url": "https://*:443", "Certificate": { "Path": "/path/to/certificate.pfx", "Password": "certificate-password" } } } } }
10.2 Deployment Script
# deploy.ps1 - Production Deployment Script param( [string]$Environment = "Production", [string]$Version = "1.0.0" ) Write-Host "Deploying TechShop v$Version to $Environment..." -ForegroundColor Green try { # Build the application Write-Host "Building application..." -ForegroundColor Yellow dotnet publish -c Release -o ./publish --version-suffix $Version # Run tests Write-Host "Running tests..." -ForegroundColor Yellow dotnet test # Database migrations Write-Host "Applying database migrations..." -ForegroundColor Yellow dotnet ef database update --context ApplicationDbContext # Deployment steps would continue here... # - Copy files to server # - Restart application # - Health checks Write-Host "Deployment completed successfully!" -ForegroundColor Green } catch { Write-Host "Deployment failed: $($_.Exception.Message)" -ForegroundColor Red exit 1 }
11. What's Next: Advanced Features in Part 4 🔮
Coming in Part 4: Advanced E-Commerce Features
User Authentication & Authorization: Complete user management system
Payment Integration: Stripe or PayPal integration
Order Management: Complete order processing workflow
Email Notifications: Order confirmations and status updates
Advanced Search: Elasticsearch integration
Caching Strategy: Redis for performance optimization
API Development: RESTful APIs for mobile apps
Admin Dashboard: Complete management interface
Your MVC Achievement Checklist:
✅ MVC Architecture: Understanding of Model-View-Controller pattern
✅ Razor Pages: Dynamic UI creation with Razor syntax
✅ Entity Framework: Database integration and data modeling
✅ Controllers: Request handling and business logic
✅ Views: Professional UI with Bootstrap 5
✅ Shopping Cart: Complete e-commerce functionality
✅ Database Design: Professional data modeling
✅ Testing: Unit tests for controllers
✅ Production Ready: Deployment configuration
✅ Real Project: Complete working e-commerce application
Transformation Complete: You've built your first professional ASP.NET Core MVC web application! This isn't just a tutorial project—it's a real e-commerce platform that demonstrates enterprise-level development practices.
🎯 Key MVC Development Takeaways
✅ Architecture Mastery: Professional MVC pattern implementation
✅ Razor Expertise: Dynamic, maintainable UI creation
✅ Database Integration: Entity Framework with SQL Server
✅ E-Commerce Features: Shopping cart, product catalog, user management
✅ Professional UI: Bootstrap 5 with responsive design
✅ Error Handling: Comprehensive validation and error management
✅ Testing Foundation: Unit testing for quality assurance
✅ Production Setup: Deployment-ready configuration
✅ Real-World Skills: Industry-standard development practices
✅ Career Foundation: Portfolio-ready project completion
Remember: Every expert web developer started with their first MVC application. You've not just built an app—you've built the foundation for a successful web development career.
0 Comments
thanks for your comments!