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 RESTful 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, and 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 the properties defined without considering them to be processed in detail. It helps the system 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 and execute queries/commands (not covered in this article) methods.

Because the Domain is the central layer of the system, we only interact with the database through the Repository's 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>();   
    }   
}   

 

In 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 a 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.
Frequently Asked Questions (FAQs)
What are Repository and Unit of Work Design Patterns in .NET Core?

Repositories are classes implementing data access logic for a data entity, handling CRUD operations and other special logic. A Unit of Work acts as a business transaction, merging all repository transactions into a single operation, committing all changes at once or rolling

back if any transaction fails, to ensure data integrity.

Why Should Developers Use Repository and Unit of Work Patterns in .NET Core?

These patterns are chosen to avoid code duplication by using a generic repository for common CRUD functionalities across different entities. This approach promotes reusability, maintainability, and readability in code related to database interactions.

When is it Ideal to Implement Repository and Unit of Work Patterns in .NET Core Projects?

The decision to use these patterns depends on factors like the size and complexity of the application, future maintenance plans, existing patterns in the project, and development time constraints. They are typically used in various .NET project types due to their flexibility and advantages.

How Can You Implement a Generic Repository and Unit of Work in a .NET Core Application?

Implement a generic repository to manage CRUD operations, and create a Unit of Work class to handle transactions. Use Entity Framework Core for database operations, and define entity base classes for common properties. Then, create specific repositories for each data model, implementing the required business logic.

What are the Benefits of Using Dependency Injection with Repository and Unit of Work Patterns in .NET Core?

Dependency Injection in .NET Core allows for decoupling the application layers, making the system more maintainable and testable. It simplifies the process of injecting repositories and the Unit of Work into services, ensuring that the application components are loosely coupled and more scalable.

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