Md Mominul Islam | Software and Data Enginnering | SQL Server, .NET, Power BI, Azure Blog

while(!(succeed=try()));

LinkedIn Portfolio Banner

Latest

Home Top Ad

Responsive Ads Here

Post Top Ad

Responsive Ads Here

Tuesday, September 2, 2025

Best Practices for Repository Pattern and Unit of Work in ASP.NET Core

 

Introduction: Why Repository Pattern and Unit of Work Matter

Imagine you're building an e-commerce platform where users browse products, add items to carts, and place orders. As the app grows, managing database operations with Entity Framework Core (EF Core) directly in controllers becomes messy—code duplication, tight coupling, and testing nightmares. Enter the Repository Pattern and Unit of Work—design patterns that streamline data access, improve maintainability, and enhance testability.

The Repository Pattern abstracts data access logic, acting like a middleman between your business logic and the database. The Unit of Work coordinates multiple repository operations, ensuring atomicity (all-or-nothing transactions). Together, they make your ASP.NET Core app modular and scalable, like organizing a warehouse for efficient inventory management.

This blog is a detailed, SEO-friendly guide for developers of all levels, covering the Repository Pattern and Unit of Work from basics to advanced scenarios. We'll use a real-world e-commerce app as our example, provide interactive code snippets, discuss pros, cons, alternatives, best practices, and standards, and ensure the content is engaging with hands-on challenges. By the end, you'll be equipped to implement these patterns effectively in your projects.

Let’s dive in and build a robust data layer!

Section 1: Understanding the Repository Pattern and Unit of Work

What is the Repository Pattern?

The Repository Pattern provides a collection-like interface for accessing domain objects, abstracting the data layer (e.g., EF Core’s DbContext). It encapsulates CRUD operations and queries, making your business logic database-agnostic.

Real-World Analogy: Think of a repository as a librarian who fetches books (data) from shelves (database) without you needing to know how the library is organized.

What is the Unit of Work Pattern?

The Unit of Work Pattern tracks changes across multiple repositories and coordinates saving them as a single transaction. In EF Core, DbContext naturally acts as a Unit of Work, managing changes and committing them via SaveChanges.

Real-World Analogy: It’s like a warehouse manager ensuring all inventory updates (additions, removals) are finalized together to avoid inconsistencies.

Pros of Repository Pattern and Unit of Work:

  • Abstraction: Decouples business logic from data access, making it easier to switch databases (e.g., SQL Server to PostgreSQL).
  • Testability: Simplifies unit testing by mocking repositories.
  • Consistency: Unit of Work ensures atomic transactions, reducing partial updates.
  • Maintainability: Centralizes data logic, reducing code duplication.

Cons:

  • Complexity: Adds boilerplate code, especially for simple apps.
  • Overhead: May be unnecessary for small projects with straightforward data access.
  • Learning Curve: Beginners may struggle with pattern nuances.

Alternatives:

  • Direct EF Core Usage: Use DbContext directly in controllers or services for simple apps.
  • CQRS (Command Query Responsibility Segregation): Separates reads and writes for complex systems.
  • Dapper: Lightweight ORM for raw SQL, bypassing EF Core’s complexity.

Best Practices (Preview):

  • Use interfaces for repositories to enable mocking and DI.
  • Keep repositories focused on a single entity or aggregate root.
  • Leverage DbContext as the Unit of Work rather than creating a custom one.
  • Follow SOLID principles, especially Single Responsibility Principle (SRP).

Section 2: Setting Up the Repository Pattern

Step 1: Define the Model

For our e-commerce app, let’s create a Product model.

csharp
using System.ComponentModel.DataAnnotations;
public class Product
{
public int Id { get; set; }
[Required]
public string Name { get; set; }
[Range(0.01, double.MaxValue)]
public decimal Price { get; set; }
public int Stock { get; set; }
}

Step 2: Create a Generic Repository Interface

A generic repository reduces code duplication by providing common CRUD operations.

csharp
using System.Linq.Expressions;
public interface IRepository<T> where T : class
{
Task<T> GetByIdAsync(int id);
Task<IEnumerable<T>> GetAllAsync();
Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate);
Task AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(T entity);
}

Step 3: Implement the Generic Repository

Use EF Core’s DbContext to implement the interface.

