How the Generic Repository Pattern Helps Reduce Code in ASP.NET Core API
Explore how the generic repository pattern reduces boilerplate and promotes consistency.
🧩 What is Generic Repository Pattern?
The Generic Repository Pattern is a widely used design approach that helps maintain a clean and organized Data Access Layer by grouping commonly used CRUD (Create, Read, Update, Delete) operations into a single, reusable component. Instead of creating separate repository classes for each entity, a generic repository leverages C# Generics to handle multiple entity types with the same logic.
This pattern allows you to define one repository class that can operate on any entity type, reducing code duplication and simplifying maintenance. Whether you have 5 or 50 entities, the same CRUD functionality can adapt and work seamlessly across all of them.
By centralizing and reusing data access logic, the Generic Repository Pattern promotes consistency, scalability, and easier updates across your application.
A Base Generic Repository (async) interface would like this:
public interface IRepository<T> where T : class
{
Task<IEnumerable<T>> GetAllAsync();
Task<T> GetByIdAsync(object id);
Task<T> AddAsync(T entity);
Task<int> UpdateAsync(T entity);
Task DeleteAsync(T entity);
}
T
stands for a generic type—think of it as a placeholder that can represent any data model.
The implementation of the above interface using Entity Framework would look like this:
public class Repository<T> : IRepository<T> where T : class
{
private readonly AppDbContext _context;
private readonly DbSet<T> _dbSet;
public Repository(AppDbContext context)
{
_context = context;
_dbSet = _context.Set<T>();
}
public async Task<IEnumerable<T>> GetAllAsync()
{
return await _dbSet.ToListAsync();
}
public async Task<T> GetByIdAsync(object id)
{
return await _dbSet.FindAsync(id);
}
public async Task<T> AddAsync(T entity)
{
await _dbSet.AddAsync(entity);
await _context.SaveChangesAsync();
return entity;
}
public async Task<int> UpdateAsync(T entity)
{
_dbSet.Update(entity);
return await _context.SaveChangesAsync();
}
public async Task DeleteAsync(T entity)
{
_dbSet.Remove(entity);
await _context.SaveChangesAsync();
}
}
Let’s assume we have following entities
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
Wire up the other part of code in Startup.cs.
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
Wire up the DbContext class in AppDbContext.cs
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<Product> Products { get; set; }
public DbSet<Category> Categories { get; set; }
}
Now, let's take a look at the controller implementation.
CategoryController.cs
[ApiController]
[Route("api/[controller]")]
public class CategoryController : ControllerBase
{
private readonly IRepository<Category> _categoryRepository;
public CategoryController(IRepository<Category> categoryRepository)
{
_categoryRepository = categoryRepository;
}
// GET: api/category
[HttpGet]
public async Task<ActionResult<IEnumerable<Category>>> GetAll()
{
var categories = await _categoryRepository.GetAllAsync();
return Ok(categories);
}
// GET: api/category/5
[HttpGet("{id}")]
public async Task<ActionResult<Category>> GetById(int id)
{
var category = await _categoryRepository.GetByIdAsync(id);
if (category == null)
return NotFound();
return Ok(category);
}
// POST: api/category
[HttpPost]
public async Task<ActionResult<Category>> Create(Category category)
{
var created = await _categoryRepository.AddAsync(category);
return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
}
// PUT: api/category/5
[HttpPut("{id}")]
public async Task<IActionResult> Update(int id, Category category)
{
if (id != category.Id)
return BadRequest();
var existing = await _categoryRepository.GetByIdAsync(id);
if (existing == null)
return NotFound();
await _categoryRepository.UpdateAsync(category);
return NoContent();
}
// DELETE: api/category/5
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id)
{
var category = await _categoryRepository.GetByIdAsync(id);
if (category == null)
return NotFound();
await _categoryRepository.DeleteAsync(category);
return NoContent();
}
}
ProductController.cs
[ApiController]
[Route("api/[controller]")]
public class ProductController : ControllerBase
{
private readonly IRepository<Product> _productRepository;
public ProductController(IRepository<Product> productRepository)
{
_productRepository = productRepository;
}
// GET: api/product
[HttpGet]
public async Task<IActionResult> GetAll()
{
var products = await _productRepository.GetAllAsync();
return Ok(products);
}
// GET: api/product/5
[HttpGet("{id}")]
public async Task<IActionResult> GetById(int id)
{
var product = await _productRepository.GetByIdAsync(id);
if (product == null) return NotFound();
return Ok(product);
}
// POST: api/product
[HttpPost]
public async Task<IActionResult> Create(Product product)
{
var created = await _productRepository.AddAsync(product);
return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
}
// PUT: api/product/5
[HttpPut("{id}")]
public async Task<IActionResult> Update(int id, Product product)
{
if (id != product.Id) return BadRequest();
var existing = await _productRepository.GetByIdAsync(id);
if (existing == null) return NotFound();
await _productRepository.UpdateAsync(product);
return NoContent();
}
// DELETE: api/product/5
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id)
{
var product = await _productRepository.GetByIdAsync(id);
if (product == null) return NotFound();
await _productRepository.DeleteAsync(product);
return NoContent();
}
}
Code changes are done now, we are ready to run the application.
📄 Result from Swagger UI
📌 When to Use Generic Repository Pattern in C#?
The Generic Repository Pattern isn't a one-size-fits-all solution, but it can be highly beneficial in the right scenarios. Here are some ideal use cases:
✅ Ideal Scenarios:
CRUD Operations
If your application frequently performs standard Create, Read, Update, and Delete operations across different entities, a generic repository helps reduce repetitive code.Decoupling
It serves as a middle layer that separates business logic from data access logic, allowing changes in one layer without affecting the other.Testing and Mocking
For unit testing, a generic repository is easier to mock than creating mocks for multiple specific repositories.Code Reusability
When multiple entities share similar data access patterns, this pattern promotes reuse and avoids duplication.Maintainability
Large codebases benefit from the simplified structure, making it easier to manage and extend the application.Simple Data Models
For applications with straightforward data access needs, a generic repository offers a clean and lightweight solution.
⚠️ When to Avoid:
While the pattern is powerful, it’s not always the best fit. Be cautious in these cases:
Complex Queries
Applications requiring advanced, entity-specific queries may find the generic repository too restrictive or cumbersome.Over-Abstraction
Hiding the actual data access logic can lead to difficulties when debugging, optimizing, or customizing specific operations.Performance Concerns
In performance-critical applications, a generic abstraction might introduce overhead or lead to inefficient queries if not carefully managed.
🔚 Summary
In this tutorial, we explored how to optimize and reduce repetitive code by implementing a Generic Repository Pattern in ASP.NET Core.
We also saw how to extend the generic repository when more specific or customized behavior is needed for particular entities.
I hope you found this guide helpful and informative.
Thanks for reading!
If you enjoyed this article, feel free to share it and follow me for more practical, developer-friendly content like this.