DEV Community

Cover image for Gentle introduction to Generic Repository Pattern with C#
Karen Payne
Karen Payne

Posted on • Updated on

Gentle introduction to Generic Repository Pattern with C#

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#)

Top comments (9)

Collapse
 
dipayansukul profile image
Dipayan Sukul

Good document. But in real life atleast all my projects never had to use this kind of generic repository with so many CRUD only functions.

Anyways any application which needs CRUD(normally it is most often used in hobby projects), this repository pattern is really useful and needful.

Collapse
 
karenpayneoregon profile image
Karen Payne

Agree with your statements but the idea is for consistency when a diverse team of developers come together to write code with different styles. If we took say working with Dapper as an alternative this would be better suited to this repository pattern rather than just show EF Core.

For the record I expected comment like this.

Collapse
 
dipayansukul profile image
Dipayan Sukul

Agreed. Other ORMs doesn't have this kind of patterns built in.

But my view was do we really need this in real life application? I am developing 10+ years but hardly needed so many functionalities .

Thread Thread
 
karenpayneoregon profile image
Karen Payne

I've been a developer for 30 plus years and have used it on four projects with teams of ten developers each time.

Thread Thread
 
dipayansukul profile image
Dipayan Sukul • Edited

That proves my point mate. 30+ years and just 4 projects. You just replied the ratio and chances of usage.

Moreover you may not agree on my points. That's totally acceptable. But there should be a window to be a listener.

I am out of this topic.
Thank you 🙏

Thread Thread
 
karenpayneoregon profile image
Karen Payne

Yet I know those developers have moved on and most likely took this practice with them. Personally I have only used this with Dapper. I've not used it with EF Core because the majority of my contract work is Cold Fusion.

Collapse
 
danjrwalsh_81 profile image
Dan Walsh

EF Core itself largely implements the repository and unit-of-work patterns. If you're using EF within a project, wrapping it inside another generic repository layer adds little value and is mostly pointless abstraction for the sake of abstraction, especially if it's just a basic layer that just calls EF's DbContext and DbSet equivalents (which lets be honest, is most of them).

One of the major benefits of using EF is to eliminate the need to write much of the traditional DAL boilerplate. I stopped writing custom repository layers inside my .NET projects utilizing EF years ago and will never go back.

There can be use cases for it but 95% of the repository layer implementations I've seen that use EF under the hood were almost entirely pointless.

Collapse
 
karenpayneoregon profile image
Karen Payne

Agree with your statements but the idea is for consistency when a diverse team of developers come together to write code with different styles. If we took say working with Dapper as an alternative this would be better suited to this repository pattern rather than just show EF Core.

For the record I expected comment like this.

Collapse
 
kappyz0r profile image
João Silva

I have to agree that Repositories are greatly overrated and to abstract EF the value it adds is close to none.
More so, Generic Repository is even worst. Yes, it can save development time, but if you tables with millions of rows, exposing Get all's without mandatory filters or paging can throw a (junior) developer off.
On saving (write) you can get way with with but on Query (read), Each entity has its own way of being queried.

A great alternative is encapsulating queries on their own objects. Used it on one project and looked really cool. If you don't want to create your own, see Ardalis specification package. Really cool stuff