Design Patterns for Scalable ASP.NET MVC Applications

Introduction

Design patterns are proven solutions to common problems in software design. In ASP.NET MVC applications, implementing these patterns can help improve code maintainability, scalability, and testability. This article explores key design patterns used in ASP.NET MVC projects and provides practical examples of their implementation.

1. Repository Pattern

The repository pattern abstracts data access logic, providing a clean separation between the business logic and the data layer.

Benefits

  • Promotes loose coupling between the application and data storage.
  • Simplifies unit testing by allowing mocking of the data layer.

Implementation

// IRepository Interface
public interface IRepository<T> where T : class
{
    IEnumerable<T> GetAll();
    T GetById(int id);
    void Insert(T entity);
    void Update(T entity);
    void Delete(int id);
}

// Repository Implementation
public class Repository<T> : IRepository<T> where T : class
{
    private readonly ApplicationDbContext _context;
    private DbSet<T> entities;

    public Repository(ApplicationDbContext context)
    {
        _context = context;
        entities = context.Set<T>();
    }

    public IEnumerable<T> GetAll() => entities.ToList();
    public T GetById(int id) => entities.Find(id);
    public void Insert(T entity) => entities.Add(entity);
    public void Update(T entity) => _context.Entry(entity).State = EntityState.Modified;
    public void Delete(int id) => entities.Remove(GetById(id));
}

2. Unit of Work Pattern

The unit of work pattern helps manage transactions by coordinating changes across multiple repositories in a single transaction.

Benefits

  • Ensures consistency across repositories.
  • Reduces redundant calls to the database.

Implementation

public class UnitOfWork : IDisposable
{
    private readonly ApplicationDbContext _context;
    private IRepository<Product> _productRepository;

    public UnitOfWork(ApplicationDbContext context)
    {
        _context = context;
    }

    public IRepository<Product> ProductRepository
    {
        get
        {
            return _productRepository ??= new Repository<Product>(_context);
        }
    }

    public void Save()
    {
        _context.SaveChanges();
    }

    public void Dispose()
    {
        _context.Dispose();
    }
}

3. Dependency Injection (DI)

Dependency injection (DI) injects dependencies into controllers or classes instead of creating them directly.

Benefits

  • Reduces tight coupling.
  • Simplifies testing by allowing dependency substitution.

Implementation

Configure DI in Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IRepository<Product>, Repository<Product>>();
    services.AddScoped<UnitOfWork>();
    services.AddControllersWithViews();
}

Inject Dependencies in Controller

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int StockQuantity { get; set; }
}

Products Controller

using System.Linq;
using System.Web.Mvc;
using MVCDesignPatternsApp.Models;  
using MVCDesignPatternsApp.Repositories;

public class ProductsController : Controller
{
    private readonly UnitOfWork _unitOfWork;

    public ProductsController(UnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }

    // GET: Products
    public ActionResult Index()
    {
        var products = _unitOfWork.ProductRepository.GetAll().ToList();
        return View(products);
    }

    // GET: Products/Details/5
    public ActionResult Details(int id)
    {
        var product = _unitOfWork.ProductRepository.GetById(id);
        if (product == null)
        {
            return HttpNotFound();
        }
        return View(product);
    }

    // GET: Products/Create
    public ActionResult Create()
    {
        return View();
    }

    // POST: Products/Create
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Create(Product product)
    {
        if (ModelState.IsValid)
        {
            _unitOfWork.ProductRepository.Insert(product);
            _unitOfWork.Save();
            return RedirectToAction("Index");
        }
        return View(product);
    }

    // GET: Products/Edit/5
    public ActionResult Edit(int id)
    {
        var product = _unitOfWork.ProductRepository.GetById(id);
        if (product == null)
        {
            return HttpNotFound();
        }
        return View(product);
    }

    // POST: Products/Edit/5
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Edit(Product product)
    {
        if (ModelState.IsValid)
        {
            _unitOfWork.ProductRepository.Update(product);
            _unitOfWork.Save();
            return RedirectToAction("Index");
        }
        return View(product);
    }

    // GET: Products/Delete/5
    public ActionResult Delete(int id)
    {
        var product = _unitOfWork.ProductRepository.GetById(id);
        if (product == null)
        {
            return HttpNotFound();
        }
        return View(product);
    }