csharp
using Microsoft.EntityFrameworkCore;
using System.Linq.Expressions;
public class Repository<T> : IRepository<T> where T : class
{
private readonly DbContext _context;
private readonly DbSet<T> _dbSet;
public Repository(DbContext context)
{
_context = context;
_dbSet = context.Set<T>();
}
public async Task<T> GetByIdAsync(int id)
{
return await _dbSet.FindAsync(id);
}
public async Task<IEnumerable<T>> GetAllAsync()
{
return await _dbSet.ToListAsync();
}
public async Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate)
{
return await _dbSet.Where(predicate).ToListAsync();
}
public async Task AddAsync(T entity)
{
await _dbSet.AddAsync(entity);
}
public async Task UpdateAsync(T entity)
{
_dbSet.Update(entity);
}
public async Task DeleteAsync(T entity)
{
_dbSet.Remove(entity);
}
}

Best Practice: Use async methods to ensure scalability. Keep the repository generic to reuse across entities.

Interactive Challenge: Add a GetPagedAsync method to the repository for pagination. How would you implement it?

Section 3: Implementing the Unit of Work Pattern

Step 1: Define the Unit of Work Interface

The Unit of Work exposes repositories and commits changes.

csharp
public interface IUnitOfWork : IDisposable
{
IRepository<Product> Products { get; }
Task<int> SaveChangesAsync();
}

Step 2: Implement the Unit of Work

Use DbContext as the underlying Unit of Work.

csharp
public class UnitOfWork : IUnitOfWork
{
private readonly ECommerceContext _context;
private IRepository<Product> _products;
public UnitOfWork(ECommerceContext context)
{
_context = context;
}
public IRepository<Product> Products
{
get => _products ??= new Repository<Product>(_context);
}
public async Task<int> SaveChangesAsync()
{
return await _context.SaveChangesAsync();
}
public void Dispose()
{
_context.Dispose();
}
}

Step 3: Set Up DbContext and DI

Define the ECommerceContext.

csharp
using Microsoft.EntityFrameworkCore;
public class ECommerceContext : DbContext
{
public ECommerceContext(DbContextOptions<ECommerceContext> options) : base(options) { }
public DbSet<Product> Products { get; set; }
}

Configure DI in Program.cs:

csharp
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<ECommerceContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
builder.Services.AddControllers();
var app = builder.Build();
app.UseRouting();
app.MapControllers();
app.Run();

appsettings.json:

json
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=ECommerceDb;Trusted_Connection=True;"
}
}

Best Practice: Register DbContext and IUnitOfWork as scoped to ensure one instance per HTTP request, aligning with EF Core’s default behavior.

Section 4: Using Repository and Unit of Work in a Real-World Scenario

Scenario: Managing Products in an E-Commerce API

Let’s build a controller to handle product CRUD operations.

csharp
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IUnitOfWork _unitOfWork;
public ProductsController(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
[HttpGet]
public async Task<IActionResult> GetProducts()
{
var products = await _unitOfWork.Products.GetAllAsync();
return Ok(products);
}
[HttpPost]
public async Task<IActionResult> CreateProduct(Product product)
{
await _unitOfWork.Products.AddAsync(product);
await _unitOfWork.SaveChangesAsync();
return CreatedAtAction(nameof(GetProducts), new { id = product.Id }, product);
}
[HttpPut("{id}")]
public async Task<IActionResult> UpdateProduct(int id, Product updatedProduct)
{
var product = await _unitOfWork.Products.GetByIdAsync(id);
if (product == null) return NotFound();
product.Name = updatedProduct.Name;
product.Price = updatedProduct.Price;
product.Stock = updatedProduct.Stock;
await _unitOfWork.Products.UpdateAsync(product);
await _unitOfWork.SaveChangesAsync();
return NoContent();
}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteProduct(int id)
{
var product = await _unitOfWork.Products.GetByIdAsync(id);
if (product == null) return NotFound();
await _unitOfWork.Products.DeleteAsync(product);
await _unitOfWork.SaveChangesAsync();
return NoContent();
}
}

Interactive Challenge: Add a GetProductsByPriceRangeAsync method to the repository and use it in the controller. Test with Postman.

Advanced Scenario: Transactional Operations

In an e-commerce checkout, you need to update product stock and create an order atomically.

