Why You Should “Break Up” with Your Old Coding Habits
When I first started out with ASP.NET Core, I used to dump all the database logic directly into the Controller. At the time, it seemed fast—just _context.Users.Add(user); await _context.SaveChangesAsync(); and you’re done. But as the project began to scale up with dozens of tables, the real nightmare began.
Imagine you have 50 Controllers and need to switch from SQL Server to MongoDB. Hunting through every single file to modify query logic would consume an entire week. Not to mention, writing Unit Tests for the Controller becomes nearly impossible because it is tightly coupled with Entity Framework. The Repository Pattern and Unit of Work were created specifically to solve this mess, often as part of applying Domain-Driven Design (DDD) to complex projects.
Think of a Repository as a “smart warehouse” for data. Instead of requesting directly from the Database, you simply ask the Repository. Meanwhile, the Unit of Work acts as a transaction manager. It ensures that if you perform a sequence of five data saving operations, either all of them succeed, or nothing changes at all, a concept central to the Transactional Outbox Pattern. This is crucial for maintaining data integrity (ACID), though some advanced architectures use Event Sourcing & CQRS to track the entire journey of data.
Environment Setup and Sample Project
First, you need to prepare an ASP.NET Core Web API project. I’ll assume you’re already familiar with creating projects and configuring Entity Framework Core (EF Core). Quickly install the necessary packages via NuGet:
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.Tools
We will work on a simple Product entity as follows:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
Detailed Configuration: Building a Professional Framework
1. Setting Up the Generic Repository
Instead of writing a separate Repository for every table (Product, Category…), I prefer using an IGenericRepository. This approach helps reduce boilerplate code for basic CRUD operations by 60-70%.
public interface IGenericRepository<T> where T : class
{
Task<T> GetByIdAsync(int id);
Task<IEnumerable<T>> GetAllAsync();
Task AddAsync(T entity);
void Update(T entity);
void Delete(T entity);
}
Below is the actual implementation for your reference:
public class GenericRepository<T> : IGenericRepository<T> where T : class
{
protected readonly MyDbContext _context;
public GenericRepository(MyDbContext context)
{
_context = context;
}
public async Task<T> GetByIdAsync(int id) => await _context.Set<T>().FindAsync(id);
public async Task<IEnumerable<T>> GetAllAsync() => await _context.Set<T>().ToListAsync();
public async Task AddAsync(T entity) => await _context.Set<T>().AddAsync(entity);
public void Update(T entity) => _context.Set<T>().Update(entity);
public void Delete(T entity) => _context.Set<T>().Remove(entity);
}
2. Implementing Unit of Work
This is the “orchestrator” that coordinates the Repositories. Suppose you are processing an order: you need to deduct stock in the Product Repository and create an invoice in the Order Repository. The Unit of Work helps you save both within a single Transaction.
public interface IUnitOfWork : IDisposable
{
IGenericRepository<Product> Products { get; }
Task<int> CompleteAsync();
}
public class UnitOfWork : IUnitOfWork
{
private readonly MyDbContext _context;
public IGenericRepository<Product> Products { get; private set; }
public UnitOfWork(MyDbContext context)
{
_context = context;
Products = new GenericRepository<Product>(_context);
}
public async Task<int> CompleteAsync() => await _context.SaveChangesAsync();
public void Dispose() => _context.Dispose();
}
3. Registering Dependency Injection (DI)
Open the Program.cs file and add the following two lines. This step allows the system to automatically “inject” services into the Controller when needed:
builder.Services.AddScoped(typeof(IGenericRepository<>), typeof(GenericRepository<>));
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
Testing and Optimizing the Workflow
Now, your Controller will be extremely lean. It no longer cares how the DbContext operates and focuses solely on business logic.
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IUnitOfWork _unitOfWork;
public ProductsController(IUnitOfWork unitOfWork) => _unitOfWork = unitOfWork;
[HttpGet]
public async Task<IActionResult> GetAll()
{
var products = await _unitOfWork.Products.GetAllAsync();
return Ok(products);
}
}
During API development, debugging the returned JSON is a frequent task. Instead of installing heavy extensions, I often use toolcraft.app (such as the JSON Formatter tool). It helps me quickly format data and check nested structures very effectively right in the browser.
Why is this a “Lifesaver” for Unit Testing?
When writing tests for ProductsController, you don’t need to set up a real database or use an In-Memory DB, similar to how you would simulate APIs for frontend development. You only need to Mock the IUnitOfWork interface using the Moq library. You can simulate data returned from GetAllAsync() in just a few lines of code. This makes your test suite run many times faster.
var mockUow = new Mock<IUnitOfWork>();
mockUow.Setup(u => u.Products.GetAllAsync()).ReturnsAsync(new List<Product>());
var controller = new ProductsController(mockUow.Object);
Practical Considerations
Don’t apply the Repository Pattern mechanically to every project. If you’re building a small app with only 1-2 tables, creating extra interfaces might just make the code more cumbersome. However, for Enterprise projects or when serious Unit Testing is required, this is the top choice.
When encountering complex queries that require Include or ThenInclude, don’t try to cram them into the Generic Repository. Create a specific repository (e.g., ProductRepository) that inherits from the Generic one to handle those specific logics.
I hope this article helps you feel more confident in organizing a professional and manageable ASP.NET Core code architecture, helping you escape spaghetti code as your project grows.

