Think with Enlab

Diving deep into the ocean of technology

Stay Connected. No spam!

How to implement Repository & Unit of Work design patterns in .NET Core with practical examples [Part 1]

 

Repository and unit of work patterns in the concept

Almost all software requires a database system where it interacts (CRUD) to store and retrieve data. Many available technologies and frameworks on the market allow accessing databases smoothly and efficiently. To developers using Microsoft .NET technologies, Entity Framework or Dapper have gained popularity. However, the concern is not how to use these frameworks but how to write code using these frameworks that are reusable, maintainable, and readable. This article will briefly explain the repository and unit of work design pattern to solve all common problems with database access and business logic transactions. We also include several practical examples of how to implement them in a typical .NET core project.

 

What is a repository and unit of work design pattern?

Repositories are just classes that implement data access logic. It is generally implemented in the data access layer and provides data access service to the application layer. A repository represents a data entity (a table on the database), including common data access logic (CRUD) and other special logic. The application layer does not need to care about how it is implemented or how it interacts with the database. Instead, it only needs to consume available APIs published by the data access layer.

Meanwhile, a Unit of Work acts as a business transaction. In other words, the Unit of Work will merge all the transactions (Create/Update/Delete) of Repositories into a single transaction. All changes will be committed only once. They will be rolled back if any transaction fails to ensure data integrity. People often consider the Unit of Work a lazy evaluation transaction because it will not block any data table until it commits the changes.

 

Unit of Work

 

 

Why should we choose this pattern?

There are multiple data entities in an application that we need to maintain the data in most situations. They have some general functionalities that all entities should have (CRUD). Therefore, we will need a generic repository that can be applied to all entities in a given project rather than writing code for each entity that might cause code duplication problems. With a generic repository, we write one base class that handles all CRUD operations and inherit to write more entity-specific operations when necessary.

 

When should we use this pattern?

Any pattern has its pros/cons. Choosing a pattern depends on several aspects, such as the project's circumstances, the developers' skills, or technology limitations, to name a few. In our opinion, to apply the repository and unit of work patterns, we should consider the following factors:

  • Application size: Is it small or large? Are there any plans for expansion or maintenance in the future?
  • Previous works: Is there any different pattern applied to the application before? Is there any conflict between the two patterns?
  • Time: How long does it take to develop the project?

In most .NET projects these days, we often come across this pattern because of its undisputed advantages. The pattern is also very flexible and can be applied to many types of .NET projects such as REST API, MVC, MVVM, WebForm, etc.

 

Repository and Unit of Work patterns with practical examples

Now let's start a small sample project using ASP.NET Core 3.1 and 2-layer architecture. Management.API provides RESTfull APIs. Management.Domain acts as data modeling and interfacing. And Management. Infrastructure offers a data access service.

 

a small sample project using ASP.NET Core 3.1 and 2-layer architecture

 

Here, we need a database with three tables: Users, Departments, Salary. We begin with the Entity Framework Core with Code first. But before that, we need an EntityBase class for the entities.

In Management.Domain project, I create a few base classes as follows:



public interface IEntityBase   
{   
    TKey Id { get; set; }   
}   
   
public interface IDeleteEntity   
{   
    bool IsDeleted { get; set; }   
}   
   
public interface IDeleteEntity : IDeleteEntity, IEntityBase   
{   
}   
   
public interface IAuditEntity   
{   
    DateTime CreatedDate { get; set; }   
    string CreatedBy { get; set; }   
    DateTime? UpdatedDate { get; set; }   
    string UpdatedBy { get; set; }   
}   
public interface IAuditEntity : IAuditEntity, IDeleteEntity   
{   
}


Code language: C# (cs)

 

And implement classes for it.

 


    public abstract class EntityBase : IEntityBase   
{   
    [Key]   
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]   
    public virtual TKey Id { get; set; }   
}   
   
public abstract class DeleteEntity : EntityBase, IDeleteEntity   
{   
    public bool IsDeleted { get; set; }   
}   
   
public abstract class AuditEntity  : DeleteEntity , IAuditEntity< TKey>   
{   
    public DateTime CreatedDate { get; set; }   
    public 
Code language: C# (cs)

 

The primary purpose is to reuse common properties and methods or initialize default values when committing to the database.

OK. Now it's time to create the User, Salary, and Department data model classes.

User.cs


    [Table("Users")]   
public partial class User : DeleteEntity   
{   
    public User()   
    {   
        Salaries = new HashSet();   
    }   
   
    public string UserName { get; set; }   
   
    [EmailAddress]   
    public string Email { get; set; }   
   
    public short DepartmentId { get; set; }   
   
    [ForeignKey(nameof(DepartmentId))]   
    public virtual Department Department { get; set; }   
   
    public virtual ICollection Salaries { get; set; }   
}   

Code language: C# (cs)

 

Department.cs


[Table("Departments")]   
public partial class Department : AuditEntity  
{   
    public Department()   
    {   
        Users = new HashSet();   
    }   
   
    public string DepartmentName { get; set; }   
   
    public virtual ICollection< User> Users { get; set; }   
}

Code language: C# (cs)

 

Salary.cs


[Table("Salaries")]   
public partial class Salary : AuditEntity   
{   
    public Salary()   
    {   
   
    }   
    public int UserId { get; set; }   
    public float CoefficientsSalary { get; set; }   
    public float WorkDays { get; set; }   
    public decimal TotalSalary { get; set; }   
   
    [ForeignKey(nameof(UserId))]   
    public virtual User User { get; set; }   
}
Code language: C# (cs)

 

Now let's create a Generic Repository and Unit of work classes.

 

Interfaces/IRepository.cs


public interface IRepository where T : class   
{   
    void Add(T entity);   
    void Delete(T entity);   
    void Update(T entity);   
    IQueryableList(Expression<Func<T, bool>> expression);   
}
  
Code language: C# (cs)

 

Interfaces/IUnitOfWork .cs


public interface IUnitOfWork    
{   
    Task CommitAsync();   
}  

Code language: C# (cs)

 

Inside the Management.Infrastructure project, create implementation classes for the interfaces that we just created above.

 


public class DbFactory : IDisposable   
{   
    private bool _disposed;   
    private Func _instanceFunc;   
    private DbContext _dbContext;   
    public DbContext DbContext => _dbContext ?? (_dbContext = _instanceFunc.Invoke());   
   
    public DbFactory(Func dbContextFactory)   
    {   
        _instanceFunc = dbContextFactory;   
    }   
   
    public void Dispose()   
{   
    if (!_disposed && _dbContext != null)   
        {   
            _disposed = true;   
            _dbContext.Dispose();   
        }   
    }   
}

Code language: C# (cs)

 

Repository.cs


public class Repository : IRepository where T : class   
{   
    private readonly DbFactory _dbFactory;   
    private DbSet _dbSet;   
   
    protected DbSet DbSet   
    {   
        get => _dbSet ?? (_dbSet = _dbFactory.DbContext.Set());   
    }   
   
    public Repository(DbFactory dbFactory)   
    {   
        _dbFactory = dbFactory;   
    }   
   
    public void Add(T entity)   
    {   
        if (typeof(IAuditEntity).IsAssignableFrom(typeof(T)))   
        {   
            ((IAuditEntity)entity).CreatedDate = DateTime.UtcNow;   
        }   
        DbSet.Add(entity);   
    }   
   
    public void Delete(T entity)   
    {   
        if (typeof(IDeleteEntity).IsAssignableFrom(typeof(T)))   
        {   
            ((IDeleteEntity)entity).IsDeleted = true;   
            DbSet.Update(entity);   
        }   
       else
            DbSet.Remove(entity);   
    }   
   
    public IQueryable List(Expression<Func<T, bool>> expression)   
    {   
        return DbSet.Where(expression);   
    }   
   
    public void Update(T entity)   
    {   
        if (typeof(IAuditEntity).IsAssignableFrom(typeof(T)))   
        {   
            ((IAuditEntity)entity).UpdatedDate = DateTime.UtcNow;   
        }   
        DbSet.Update(entity);   
    }   
}  

Code language: C# (cs)

 

UnitOfWork .cs


public class UnitOfWork  : IUnitOfWork    
{   
    private DbFactory _dbFactory;   
   
    public UnitOfWork (DbFactory dbFactory)   
    {   
        _dbFactory = dbFactory;   
    }   
   
    public Task CommitAsync()   
    {   
        return _dbFactory.DbContext.SaveChangesAsync();   
    }   
}
Code language: C# (cs)

 

Let me explain this part further. At the application layer (Management.Domain), we just need to declare interfaces for them. The Domain layer will only need to know the parameter passed, the methods, and properties defined without considering them to be processed in detail. It helps the system to hide details from external systems (Encapsulation).

  • DbFactory: The system will initialize a DbContext when we actually use it. After a lifetime (default is Scoped), we need to dispose of the DbContext.
  • Repository: Generic Repository defines common operations that most entities need to have (CRUD).
  • UnitOfWork: It will contain the commit changes, execute queries/commands (not covered in this article) methods.

Because Domain is the central layer of the system, we only interact with the database through the Repositories' interface.

 

Management.Domain

 

Now we create three repositories for three tables, namely DepartmentRepository, UserRepository, and SalaryRepository.

In Management.Domain project:

Departments/IDepartmentRepository.cs


public interface IDepartmentRepository : IRepository   
{   
    Department AddDepartment(string departmentName);   
}
Code language: C# (cs)

 

Users/IUserRepository.cs


public interface IUserRepository : IRepository   
{   
    User NewUser(string userName   
        , string email   
        , Department department);   
}
Code language: C# (cs)

 

Salaries/ISalaryRepository.cs


public interface ISalaryRepository : IRepository   
{   
    Salary AddUserSalary(User user, float coefficientsSalary, float workdays);   
}
Code language: C# (cs)

 

In Management.Infrastructure project:

Repositories/IDepartmentRepository.cs


public class DepartmentRepository : Repository, IDepartmentRepository   
{   
    public DepartmentRepository(DbFactory dbFactory) : base(dbFactory)   
    {   
    }   
   
    public Department AddDepartment(string departmentName)   
    {   
        var department = new Department(departmentName);   
        if (department.ValidOnAdd())   
        {   
            this.Add(department);   
            return department;   
        }   
        else   
            throw new Exception("Department invalid");   
    }   
}
Code language: C# (cs)

 

Repositories/IUserRepository.cs


public class UserRepository : Repository, IUserRepository   
{   
    public UserRepository(DbFactory dbFactory) : base(dbFactory)   
    { 

    }   
   
    public User NewUser(string userName, string email, Department department)   
    {   
        var user = new User(userName, email, department);   
        if (user.ValidOnAdd())   
        {   
            this.Add(user);   
            return user;   
        }   
        else   
            throw new Exception("User invalid");   
    }   
}
Code language: C# (cs)

 

Repositories/ISalaryRepository.cs


public class SalaryRepository : Repository, ISalaryRepository   
{   
    public SalaryRepository(DbFactory dbFactory) : base(dbFactory)   
    {   
    }   
   
