Think with Enlab

Diving deep into the ocean of technology

Stay Connected. No spam!
  • Home
  • Development
  • Implement Repository & Unit of Work patterns in .NET Core with practical examples [Part 2]

Implement Repository & Unit of Work patterns in .NET Core with practical examples [Part 2]

 

Hi, back to the topic Repository & Unit of Work pattern, last time I explained the concept of what a repository and Unit of Work are and how to apply it to a project. Today we will continue to extend this topic to use the pattern in a typical microservices architecture.

Generic Repository & Generic Unit of Work in microservice architect

Let's start building a recruiting system consisting of 4 big businesses: Recruiters, Training unit, Worker, and Management. These services are independent of each other, and each service has its database and business domain. However, these services have a close relationship. It is very beneficial to apply the microservices architecture to implement this system. The separation of large businesses into services operating independently of each other helps us be more flexible in developing concurrently between teams, avoiding concentrating bandwidth on a service, highly scalable, and easy to maintain.

Applying Repository & Unit of Work pattern to this microservices system, we will need to implement each service, and we might encounter the code duplication problem. We can solve this problem by introducing some kind of generic repository and unit of work. We will need to implement the Generic Repository and Unit of Work classes and other common code to share between the services. The following diagrams illustrate this idea.

 

Generic Repository

 

The overall structure of each service contains three layers, API, domain, and infrastructure. We will implement all the common codes in shared projects and will inherit them in all other services.

 

Generic Repository & Unit of Work

 

The Generic Repository & Unit of Work is implemented in the shared service with almost all repositories other services need. The generic Unit of Work is responsible for saving data changes made by all repositories in a particular service.

Microservices will inherit these base classes and use them.

We will move all the base classes in part 1 into the Shared service as follows:

Shared.Domain

  • IDbFactory.cs
public interface IDbFactory where TContext : DbContext { TContext Context { get; } }
Code language: C# (cs)
  • IRepository.cs
public interface IRepository where T: IEntityBase { void Add(T entity); void Delete(T entity); void Update(T entity); IQueryable List(Expression<Func<T, bool>> expression); }
Code language: C# (cs)
  • IUnitOfWork.cs
public interface IUnitOfWork where TContext: DbContext { Task CommitAsync(); TContext Context { get; } }
Code language: C# (cs)

Shared.Infrastructure

  • DbFactoryBase.cs
public class DbFactoryBase : IDbFactory where TContext: DbContext { private bool _disposed; private Func _instanceFunc; private TContext _context; public TContext Context => _context ?? (_context = _instanceFunc.Invoke()); public DbFactoryBase(Func dbContextFactory)
Code language: C# (cs)
  • RepositoryBase.cs
public class RepositoryBase<TEntity, TContext> : IRepository where TEntity : class, IEntityBase where TContext : DbContext { private readonly DbFactoryBase _dbFactory; protected DbSet _dbSet; protected DbSet DbSet { get => _dbSet ?? (_dbSet = _dbFactory.Context.Set()); } public RepositoryBase(DbFactoryBase dbFactory) { _dbFactory = dbFactory; } public virtual void Add(TEntity entity) { DbSet.Add(entity); } public virtual void Delete(TEntity entity) { DbSet.Remove(entity); } public virtual IQueryable List(Expression<Func<TEntity, bool>> expression) { return DbSet.Where(expression); } public virtual void Update(TEntity entity) { DbSet.Update(entity); } }
Code language: C# (cs)
  • UnitOfWorkBase.cs
public class UnitOfWorkBase : IUnitOfWork where TContext : DbContext { private DbFactoryBase _dbFactory; public UnitOfWorkBase(DbFactoryBase dbFactory) { _dbFactory = dbFactory; } public TContext Context { get; } public Task CommitAsync() { return _dbFactory.Context.SaveChangesAsync(); } }
Code language: C# (cs)

Management.Domain

  • IAppRepository.cs
public interface IAppRepository : IRepository where T : IEntityBase { }
Code language: C# (cs)
  • IAppUnitOfWork.cs
public interface IAppUnitOfWork { Task CommitAsync(); }
Code language: C# (cs)

 Management.Infrastructure

  • AppDbFactory.cs
public class AppDbFactory : DbFactoryBase { public AppDbFactory(Func dbContextFactory) : base(dbContextFactory) { } }
Code language: C# (cs)
  • AppRepository.cs
public class AppRepository : RepositoryBase<T, AppDbContext> , IAppRepository where T : EntityBase { public AppRepository(AppDbFactory dbFactory) : base(dbFactory) { } }
Code language: C# (cs)
  • AppUnitOfWork.cs
public class AppUnitOfWork : UnitOfWorkBase, IAppUnitOfWork { public AppUnitOfWork(AppDbFactory dbFactory) : base(dbFactory) { } }
Code language: C# (cs)

