Gentle introduction to Generic Repository Pattern with C#

Karen Payne - Jan 20 - - Dev Community

The Generic Repository pattern in C# is a design pattern that abstracts the application’s data layer, making it easier to manage data access logic across different data sources and prjects. It aims to reduce redundancy by implementing common data operations in a single, generic repository rather than having separate repositories for each entity type. Type of project can be desktop or web. And when web, considerations on implementation using dependency injection. In the samples provided a console project is used to reach a wider audience and dependency injection is not used to keep focus on how to write a generic repository.

  • A generic repository begins with a generic interface defining common operations for instance CRUD operations. These operations are defined in a generic way, applicable to any entity type.
  • The generic interface is then implemented in a concrete class. This class handles the data source interactions, such as querying a database using an ORM like EF Core or Dapper.
  • The implementation will often utilize an Entity Framework Core DbContext or Dapper with IDbConnection to interact with a database. ## Creating the generic repository

A common repository will provide CRUD functionality, select all records, a single record, insert a new record with the option to return the new primary key, update and delete single or batch set of records.

GitHub Source code

🟢 No matter if the code will be used by a single developer or a team of developers it is critical to consider what should be included and naming conventions as the idea here is consistency when the idea is to be used in more than one project.

First a base interface is defined so that if there is a need to iterate data using a primary key, Id will be used for consistency and is not part of the generic interface.

public interface IBase 
{
    public int Id { get; }
}
Enter fullscreen mode Exit fullscreen mode

The generic interface. If you don't care for method names feel free to change them along with adding or removing methods.

public interface IGenericRepository<T> where T : class
{
    IEnumerable<T> GetAll();
    Task<List<T>> GetAllAsync();
    T GetById(int id);
    T GetByIdWithIncludes(int id);
    Task<T> GetByIdAsync(int id);
    Task<T> GetByIdWithIncludesAsync(int id);
    bool Remove(int id);
    void Add(in T sender);
    void Update(in T sender);
    int Save();
    Task<int> SaveAsync();
    public T Select(Expression<Func<T, bool>> predicate);
    public Task<T> SelectAsync(Expression<Func<T, bool>> predicate);
}
Enter fullscreen mode Exit fullscreen mode

The following models will be used, one will be created with all methods from the interface below and two others are there for the reader to practice with.

public class Category : IBase
{
    public int Id => CategoryId;
    public int CategoryId { get; set; }
    public string? Name { get; set; }
    public virtual ICollection<Product>? Products { get; set; }
    public override string? ToString() => Name;
}

public partial class Countries : IBase
{
    public int Id => CountryId;
    [Key]
    public int CountryId { get; set; }
    public string Name { get; set; }
    public override string ToString() => Name;
}

public class Product : IBase
{
    public int Id => ProductId;
    public int ProductId { get; set; }
    public string? Name { get; set; }
    public int CategoryId { get; set; }
    public virtual Category Category { get; set; } = null!;
    public override string? ToString() => Name;
}
Enter fullscreen mode Exit fullscreen mode

To implement the interface for Production operations, create a new class named ProductsRepository. Change the class signature as follows.

public class ProductsRepository : IGenericRepository<Product>
Enter fullscreen mode Exit fullscreen mode

At this point Visual Studio will prompt to create missing methods, allow Visual Studio to create the methods. Each method will be stubbed out ready for your code

public IEnumerable<Countries> GetAll()
{
    throw new NotImplementedException();
}
Enter fullscreen mode Exit fullscreen mode

As this example uses EF Core with a DbContext and DbSets in the same class project we can initialize the DbContext named Context here as follows so each time the repository is needed the DbContext is ready.

public class ProductsRepository : IGenericRepository<Product>
{
    private Context _context;

    public ProductsRepository(Context context)
    {
        _context = context;
    }
Enter fullscreen mode Exit fullscreen mode

It is also good to clean up afterwards so we implement IDisposable.

public class ProductsRepository : IGenericRepository<Product>, 
    IDisposable
{

...

    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                _context.Dispose();
            }
        }
        _disposed = true;
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}
Enter fullscreen mode Exit fullscreen mode

From here, write code for each method so that when done you end up with something like this.

public class ProductsRepository : IGenericRepository<Product>, 
    IDisposable
{
    private Context _context;

    public ProductsRepository(Context context)
    {
        _context = context;
    }

    public IEnumerable<Product> GetAll()
    {
        return _context.Products.ToList();
    }

    public Task<List<Product>> GetAllAsync()
    {
        return _context.Products.ToListAsync();
    }

    public Product GetById(int id)
    {
        return _context.Products.Find(id);
    }

    public Product GetByIdWithIncludes(int id)
    {
        return _context.Products.Include(x => x.Category)
            .FirstOrDefault(x => x.ProductId == id);
    }

    public async Task<Product> GetByIdAsync(int id)
    {
        return await _context.Products.FindAsync(id);
    }

    public async Task<Product> GetByIdWithIncludesAsync(int id)
    {
        return await _context.Products.Include(x => x.Category)
            .FirstOrDefaultAsync(x => x.ProductId == id);
    }

    public bool Remove(int id)
    {
        var product = _context.Products.Find(id);
        if (product is { })
        {
            _context.Products.Remove(product);
            return true;
        }

        return false;
    }

    public void Add(in Product sender)
    {
        _context.Add(sender).State = EntityState.Added;
    }

    public void Update(in Product sender)
    {
        _context.Entry(sender).State = EntityState.Modified;
    }

    public int Save()
    {
        return _context.SaveChanges();
    }

    public Task<int> SaveAsync()
    {
        return _context.SaveChangesAsync();
    }

    public Product Select(
        Expression<Func<Product, bool>> predicate)
    {
        return _context.Products
            .WhereNullSafe(predicate).FirstOrDefault()!;
    }

    public async Task<Product> SelectAsync(
        Expression<Func<Product, bool>> predicate)
    {
        return 
            (
                await _context.Products
                    .WhereNullSafe(predicate).FirstOrDefaultAsync())!;
    }
    private bool _disposed = false;

    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                _context.Dispose();
            }
        }
        _disposed = true;
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}
Enter fullscreen mode Exit fullscreen mode

