![Build First ASP.NET Core MVC Web App Part 3 - Complete E-Commerce Tutorial with Razor Pages | FreeLearning365 Build First ASP.NET Core MVC Web App Part 3 - Complete E-Commerce Tutorial with Razor Pages | FreeLearning365]()
๐ 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:
csharp
// 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
text
๐ 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
bash
# 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
text
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
bash
# 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
csharp
// 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
csharp
// 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
csharp
// 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
csharp
// 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
csharp
// 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
html
<!-- 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/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/[email protected]/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> [email protected]<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/[email protected]/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
html
<!-- 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
html
<!-- 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
csharp
// 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
csharp
// 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
csharp
// 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
json
// 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
powershell
# 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 the 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.
series-post,asp.net_core,ASPNETCore2025,asp.net,ASPNETCoreMVC,WebAppTutorial,RazorPages,ECommerceProject,MVCPattern,WebDevelopment,csharp,RealWorldExample,FreeLearning365,
My Main Article : https://www.freelearning365.com/2025/10/build-first-aspnet-core-mvc-web-app.html
๐ASP.NET Core Mastery with Latest Features : 40-Part Series
๐ฏ Visit Free Learning Zone