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