Think with Enlab

Diving deep into the ocean of technology

Stay Connected. No spam!

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 codes 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<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.

 

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<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!

 

CTA Enlab Software

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.
Frequently Asked Questions (FAQs)
What are the Advantages of Using the Repository and Unit of Work Patterns in Microservices Architecture?

Implementing Repository and Unit of Work patterns in microservices architecture provides clear separation of concerns, promotes code reusability, and enhances maintainability. These patterns abstract the data layer, allowing microservices to be more flexible and independent in terms of data operations. This approach helps in managing data consistency across services and simplifies transaction management.

How to Implement Generic Repository and Unit of Work in Microservices Using .NET Core?

To implement these patterns in .NET Core microservices, start by creating base classes for the repository and unit of work in a shared service. Each microservice will then inherit from these base classes. This setup allows you to define common data operations in one place and share them across different microservices, thereby reducing code duplication and ensuring consistency.

Can Repository and Unit of Work Patterns Handle Complex Queries in Microservices?

Yes, these patterns are capable of handling complex queries. The Repository pattern allows encapsulation of query logic, making it easier to manage complex data operations. The Unit of Work, on the other hand, ensures that these operations are executed in a consistent manner. For complex scenarios, you can extend repositories with custom methods to handle specific requirements.

What are the Challenges of Implementing Repository and Unit of Work Patterns in Microservices?

Implementing these patterns in a microservices architecture can introduce challenges like ensuring data consistency across services, managing distributed transactions, and handling data schema changes. It requires careful design

to ensure that services remain loosely coupled and that the data layer abstraction does not become a bottleneck or overly complex. Proper implementation is crucial to avoid tight coupling and to maintain the flexibility and scalability inherent in microservices.

How Do Repository and Unit of Work Patterns Support Scalability and Maintainability in Microservices?

The Repository and Unit of Work patterns support scalability by decoupling the data access logic from the business logic. This separation allows microservices to scale independently based on their specific data access patterns and load. Furthermore, it simplifies the maintenance of the system as changes to the database or data access logic can be managed in one place without affecting the entire microservice architecture.

Up Next

Big Data Technologies Transforming Software Development
July 05, 2024 by Dat Le
In the rapidly evolving world of software development, Big Data stands out as a transformative force....
June 27, 2024 by Dat Le
In today's rapidly evolving digital landscape, secure coding practices are paramount to safeguarding applications from a...
June 20, 2024 by Dat Le
In the rapidly evolving digital landscape, the role of User Interface (UI) and User Experience (UX)...
Leveraging UX Design Principles in Software Development
June 17, 2024 by Dat Le
In the dynamic world of software development, one element has emerged as crucial to success: User...
Roll to Top

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

Subscribe