Design Patterns – Repository Pattern in C#
- Posted by Muthukumar Dharmar
- Categories Design Patterns, EF Core
- Date July 17, 2021
In this article, we will discuss the repository pattern. Then we will have a walk-through of the step-by-step implementation of a Repository Pattern in a C# project.
Data Access Layer
It is good we have several ORM mappers like, Dapper, EntityFramework, etc., These libraries are doing all the heavy lifting for us and help us to communicate with the database by simply using the programming language that we know.
Typical Usage
In EntityFramework our DbContext class acts as a Data Access Layer.
In the below example we are injecting the DbContext in our Controller class, by doing this we are making our business layer directly dependent on our Data Access Layer, in other words tightly coupled with the DbContext class. Still this approach work.
#region Constructor public CustomersController(DbContext context) { this.dbContext = context; } #endregion Constructor
Disadvantages
It’s hard to Unit Test our Controllers/Services with the dependency of DbContext, we don’t need to perform actual database operations while executing our Unit Test cases right, All we need is to test the logic inside Controller actions.
Let’s say, at some point, there is a decision from management to migrate the database with some latest technology. Along with this change we have to update all our Controller classes, right. this may result in some regressions.
Decoupling with Repository Pattern
Actually, the Repository Pattern solves the above-mentioned problem by standing as a mediator between the Consumer and the Data Access Layer. As you all know, in any application a typical data access operation could be Create, Read, Update & Delete(CRUD), So we need to perform CRUD operation for each and every Entity of our Domain Models.
Repeating these functions for all the entities, kind of a duplication of work.
We will take advantage of the C# Generics technique to reuse the logic and avoid code duplications.
Step1: Interface Creation
We need to define an interface with these common operations.
public interface IRepository<TEntity> : IRepository<TEntity, long> where TEntity: IEntityBase { } public interface IRepository<TEntity, TKey> where TEntity : IEntityBase<TKey> { TEntity GetItemById(TKey id); IQueryable<TEntity> GetEntities(Expression<Func<TEntity, bool>> filterPredicate); int Count(Expression<Func<TEntity, bool>> filterPredicate); void Add(TEntity entity); void Update(TEntity entity); void Delete(TEntity entity); void DeleteById(TKey id); }
Step2: Generic Repository Class Creation
Create a Repository base class with the actual implementation.
public abstract class Repository<TEntity> : Repository<TEntity, long> where TEntity : EntityBase, IEntityBase { protected Repository(DbContext dbContext) : base(dbContext) { } } public abstract class Repository<TEntity, TKey> : IRepository<TEntity, TKey> where TEntity : EntityBase<TKey>, IEntityBase<TKey> { protected readonly DbContext dbContext; public Repository(DbContext dbContext) { this.dbContext = dbContext; } public virtual TEntity GetItemById(TKey id) { return this.dbContext.Set<TEntity>().Find(id); } public virtual IQueryable<TEntity> GetEntities(Expression<Func<TEntity, bool>> filterCriteria = null) { return filterCriteria != null ? this.dbContext.Set<TEntity>().AsNoTracking().Where(filterCriteria) : this.dbContext.Set<TEntity>().AsNoTracking(); ; } public virtual int Count(Expression<Func<TEntity, bool>> predicate) { return this.dbContext.Set<TEntity>().AsNoTracking().Where(predicate).Count(); } public virtual void Add(TEntity entity) { this.dbContext.Set<TEntity>().Add(entity); } public virtual int SaveChanges() { return this.dbContext.SaveChanges(); } public virtual void Update(TEntity entity) { this.dbContext.Set<TEntity>().Update(entity); } public virtual void Delete(TEntity entity) { this.dbContext.Set<TEntity>().Remove(entity); } public virtual void DeleteById(TKey id) { TEntity entity = this.GetItemById(id); this.dbContext.Set<TEntity>().Remove(entity); } }
Step3: Actual Repository Creation
Now we can simply inherit from our Generic Repository base class which will automatically provide all the basic CRUD operations to manipulate the data from the database.
public class CustomerRepository : Repository<Customer> { public CustomerRepository(DbContext dbContext) : base(dbContext) { } } public class ProductRepository : Repository<Product> { public ProductRepository(DbContext dbContext) : base(dbContext) { } }
Step4: Update the Controller
Now inject our Repository class using the IRepository interface and update all the actions with the repository methods.
private readonly IRepository<Customer> customerRepo; #region Constructor public CustomersController(IRepository<Customer> customerRepo) { this.customerRepo = customerRepo; } #endregion Constructor
Advantages
Now it is simple to Unit Test this controller class by making a mock object for this Repository object.
Even if there is a decision on moving to any new technology, Still there won’t be any change in the operations of our repository class and we don’t have to make changes to our Controllers.
All we have to do is update our mediator. Yes, we just need to update our Generic Repository class and everything works as before. If you notice how the Repository pattern helps to encapsulate the actual data access logic, So that the Business Layer doesn’t need to know anything about our storage layer, it could be anything like SQL, Oracle, NoSQL, FileSysem, etc.,
Hope you have enjoyed this article 🙂