csharp
public class CheckoutService
{
private readonly IUnitOfWork _unitOfWork;
public CheckoutService(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task<bool> ProcessCheckoutAsync(int productId, int quantity)
{
using var transaction = await _unitOfWork.BeginTransactionAsync();
try
{
var product = await _unitOfWork.Products.GetByIdAsync(productId);
if (product == null || product.Stock < quantity)
return false;
product.Stock -= quantity;
await _unitOfWork.Products.UpdateAsync(product);
// Simulate order creation
var order = new Order { ProductId = productId, Quantity = quantity };
await _unitOfWork.Orders.AddAsync(order);
await _unitOfWork.SaveChangesAsync();
await transaction.CommitAsync();
return true;
}
catch
{
await transaction.RollbackAsync();
return false;
}
}
}

Note: Assumes an Order model and repository. Extend IUnitOfWork and UnitOfWork to include IRepository<Order>.

Best Practice: Use transactions for operations involving multiple entities to ensure data integrity.

Section 5: Pros, Cons, Alternatives, Best Practices, and Standards

Overall Pros of Repository and Unit of Work:

  • Modularity: Separates data access from business logic.
  • Testability: Easy to mock repositories for unit tests.
  • Atomicity: Unit of Work ensures consistent database updates.
  • Flexibility: Simplifies database provider changes.

Overall Cons:

  • Boilerplate Code: Generic repositories can feel repetitive.
  • Overuse: May overcomplicate simple apps with few entities.
  • Performance: Abstraction can hide inefficient queries if not careful.

Alternatives:

  • Direct EF Core: Use DbContext directly for small apps (less abstraction).
  • CQRS: Separate read and write operations for complex systems.
  • MediatR: Combine with CQRS for decoupled command/query handling.

Best Practices:

  • Interface-Driven Design: Always define repository interfaces for DI and mocking.
  • Scoped Lifetime: Register DbContext and IUnitOfWork as scoped to match HTTP request lifecycle.
  • Query Optimization: Use projections (Select) to reduce data transfer.
  • Error Handling: Wrap Unit of Work operations in try-catch for robust transactions.
  • Avoid Over-Abstraction: Don’t create repositories for every entity—focus on aggregate roots.

Standards:

  • Follow Microsoft’s EF Core guidelines for DI and async operations.
  • Adhere to SOLID principles, especially SRP and Dependency Inversion.
  • Use OWASP guidelines for secure database access (e.g., parameterized queries).
  • Apply Semantic Versioning for EF Core and related dependencies.

Section 6: Testing the Repository and Unit of Work

Unit Testing with Moq

Mock the repository for testing the controller.

csharp
using Moq;
using Xunit;
public class ProductsControllerTests
{
[Fact]
public async Task GetProducts_ReturnsAllProducts()
{
// Arrange
var mockRepo = new Mock<IRepository<Product>>();
mockRepo.Setup(repo => repo.GetAllAsync())
.ReturnsAsync(new List<Product> { new Product { Id = 1, Name = "Laptop", Price = 999.99m } });
var mockUnitOfWork = new Mock<IUnitOfWork>();
mockUnitOfWork.Setup(uow => uow.Products).Returns(mockRepo.Object);
var controller = new ProductsController(mockUnitOfWork.Object);
// Act
var result = await controller.GetProducts();
// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var products = Assert.IsAssignableFrom<IEnumerable<Product>>(okResult.Value);
Assert.Single(products);
}
}

Best Practice: Use an in-memory database (Microsoft.EntityFrameworkCore.InMemory) for integration tests to simulate real DbContext behavior.

Interactive Challenge: Write a test for the CreateProduct endpoint. Mock SaveChangesAsync to verify it’s called.

Section 7: Common Pitfalls and How to Avoid Them

Pitfall 1: Overusing Generic Repositories

Generic repositories can become too generic, leading to bloated interfaces.

Solution: Create specific repositories for complex entities with custom methods (e.g., IProductRepository with GetByCategoryAsync).

Pitfall 2: DbContext Lifetime Issues

Using a singleton DbContext or reusing it across requests causes concurrency errors.

Solution: Always use scoped lifetime for DbContext and IUnitOfWork.

Pitfall 3: Inefficient Queries

Fetching entire entities when only a few fields are needed.

Solution: Use projections to select only required data:

csharp
var products = await _unitOfWork.Products
.FindAsync(p => p.Price > 100)
.Select(p => new { p.Id, p.Name })
.ToListAsync();

Conclusion: Building Scalable ASP.NET Core Apps

The Repository Pattern and Unit of Work streamline data access in ASP.NET Core, making your e-commerce app (or any app) modular, testable, and maintainable. By following best practices—interface-driven design, scoped DI, and optimized queries—you can avoid common pitfalls and build robust systems.

Try the code in your project! Start with the generic repository, then add specific methods for your domain. Share your experiences or questions in the comments. What’s your next Repository Pattern challenge?

No comments:

Post a Comment

Thanks for your valuable comment...........
Md. Mominul Islam

Post Bottom Ad

Responsive Ads Here