 In each service, just inherit the RepositoryBase, DbFactoryBase, UnitOfWorkBase classes and extend them to each project’s specifics without affecting the shared services.

Congratulations, you have successfully designed your Generic Repository & Unit of Work.

 

Audit log with repositories

Suppose that the customer requests to add the feature to track all end-user activities that made changes to any database table.

In part 1, we have created three repositories for Management (DepartmentRepository, SalaryRepository, UserRepository). Now update them to fulfill the requirements.

  • DepartmentReposiroty.cs
public class DepartmentRepository : AppRepository, IDepartmentRepository { private readonly IHttpContextAccessor _contextAccessor; public DepartmentRepository(AppDbFactory dbFactory , IHttpContextAccessor contextAccessor) : base(dbFactory) { _contextAccessor = contextAccessor; } public Department AddDepartment(string departmentName) { var department = new Department(departmentName); if (department.ValidOnAdd()) { department.CreatedBy = _contextAccessor.HttpContext.User.Identity.Name; department.CreatedDate = DateTime.UtcNow; base.Add(department); return department; } else throw new Exception("Department invalid"); } }
Code language: C# (cs)
  • SalaryRepository.cs
public class SalaryRepository : AppRepository, ISalaryRepository { private readonly IHttpContextAccessor _contextAccessor; public SalaryRepository(AppDbFactory dbFactory , IHttpContextAccessor contextAccessor) : base(dbFactory) { _contextAccessor = contextAccessor; } public Salary AddUserSalary(User user, float coefficientsSalary, float workdays) { var salary = new Salary(user, coefficientsSalary, workdays); if (salary.ValidOnAdd()) { salary.CreatedBy = _contextAccessor.HttpContext.User.Identity.Name; salary.CreatedDate = DateTime.UtcNow; base.Add(salary); return salary; } else throw new Exception("Salary invalid"); } }
Code language: C# (cs)

With the above update, it pretty much does the job. However, if the system continues to grow in the future, there will be many different add/delete/update methods to update, and the code will be scattered across the projects. There will be shortcomings, ignorance, or failure of developers that can make this feature incomplete, and it will take a lot of time to write for all methods.

So what's the solution? The answer is we need to provide a base entity to serve this, and all entities assigned from this interface will be given information each time they are added/deleted/updated.

Now let’s start with the base classes