    // POST: Products/Delete/5
    [HttpPost, ActionName("Delete")]
    [ValidateAntiForgeryToken]
    public ActionResult DeleteConfirmed(int id)
    {
        _unitOfWork.ProductRepository.Delete(id);
        _unitOfWork.Save();
        return RedirectToAction("Index");
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            _unitOfWork.Dispose();
        }
        base.Dispose(disposing);
    }
}

Views

Ensure you have corresponding views in the Views/Products/ folder:

  1. Index.cshtml: Display the product list.
  2. Details.cshtml: Show product details.
  3. Create.cshtml: Form to add a new product.
  4. Edit.cshtml: Form to update product information.
  5. Delete.cshtml: Confirm product deletion.

View Example for Index.cshtml

@model IEnumerable<YourNamespace.Models.Product>

<h2>Product List</h2>

<p>
    @Html.ActionLink("Create New Product", "Create")
</p>

<table class="table">
    <thead>
        <tr>
            <th>Name</th>
            <th>Price</th>
            <th>Stock Quantity</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
    @foreach (var item in Model) {
        <tr>
            <td>@item.Name</td>
            <td>@item.Price</td>
            <td>@item.StockQuantity</td>
            <td>
                @Html.ActionLink("Edit", "Edit", new { id = item.Id }) |
                @Html.ActionLink("Details", "Details", new { id = item.Id }) |
                @Html.ActionLink("Delete", "Delete", new { id = item.Id })
            </td>
        </tr>
    }
    </tbody>
</table>

4. Factory Pattern

The factory pattern centralizes object creation logic.

Benefits

  • Decouples object creation from usage.
  • Promotes flexibility for varying object requirements.

Implementation

Factory Implementation

public interface IProductService
{
    void ProcessOrder();
}

public class PhysicalProductService : IProductService
{
    public void ProcessOrder() => Console.WriteLine("Processing physical product order.");
}

public class DigitalProductService : IProductService
{
    public void ProcessOrder() => Console.WriteLine("Processing digital product order.");
}

public class ProductServiceFactory
{
    public IProductService GetProductService(string productType)
    {
        return productType switch
        {
            "Physical" => new PhysicalProductService(),
            "Digital" => new DigitalProductService(),
            _ => throw new ArgumentException("Invalid product type")
        };
    }
}

Usage in Controller

public class OrdersController : Controller
{
    private readonly ProductServiceFactory _factory;

    public OrdersController(ProductServiceFactory factory)
    {
        _factory = factory;
    }

    public void CreateOrder(string productType)
    {
        var service = _factory.GetProductService(productType);
        service.ProcessOrder();
    }
}

5. Singleton Pattern

The singleton pattern ensures only one instance of a class is created and shared.

Benefits

  • Ideal for shared resources like logging or caching.
  • Ensures a single point of control.

Implementation

Singleton Class

public sealed class LogManager
{
    private static readonly Lazy<LogManager> instance = new(() => new LogManager());

    private LogManager() { }

    public static LogManager Instance => instance.Value;

    public void Log(string message) => Console.WriteLine($"Log: {message}");
}

6. Command Pattern

The command pattern encapsulates a request as an object, allowing for more complex request handling.

Benefits

  • Supports undoable operations.
  • Decouples request handling from request execution.

Implementation

Command Interface and Implementation

public interface ICommand
{
    void Execute();
}

public class SaveOrderCommand : ICommand
{
    public void Execute() => Console.WriteLine("Order saved.");
}

public class CancelOrderCommand : ICommand
{
    public void Execute() => Console.WriteLine("Order canceled.");
}

Invoker Class

public class CommandInvoker
{
    private readonly List<ICommand> _commands = new();

    public void AddCommand(ICommand command) => _commands.Add(command);

    public void ExecuteCommands()
    {
        foreach (var command in _commands)
        {
            command.Execute();
        }
        _commands.Clear();
    }
}

GitHub Project Link: https://github.com/SardarMudassarAliKhan/MVCDesignPatternsApp

Output

Conclusion

Design patterns enhance the development of ASP.NET MVC applications by promoting best practices, reducing redundancy, and improving maintainability. By implementing patterns such as Repository, Unit of work, dependency injection, factory, singleton, and command, you can build scalable and testable applications.