Data access is a critical part of application development, and using the Repository Pattern can help create a more maintainable and testable architecture. In this post, we will explore how to implement a Generic Repository in C# using Entity Framework Core within an ASP.NET Core application.
By the end of this guide, you will understand:
What the Repository Pattern is and why it's useful.
How to implement a Generic Repository.
How to integrate the repository with Entity Framework Core.
Best practices and performance optimizations.
Understanding the Repository Pattern
The Repository Pattern abstracts the data access logic from the business logic, providing a clean separation of concerns. It acts as an intermediary between the application and the database, ensuring:
Decoupling of data access logic from business logic.
Encapsulation of query logic within a single interface.
Better testability by allowing mocking of data repositories.
When Should You Use It?
While Entity Framework Core already provides the DbContext as a unit of work, the repository pattern is still beneficial when:
You need to create a reusable data access layer.
You want to enforce consistent querying logic.
You are working on a Domain-Driven Design (DDD) project.
Implementing a Generic Repository in C#
A Generic Repository allows you to work with multiple entities without writing redundant code. Let's start by defining our base repository interface.
Step 1: Define the Generic Repository Interface
Create an interface named IGenericRepository<T>
where T
is a generic type constraint for Entity Framework entities.
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);
}
Step 2: Implement the Generic Repository
Now, implement the GenericRepository<T>
class that interacts with DbContext.
public class GenericRepository<T> : IGenericRepository<T> where T : class
{
private readonly DbContext _context;
private readonly DbSet<T> _dbSet;
public GenericRepository(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 AddAsync(T entity)
{
await _dbSet.AddAsync(entity);
await _context.SaveChangesAsync();
}
public void Update(T entity)
{
_dbSet.Update(entity);
_context.SaveChanges();
}
public void Delete(T entity)
{
_dbSet.Remove(entity);
_context.SaveChanges();
}
}
Integrating the Repository with Entity Framework Core
To use this repository with Entity Framework Core, create a DbContext and set up dependency injection in ASP.NET Core.
Step 3: Define the Application DbContext
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options) { }
public DbSet<Product> Products { get; set; }
}
Step 4: Register the Repository in Dependency Injection
Modify the Program.cs
file to register the Generic Repository:
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddScoped(typeof(IGenericRepository<>), typeof(GenericRepository<>));
This allows ASP.NET Core's Dependency Injection (DI) to provide an instance of IGenericRepository<T>
when needed.
Implementing a Specific Repository
Although a Generic Repository handles most cases, sometimes entity-specific operations are needed. For example, let’s create a ProductRepository
that extends GenericRepository<Product>
.
public interface IProductRepository : IGenericRepository<Product>
{
Task<IEnumerable<Product>> GetTopSellingProductsAsync(int count);
}
public class ProductRepository : GenericRepository<Product>, IProductRepository
{
private readonly ApplicationDbContext _context;
public ProductRepository(ApplicationDbContext context) : base(context)
{
_context = context;
}
public async Task<IEnumerable<Product>> GetTopSellingProductsAsync(int count)
{
return await _context.Products.OrderByDescending(p => p.Sales).Take(count).ToListAsync();
}
}
Then, register it in Dependency Injection:
builder.Services.AddScoped<IProductRepository, ProductRepository>();
Unit Testing the Generic Repository
To make our repository testable, we should use an in-memory database like InMemoryDatabase
from Microsoft.EntityFrameworkCore.
Step 5: Writing a Unit Test
Install the package:
dotnet add package Microsoft.EntityFrameworkCore.InMemory
Create a test class:
public class GenericRepositoryTests
{
private readonly IGenericRepository<Product> _repository;
private readonly ApplicationDbContext _context;
public GenericRepositoryTests()
{
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase(databaseName: "TestDb")
.Options;
_context = new ApplicationDbContext(options);
_repository = new GenericRepository<Product>(_context);
}
[Fact]
public async Task AddAsync_ShouldAddEntity()
{
var product = new Product { Name = "Laptop", Price = 1200 };
await _repository.AddAsync(product);
var retrieved = await _repository.GetByIdAsync(product.Id);
Assert.NotNull(retrieved);
Assert.Equal("Laptop", retrieved.Name);
}
}
Conclusion
A Generic Repository in C# helps simplify data access while improving maintainability and testability. By implementing this pattern with Entity Framework Core in an ASP.NET Core application, you can create a reusable, structured, and scalable data access layer.
Key Takeaways:
The Repository Pattern abstracts data access logic.
A Generic Repository reduces code duplication.
Entity-specific repositories allow custom operations.
Dependency Injection and Unit Testing improve maintainability.
By following these best practices, you can build robust and maintainable C# applications.