Building a Generic Repository in C#: Simplify Data Access

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.