Gentle introduction to Generic Repository Pattern with C#
Published: 2024-01-20 20:02:02

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.

{% cta https://github.com/karenpayneoregon/generic-repository-samples %} GitHub Source code {% endcta %}

:green_circle: 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.

```csharp
public interface IBase 
{
    public int Id { get; }
}
```

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

```csharp
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);
}
```
 
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.

```csharp
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;
}
```

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

```csharp
public class ProductsRepository : IGenericRepository<Product>
```

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

```csharp
public IEnumerable<Countries> GetAll()
{
    throw new NotImplementedException();
}
```
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.

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

    public ProductsRepository(Context context)
    {
        _context = context;
    }
```
It is also good to clean up afterwards so we implement IDisposable.

```csharp
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);
    }
}
```

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

```csharp
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);
    }
}
```
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](https://stackoverflow.com/a/55664975/5509738). Note that their interface is similar to the interface shown above.

```csharp
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();
}
```
Then there is a base repository which unlike the one shown above is used for all entities in the ORM.

```csharp
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();
    }
}
```

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.
1. Using the ProductsRepository
   1. Use the GetAll method to get all products
   1. Use GetByIdWithIncludeAsync to get a product
   1. Delete the product with id of 4
   1. Add a new product with the Add method
   1. Save changes and check for 2 returned from save changes, one for a delete, one for an add.
   1. Edit a product
   2. Save changes.


![results of demo code](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bp7bgssar09vemb6wozn.png)

## 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](https://github.com/karenpayneoregon/generic-repository-samples).

## See also

[Gentle introduction for generics (C#)](https://dev.to/karenpayneoregon/gentle-introduction-for-generics-c-4232)

{% embed https://dev.to/karenpayneoregon %}