  • Shared.Domain/Interfaces/IAuditEntity.cs
public interface IAuditEntity { DateTime CreatedDate { get; set; } string CreatedBy { get; set; } DateTime? UpdatedDate { get; set; } string UpdatedBy { get; set; } void SetCreator(string creator, DateTime dateTime); void SetUpdater(string updater, DateTime dateTime); } public interface IAuditEntity : IAuditEntity, IDeleteEntity { }
Code language: C# (cs)
  • Shared.Domain.Base/AuditEntity.cs
public abstract class AuditEntity : DeleteEntity, IAuditEntity { public DateTime CreatedDate { get; set; } public string CreatedBy { get; set; } public DateTime? UpdatedDate { get; set; } public string UpdatedBy { get; set; } public void SetCreator(string creator, DateTime dateTime) { this.CreatedBy = creator; this.CreatedDate = dateTime; } public void SetUpdater(string updater, DateTime dateTime) { this.UpdatedBy = updater; this.UpdatedDate = dateTime; } } public abstract class AuditEntity : DeleteEntity, IAuditEntity { public DateTime CreatedDate { get; set; } public string CreatedBy { get; set; } public DateTime? UpdatedDate { get; set; } public string UpdatedBy { get; set; } public void SetCreator(string creator, DateTime dateTime) { this.CreatedBy = creator; this.CreatedDate = dateTime; } public void SetUpdater(string updater, DateTime dateTime) { this.UpdatedBy = updater; this.UpdatedDate = dateTime; } }
Code language: C# (cs)
  • Shared/Infrastructure/RepositoryBase.cs, constructor:
protected string CurrentUser => _accessor.HttpContext.User.Identity.Name; public RepositoryBase(DbFactoryBase dbFactory , IHttpContextAccessor accessor) { _accessor = accessor; _dbFactory = dbFactory; }
Code language: C# (cs)

Add:

public virtual void Add(TEntity entity) { // if the entity is of type AuditLog, assign creation info to it if (typeof(IAuditEntity).IsAssignableFrom(typeof(TEntity))) { var audit = (IAuditEntity)entity; audit.SetCreator(CurrentUser, DateTime.UtcNow); } DbSet.Add(entity); }
Code language: C# (cs)

Update:

public virtual void Update(TEntity entity) { // if the entity is of type AuditLog, assign modification info to it if (typeof(IAuditEntity).IsAssignableFrom(typeof(TEntity))) { var audit = (IAuditEntity)entity; audit.SetUpdater(CurrentUser, DateTime.UtcNow); } DbSet.Update(entity); }
Code language: C# (cs)

 Try to add/update/remove any tables that inherit the IAudiEntity to check the results.

Multi-Tenancy

Many systems are built to sell the same system’s services to several businesses, shops, factories, etc. These systems are called Multi-Tenant systems. The three most popular database design solutions are Separate Database, Separate Schema, and Shared Schema.

We will not cover each approach’s pros/cons here, and we will focus on the last one because almost every schema is shared among all tenants, but data must be completely separated between tenants.

 

Multi-Tenancy

 

From the above diagram, we expect the commands and queries to add tenant information before sending it to the database.

  • Commands: Add/Update/Delete commands will be added tenant information before sending them to the database for execution.
  • Queries: Before getting data, condition filters will be added to queries by a tenant before sending them to the database for retrieving data.

With the above scenario, using the Repository and Unit of Work, we can ensure to add tenant information before sending it to the database for execution. This will ensure data separation between tenants.

Let's start building a TenantRepository that includes CRUD methods, and of course, it will be inherited from the RepositoryBase we created earlier.

  • Shared.Domain/Interfaces/EntitiesBase/ITenantEntity.cs
public interface ITenantEntity : IEntityBase { int TenantId { get; set; } } public interface ITenantEntity : ITenantEntity, IEntityBase { }
Code language: C# (cs)
  • Shared.Domain/Base/TenantEntity.cs 
public class TenantEntity : EntityBase, ITenantEntity { public int TenantId { get; set; } } public class TenantEntity : EntityBase, ITenantEntity { public int TenantId { get; set; } }
Code language: C# (cs)
  • Shared.Domain/Interfaces/ITenantRepository.cs
public interface ITenantRepository : IRepository where T : IEntityBase { }
Code language: C# (cs)
  • Shared.Infrastructure/TenantRepository.cs
public class TenantRepository<TEntity, TContext> : RepositoryBase<TEntity, TContext>, ITenantRepository where TEntity : class, ITenantEntity where TContext : DbContext { public TenantRepository(DbFactoryBase dbFactory , IHttpContextAccessor accessor) : base(dbFactory, accessor) { } public override void Add(TEntity entity) { entity.TenantId = CurrentTenant; base.Add(entity); } public override IQueryable List(Expression<Func<TEntity, bool>> expression) { var query = base.List(expression); return query.Where(e => e.TenantId == CurrentTenant); } private int CurrentTenant { get { var httpContext = _accessor.HttpContext; var tenantKey = "TenantId"; if (httpContext.User.HasClaim(a => a.Type == tenantKey)) { var claim = httpContext.User.FindFirst(tenantKey); if (int.TryParse(claim.Value, out int tenantId)) return tenantId; } throw new Exception("The request not exist Tenant info."); } } }
Code language: C# (cs)

The above code lines are written assuming that the user login information always has tenant information per each request to the system. This helps identify the request from what user for what tenant.

Lastly, update the UnitOfWork class to provide the repository instantiation ability.

public class UnitOfWorkBase : IUnitOfWork where TContext : DbContext { private readonly IHttpContextAccessor _contextAccessor; private DbFactoryBase _dbFactory; public UnitOfWorkBase(DbFactoryBase dbFactory , IHttpContextAccessor contextAccessor) { _contextAccessor = contextAccessor; _dbFactory = dbFactory; } public TContext Context { get; } public Task CommitAsync() { return _dbFactory.Context.SaveChangesAsync(); } public IRepository Repository() where TEntity : class, IEntityBase { var service = _contextAccessor.HttpContext.RequestServices.GetService(typeof(IRepository)); return service != null ? (IRepository)service : null; } public ITenantRepository TenantRepository() where TEntity : class, ITenantEntity { var service = _contextAccessor.HttpContext.RequestServices.GetService(typeof(ITenantRepository)); return service != null ? (ITenantRepository)service : null; } }
Code language: C# (cs)

That’s pretty much all the necessary code to apply the Generic Repository and Unit of Work patterns to a microservices project.

Happy coding and have a nice day!

 

Contact us

About the author

Loc Nguyen

Loc’s a senior backend developer with a laser focus on Microsoft technologies (ASP .NET, Xamarin, WPF), DevOps (GitLab CI/CD, AWS DevOps, Docker), Design Patterns (DDD design, Clean Architect, Microservices, N-Tiers). With his +6-year experience, Loc’s happy to discuss topics like OOP, Data structures & Algorithms, and more.

Up Next

How-to-create-real-time-chat-applications-using-WebSocket-APIs-in-API-Gateway
March 26,2021 by Trong Pham
My experience developing real-time messaging applications leveraging AWS services inspires me to share with the...
Blog-Featured-Image-800x600px-10
March 11,2021 by Uyen Luu
Hi, back to the architecture patterns, in the last article I explained what the 3-layer...
Blog-Featured-Image-800x600px-12
February 27,2021 by Uyen Luu
What’s 3-layer architecture? Layer indicates the logical separation of components. Layered architecture concentrates on...
Top logging frameworks for .NET applications
January 19,2021 by Tan Nguyen
In software development, logging is an essential part that helps you to monitor the system....
Roll to Top

Can we send you our next blog posts? Only the best stuffs.

Subscribe