What is missing from above? You can add more methods such as your database model included orders and order details and only wanted products for a specific order, simply add the method.

Put ten developers in a room and ask how they would use a generic repository we most likely get at least three different versions.

The following comes from a random Stackoverflow post. Note that their interface is similar to the interface shown above.

public interface IRepositoryBase<T> where T : class
{
    void Add(T objModel);
    void AddRange(IEnumerable<T> objModel);
    T? GetId(int id);
    Task<T?> GetIdAsync(int id);
    T? Get(Expression<Func<T, bool>> predicate);
    Task<T?> GetAsync(Expression<Func<T, bool>> predicate);
    IEnumerable<T> GetList(Expression<Func<T, bool>> predicate);
    Task<IEnumerable<T>> GetListAsync(Expression<Func<T, bool>> predicate);
    IEnumerable<T> GetAll();
    Task<IEnumerable<T>> GetAllAsync();
    int Count();
    Task<int> CountAsync();
    void Update(T objModel);
    void Remove(T objModel);
    void Dispose();
}
Enter fullscreen mode Exit fullscreen mode

Then there is a base repository which unlike the one shown above is used for all entities in the ORM.

public class RepositoryBase<TEntity> : IRepositoryBase<TEntity> where TEntity : class
{

    protected readonly Context _context = new();

    public void Add(TEntity model)
    {
        _context.Set<TEntity>().Add(model);
        _context.SaveChanges();
    }

    public void AddRange(IEnumerable<TEntity> model)
    {
        _context.Set<TEntity>().AddRange(model);
        _context.SaveChanges();
    }

    public TEntity? GetId(int id)
    {
        return _context.Set<TEntity>().Find(id);
    }

    public async Task<TEntity?> GetIdAsync(int id)
    {
        return await _context.Set<TEntity>().FindAsync(id);
    }

    public TEntity? Get(Expression<Func<TEntity, bool>> predicate)
    {
        return _context.Set<TEntity>().FirstOrDefault(predicate);
    }

    public async Task<TEntity?> GetAsync(Expression<Func<TEntity, bool>> predicate)
    {
        return await _context.Set<TEntity>().FirstOrDefaultAsync(predicate);
    }

    public IEnumerable<TEntity> GetList(Expression<Func<TEntity, bool>> predicate)
    {
        return _context.Set<TEntity>().Where<TEntity>(predicate).ToList();
    }

    public async Task<IEnumerable<TEntity>> GetListAsync(Expression<Func<TEntity, bool>> predicate)
    {
        return await Task.Run(() => _context.Set<TEntity>().Where<TEntity>(predicate));
    }

    public IEnumerable<TEntity> GetAll()
    {
        return _context.Set<TEntity>().ToList();
    }

    public async Task<IEnumerable<TEntity>> GetAllAsync()
    {
        return await Task.Run(() => _context.Set<TEntity>());
    }

    public int Count()
    {
        return _context.Set<TEntity>().Count();
    }

    public async Task<int> CountAsync()
    {
        return await _context.Set<TEntity>().CountAsync();
    }

    public void Update(TEntity objModel)
    {
        _context.Entry(objModel).State = EntityState.Modified;
        _context.SaveChanges();
    }

    public void Remove(TEntity objModel)
    {
        _context.Set<TEntity>().Remove(objModel);
        _context.SaveChanges();
    }

    public void Dispose()
    {
        _context.Dispose();
    }
}
Enter fullscreen mode Exit fullscreen mode

Which pattern to use? That is personal preference and experience.

Running the provided code

The project WorkingWithInterfacesApp1 defines the connection string in appsettings.json using .\SQLEXPRESS as the server. Either leave as is if this server is available or change it to an available server.

  1. Code in the DbContext will create the database and populate the tables defined in the DbContext.
  2. Using the ProductsRepository
    1. Use the GetAll method to get all products
    2. Use GetByIdWithIncludeAsync to get a product
    3. Delete the product with id of 4
    4. Add a new product with the Add method
    5. Save changes and check for 2 returned from save changes, one for a delete, one for an add.
    6. Edit a product
    7. Save changes.

results of demo code

Spreading your wings

Study the code for products than try your hand at working with one of the other models or take the two interfaces and try it in your project.

Not using EF Core

The generic repository pattern will work with Dapper for instances or a connection and command object using a DataTable also.

Source code

Clone the following GitHub repository.

See also

Gentle introduction for generics (C#)

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .