Think with Enlab

Diving deep into the ocean of technology

Stay Connected. No spam!
  • Home
  • Development
  • How to implement Repository & Unit of Work design patterns in .NET Core with practical examples

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

 

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 access to 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. In this article, I 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>   
{   
    TKey Id { get; set; }   
}   
   
public interface IDeleteEntity   
{   
    bool IsDeleted { get; set; }   
}   
   
public interface IDeleteEntity<TKey> : IDeleteEntity, IEntityBase<TKey>   
{   
}   
   
public interface IAuditEntity   
{   
    DateTime CreatedDate { get; set; }   
    string CreatedBy { get; set; }   
    DateTime? UpdatedDate { get; set; }   
    string UpdatedBy { get; set; }   
}   
public interface IAuditEntity<TKey> : IAuditEntity, IDeleteEntity<TKey>   
{   
} 

And implement classes for it.

public abstract class EntityBase<TKey> : IEntityBase<TKey>   
{   
    [Key]   
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]   
    public virtual TKey Id { get; set; }   
}   
   
public abstract class DeleteEntity<TKey> : EntityBase<TKey>, IDeleteEntity<TKey>   
{   
    public bool IsDeleted { get; set; }   
}   
   
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; }   
}   

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<int>   
{   
    public User()   
    {   
        Salaries = new HashSet<Salary>();   
    }   
   
    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<Salary> Salaries { get; set; }   
}   

 Department.cs

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

Salary.cs

[Table("Salaries")]   
public partial class Salary : AuditEntity<long>   
{   
    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; }   
}

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

Interfaces/IRepository.cs

public interface IRepository<T> where T : class   
{   
    void Add(T entity);   
    void Delete(T entity);   
    void Update(T entity);   
    IQueryable<T> List(Expression<Func<T, bool>> expression);   
} 
  

Interfaces/IUnitOfWork .cs

public interface IUnitOfWork    
{   
    Task<int> CommitAsync();   
}   

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<AppDbContext> _instanceFunc;   
    private DbContext _dbContext;   
    public DbContext DbContext => _dbContext ?? (_dbContext = _instanceFunc.Invoke());   
   
    public DbFactory(Func<AppDbContext> dbContextFactory)   
    {   
        _instanceFunc = dbContextFactory;   
    }   
   
    public void Dispose()   
 {   
    if (!_disposed && _dbContext != null)   
        {   
            _disposed = true;   
            _dbContext.Dispose();   
        }   
    }   
} 

 Repository.cs

public class Repository<T> : IRepository<T> where T : class   
{   
    private readonly DbFactory _dbFactory;   
    private DbSet<T> _dbSet;   
   
    protected DbSet<T> DbSet   
    {   
        get => _dbSet ?? (_dbSet = _dbFactory.DbContext.Set<T>());   
    }   
   
    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<T> 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);   
    }   
}   

UnitOfWork .cs

public class UnitOfWork  : IUnitOfWork    
{   
    private DbFactory _dbFactory;   
   
    public UnitOfWork (DbFactory dbFactory)   
    {   
        _dbFactory = dbFactory;   
    }   
   
    public Task<int> CommitAsync()   
    {   
        return _dbFactory.DbContext.SaveChangesAsync();   
    }   
} 

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>   
{   
    Department AddDepartment(string departmentName);   
} 

Users/IUserRepository.cs

public interface IUserRepository : IRepository<User>   
{   
    User NewUser(string userName   
        , string email   
        , Department department);   
} 

Salaries/ISalaryRepository.cs

public interface ISalaryRepository : IRepository<Salary>   
{   
    Salary AddUserSalary(User user, float coefficientsSalary, float workdays);   
} 

In Management.Infrastructure project:

Repositories/IDepartmentRepository.cs

public class DepartmentRepository : Repository<Department>, 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");   
    }   
} 

Repositories/IUserRepository.cs

public class UserRepository : Repository<User>, 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");   
    }   
} 

Repositories/ISalaryRepository.cs

public class SalaryRepository : Repository<Salary>, 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");   
    }   
} 

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<AppDbContext>(options =>   
            {   
                options.UseSqlServer(configuration.GetConnectionString("ManagementConnection"));   
                options.UseLazyLoadingProxies();   
            }   
        );   
   
        services.AddScoped<Func<AppDbContext>>((provider) => () => provider.GetService<AppDbContext>());   
        services.AddScoped<DbFactory>();   
        services.AddScoped<IUnitOfWork , UnitOfWork >();   
   
        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<DepartmentService>();   
    }   
} 

At the Startup class, the ConfigureServices function

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

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<bool> 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;   
    }   
}

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. 

 

Contact us!

 

About the author

Loc Nguyen Dinh

 

Loc Nguyen Dinh

Loc’s a senior backend developer with a laser focus on Microsoft technologies (ASP .NET Core, ASP .NET, Xamarin Forms, Windows Form, WPF, Entity Framework/Entity Framework Core, LinQ to Entity and WebForm), DevOps (GitLab CI/CD, AWS DevOps, Docker), Design Patterns (Domain Driven Design, Clean Architect, Microservices, N-Tiers). 

With his +6-year experience, Loc’s happy to share and discuss topics like OOP, Data structures and Algorithms, Software Design and Development. 

 

Leave a Reply

avatar

Up Next

Top logging frameworks for .NET applications and our best configuration tips
January 19,2021 by Tan Nguyen
In software development, logging is an essential part that helps you to monitor the system....
Why should startups consider Flutter for cross-platform app developments
November 12,2020 by Thang Le
What makes your startup successful? In a sense, you may count upon the high-quality product...
How to Secure Sensitive Data in The Configuration
October 13,2020 by Tan Nguyen
Introduction On the blog post “How to Configure .Net Core Environments With Practical Examples”, we shared...
How IIS Processes ASP.NET Core HTTP Request
September 30,2020 by Vinh Tran
Have you ever wondered what happens under the hood when you make an API call to...

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

Subscribe