    public Salary AddUserSalary(User user, float coefficientsSalary, float workdays)   
    {   
        var salary = new Salary(user, coefficientsSalary, workdays);   
        if (salary.ValidOnAdd())   
        {   
            this.Add(salary);   
            return salary;   
        }   
        else   
            throw new Exception("Salary invalid");   
    }   
}
Code language: C# (cs)

 

So far, we've gone through most of the concepts that I want to share today. Now we need to complete it with the setup of Dependency Injection for Management.API.

In Management.API, create extension methods to configure database and Repository.

 


public static class IServiceCollectionExtensions   
{   
    public static IServiceCollection AddDatabase(this IServiceCollection services, IConfiguration configuration)   
    {   
        // Configure DbContext with Scoped lifetime   
        services.AddDbContext(options =>   
            {   
                options.UseSqlServer(configuration.GetConnectionString("ManagementConnection"));   
                options.UseLazyLoadingProxies();   
            }   
        );   
   
        services.AddScoped<Func>((provider) => () => provider.GetService());   
        services.AddScoped();   
        services.AddScoped();   
   
        return services;   
    }   
   
    public static IServiceCollection AddRepositories(this IServiceCollection services)   
    {   
        return services   
            .AddScoped(typeof(IRepository<>), typeof(Repository<>))   
            .AddScoped<IDepartmentRepository, DepartmentRepository>()   
            .AddScoped<IUserRepository, UserRepository>()   
            .AddScoped<ISalaryRepository, SalaryRepository>();   
    }   
   
    public static IServiceCollection AddServices(this IServiceCollection services)   
    {   
        return services   
            .AddScoped();   
    }   
}


Code language: C# (cs)

 

At the Startup class, the ConfigureServices function.

 


public void ConfigureServices(IServiceCollection services)   
{   
    services.AddControllers();   
   
    services   
        .AddDatabase(Configuration)   
        .AddRepositories()   
        .AddServices();   
}

Code language: C# (cs)

 

Now let's create a service to check our achievements.

 


public class DepartmentService   
{   
    private readonly IUnitOfWork  _unitOfWork;   
    private readonly IDepartmentRepository _departmentRepository;   
    private readonly IUserRepository _userRepository;   
    private readonly ISalaryRepository _salaryRepository;   
   
    public DepartmentService(IUnitOfWork  unitOfWork   
        , IDepartmentRepository departmentRepository   
        , IUserRepository userRepository   
        , ISalaryRepository salaryRepository)   
    {   
        _unitOfWork = unitOfWork;   
        _departmentRepository = departmentRepository;   
        _userRepository = userRepository;   
        _salaryRepository = salaryRepository;   
    }   
   
    public async Task AddAllEntitiesAsync()   
    {   
        // create new Department   
        var departMentName = $"department_{Guid.NewGuid():N}";   
        var department = _departmentRepository.AddDepartment(departMentName);   
   
        // create new User with above Department   
        var userName = $"user_{Guid.NewGuid():N}";   
        var userEmail = $"{Guid.NewGuid():N}@gmail.com";   
        var user = _userRepository.NewUser(userName, userEmail, department);   
   
        // create new Salary with above User   
        float coefficientsSalary = new Random().Next(1, 15);   
        float workdays = 22;   
        var salary = _salaryRepository.AddUserSalary(user, coefficientsSalary, workdays);   
   
        // Commit all changes with one single commit   
        var saved = await _unitOfWork.CommitAsync();   
   
        return saved > 0;   
    }   
}
Code language: C# (cs)

 

The result looks something like this.

 

Repository & Unit of Work Design Patterns in .NET Core

 

For better understanding, you can discover my demo source code here.

Finally, I hope this article will help you save time and energy when designing .NET projects using this Repository and Unit of Work pattern.

And don’t forget to comment below to let me know your opinion. Thanks for your time and have a nice day.

 

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

 

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.

Up Next

How to apply SOLID principles with practical examples in C#
September 07,2021 by Chuong Tran
In Object-Oriented Programming (OOP), SOLID is one of the most popular sets of design principles...
Domain-Driven Design in ASP.NET Core applications
May 31,2021 by Loc Nguyen
The technology industry has been thriving for the second half of the last century. It's...
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...
How to build and deploy a 3-tier architecture application with C#
March 11,2021 by Uyen Luu
Hi, back to the architecture patterns, in the last article I explained what the three-layer...
Roll to Top

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

Subscribe