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 codes to share between the services. The following diagrams illustrate this idea.
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.
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<TContext> where TContext : DbContext
{
TContext Context { get; }
}
- IRepository.cs
public interface IRepository<T> where T: IEntityBase
{
void Add(T entity);
void Delete(T entity);
void Update(T entity);
IQueryable<T> List(Expression<Func<T, bool>> expression);
}
- IUnitOfWork.cs
public interface IUnitOfWork<TContext> where TContext: DbContext
{
Task<int> CommitAsync();
TContext Context { get; }
}
Shared.Infrastructure
- DbFactoryBase.cs
public class DbFactoryBase<TContext> : IDbFactory<TContext> where TContext: DbContext
{
private bool _disposed;
private Func<TContext> _instanceFunc;
private TContext _context;
public TContext Context => _context ?? (_context = _instanceFunc.Invoke());
public DbFactoryBase(Func<TContext> dbContextFactory)
{
_instanceFunc = dbContextFactory;
}
public void Dispose()
{
if (!_disposed && _context != null)
{
_disposed = true;
_context.Dispose();
}
}
}
- RepositoryBase.cs
public class RepositoryBase<TEntity, TContext> : IRepository<TEntity>
where TEntity : class, IEntityBase
where TContext : DbContext
{
private readonly DbFactoryBase<TContext> _dbFactory;
protected DbSet<TEntity> _dbSet;
protected DbSet<TEntity> DbSet
{
get => _dbSet ?? (_dbSet = _dbFactory.Context.Set<TEntity>());
}
public RepositoryBase(DbFactoryBase<TContext> dbFactory)
{
_dbFactory = dbFactory;
}
public virtual void Add(TEntity entity)
{
DbSet.Add(entity);
}
public virtual void Delete(TEntity entity)
{
DbSet.Remove(entity);
}
public virtual IQueryable<TEntity> List(Expression<Func<TEntity, bool>> expression)
{
return DbSet.Where(expression);
}
public virtual void Update(TEntity entity)
{
DbSet.Update(entity);
}
}
- UnitOfWorkBase.cs
public class UnitOfWorkBase<TContext>
: IUnitOfWork<TContext>
where TContext : DbContext
{
private DbFactoryBase<TContext> _dbFactory;
public UnitOfWorkBase(DbFactoryBase<TContext> dbFactory)
{
_dbFactory = dbFactory;
}
public TContext Context { get; }
public Task<int> CommitAsync()
{
return _dbFactory.Context.SaveChangesAsync();
}
}
Management.Domain
- IAppRepository.cs
public interface IAppRepository<T> : IRepository<T> where T : IEntityBase
{
}
- IAppUnitOfWork.cs
public interface IAppUnitOfWork
{
Task<int> CommitAsync();
}
Management.Infrastructure
- AppDbFactory.cs
public class AppDbFactory : DbFactoryBase<AppDbContext>
{
public AppDbFactory(Func<AppDbContext> dbContextFactory) : base(dbContextFactory)
{
}
}
- AppRepository.cs
public class AppRepository<T>
: RepositoryBase<T, AppDbContext>
, IAppRepository<T>
where T : EntityBase
{
public AppRepository(AppDbFactory dbFactory) : base(dbFactory)
{
}
}
- AppUnitOfWork.cs
public class AppUnitOfWork : UnitOfWorkBase<AppDbContext>, IAppUnitOfWork
{
public AppUnitOfWork(AppDbFactory dbFactory) : base(dbFactory)
{
}
}
In each service, just inherit the RepositoryBase, DbFactoryBase, and 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<Department>, 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");
}
}
- SalaryRepository.cs
public class SalaryRepository : AppRepository<Salary>, 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");
}
}
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<TKey> : IAuditEntity, IDeleteEntity<TKey>
{
}
- 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<TKey> : DeleteEntity<TKey>, IAuditEntity<TKey>
{
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;
}
}
- Shared/Infrastructure/RepositoryBase.cs, constructor
protected string CurrentUser => _accessor.HttpContext.User.Identity.Name;
public RepositoryBase(DbFactoryBase<TContext> dbFactory
, IHttpContextAccessor accessor)
{
_accessor = accessor;
_dbFactory = dbFactory;
}
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);
}
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);
}
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.
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<T> : ITenantEntity, IEntityBase<T>
{
}
- Shared.Domain/Base/TenantEntity.cs
public class TenantEntity : EntityBase, ITenantEntity
{
public int TenantId { get; set; }
}
public class TenantEntity<TKey> : EntityBase<TKey>, ITenantEntity<TKey>
{
public int TenantId { get; set; }
}
- Shared.Domain/Interfaces/ITenantRepository.cs
public interface ITenantRepository<T> : IRepository<T>
where T : IEntityBase
{
}
- Shared.Infrastructure/TenantRepository.cs
public class TenantRepository<TEntity, TContext> :
RepositoryBase<TEntity, TContext>,
ITenantRepository<TEntity>
where TEntity : class, ITenantEntity
where TContext : DbContext
{
public TenantRepository(DbFactoryBase<TContext> dbFactory
, IHttpContextAccessor accessor) : base(dbFactory, accessor)
{
}
public override void Add(TEntity entity)
{
entity.TenantId = CurrentTenant;
base.Add(entity);
}
public override IQueryable<TEntity> 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.");
}
}
}
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<TContext>
: IUnitOfWork<TContext>
where TContext : DbContext
{
private readonly IHttpContextAccessor _contextAccessor;
private DbFactoryBase<TContext> _dbFactory;
public UnitOfWorkBase(DbFactoryBase<TContext> dbFactory
, IHttpContextAccessor contextAccessor)
{
_contextAccessor = contextAccessor;
_dbFactory = dbFactory;
}
public TContext Context { get; }
public Task<int> CommitAsync()
{
return _dbFactory.Context.SaveChangesAsync();
}
public IRepository<TEntity> Repository<TEntity>() where TEntity : class, IEntityBase
{
var service = _contextAccessor.HttpContext.RequestServices.GetService(typeof(IRepository<TEntity>));
return service != null ? (IRepository<TEntity>)service : null;
}
public ITenantRepository<TEntity> TenantRepository<TEntity>() where TEntity : class, ITenantEntity
{
var service = _contextAccessor.HttpContext.RequestServices.GetService(typeof(ITenantRepository<TEntity>));
return service != null ? (ITenantRepository<TEntity>)service : null;
}
}
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!