Featured

Implementing the Repository and Unit of Work Patterns in .NET 6

Last modified: March 12, 2022

In this article, we will go through Repository Pattern in ASP.NET 6 (aka Core), Generic Repository Patterns, Unit of Work by building a simple project. The source code of this implementation can be found in my Github

Why we need Implementing the Repository and Unit of Work Patterns?

The main reasons of implementing such patterns are below

  • Reduce Code
    It will prevent from DRY (Don't Repeat Yourself). For example, having to write lines to get records from the database. What will happen if you complex database, where you need to so many queries statements? It will introduce lot of repeated code, which will be solved by Repository Pattern.

  • Loosly Coupling
    Code are more easily able to swap portion of code/logic (e.g. database class) to something else. There are lots of ORMS available for .NET 6 (aka core), and many come every year. Having De-couple the application from the Data Access, allows us to change from one ORM (e.g. Entity Framework Core) to another (e.g. Dapper)

  • Testability
    Can facilitate automated unit testing or test-driven development (TDD).Database access logic and domain logic can be tested separately with this pattern.

  • Seperation of Concerns
    Each project has its own task (e.g. UI only handle presenting web UI). Separation of Concerns make this whole process much cleaner and friendlier for the engineer

img

1. Create Project

Create an ASP.NET MVC application.

createproject createproject createproject createproject

We are going to create three more projects

  • UOW.Core
  • UOW.Infrastructure
  • UOW.Service

createproject createproject

We need following folder structures in these class projects createproject

2. UOW.Core

We create Entity folder where we create a project Entity class (which will mapped to database);

public class Project { public Guid Id { get; set; } public string Name { get; set; } public string Description { get; set; } }

We create Interfaces folder and create these Interfaces

These are not implementation, it will be outside of this Core Project. Thye are just signatures of the methods define in the Domain/Core Project. The implemetation will be in UOW.Infrastructure project

  • IGenericRepository

It is based Repository Interfaces for all

public interface IGenericRepository<T> where T : class { Task<T?> GetById(Guid id); Task<IEnumerable<T>> GetAll(); IEnumerable<T> Find(Expression<Func<T, bool>> expression); Task<bool> Add(T entity); Task<bool> Remove(Guid id); Task<bool> Upsert(T entity); }
  • IProjectRepository

Now each specific Repository (e.g. Project Repository) inherit from IGenericRepository and pass Project Class as T

public interface IProjectRepository: IGenericRepository<Project> { }
  • IUnitOfWork

Unit of Work Pattern expose various respostiories in our application. It is similar to DbContext but Unit of Work is loosly couple not like dbContext to Entity Framework Core

We add IProjectRepository repository to Unit of Work Interface to access this interface. We also have CompletedAsync method that will save changes to the Database.

Let us say we two have respositories, where we have to perform few actions(e.g. add, update). But the requirment is we need to ensure it happen all or none. With the help of Unit of Work, it is possible by calling CompletedAsync method after each actions.

public interface IUnitOfWork: IDisposable { IProjectRepository Projects { get; } Task<int> CompletedAsync(); }
3. UOW.Infrastructure

nuget We install Microsoft.EntityFrameworkCore; Nuget Package

  • PMSDbContext

We create DbContext to connect to Database (in this case in memory) and Project table to it.

public class PMSDbContext : DbContext { public PMSDbContext(DbContextOptions<PMSDbContext> options) : base(options) { } public virtual DbSet<Project> Projects { get; set; } }
  • GenericRepository

This is implementation of IGenericRepository interface using Entity Framwork (EF) Core

public class GenericRepository<T> : IGenericRepository<T> where T : class { protected PMSDbContext _context; protected DbSet<T> dbSet; protected readonly ILogger _logger; public GenericRepository( PMSDbContext context, ILogger logger) { _context = context; _logger = logger; dbSet = _context.Set<T>(); } public async Task<bool> Add(T entity) { await dbSet.AddAsync(entity); return true; } public IEnumerable<T> Find(Expression<Func<T, bool>> expression) { return dbSet.Where(expression); } public async Task<IEnumerable<T>> GetAll() { return await dbSet.ToListAsync(); } public async Task<T?> GetById(Guid id) { return await dbSet.FindAsync(id); } public async Task<bool> Remove(Guid id) { var t = await dbSet.FindAsync(id); if (t != null) { dbSet.Remove(t); return true; } else return false; } public Task<bool> Upsert(T entity) { throw new NotImplementedException(); } }
  • ProjectRepository

This is implementation of IProjectRepository interface

public class ProjectRepository: GenericRepository<Project>, IProjectRepository { public ProjectRepository(PMSDbContext context, ILogger logger) : base(context, logger) { } }

Lastly we are goint to implement UnitOfWork interface, inwhich we reference ProjectRepository, have a function called CompletedAsync to save changes to database and finally dispose when no longer needed.

  • UnitOfWork
public class UnitOfWork: IUnitOfWork { private readonly PMSDbContext _context; private readonly ILogger _logger; public IProjectRepository Projects { get; private set; } public UnitOfWork( PMSDbContext context, ILoggerFactory logger ) { _context = context; _logger = logger.CreateLogger("logs"); Projects = new ProjectRepository(_context, _logger); } public async Task<int> CompletedAsync() { return await _context.SaveChangesAsync(); } public void Dispose() { _context.Dispose(); } }
4. UOW.Service

In some article, this layer is omitted and Unit of Work is directly connected to Controller but I believe we should have this layer so we can do not expose all the properties of a classes. For example, a Person entity has date of birth property which is private information should not be expose to all users. We make use of DTO and with the help of AutoMapper (object to object) we can easily done this.

A DTO is POCO class that is used to transfer the data from one layer to another layer.

  • ProjectDTO
public class ProjectDTO { public string Name { get; set; } public string Description { get; set; } }

nuget

We need to install AutoMapper from Nuget Package

  • ProjectService
public class ProjectService { private readonly IUnitOfWork _unitOfWork; private readonly IMapper _mapper; public ProjectService( IUnitOfWork unitOfWork, IMapper mapper ) { _unitOfWork = unitOfWork; _mapper = mapper; } public async Task<IEnumerable<ProjectDTO>> GetProjectAsync() { var projects = await _unitOfWork.Projects.GetAll(); return _mapper.Map<IEnumerable<ProjectDTO>>(projects); } public async Task<bool> InsertAsync(ProjectDTO projectDTO) { var project = _mapper.Map<Project>(projectDTO); return await _unitOfWork.Projects.Add(project); } public async Task<int> CompletedAsync() { return await _unitOfWork.CompletedAsync(); } }
  • AutoMapperProfile

For more information of AutoMapper click here

public class AutoMapperProfile : Profile { public AutoMapperProfile() { CreateMap<ProjectDTO, Project>().ReverseMap(); } }
5. UOW.Web

Create a Controller called ProjectController

controller

public class ProjectController : Controller { private readonly ProjectService _service; public ProjectController(ProjectService service) { _service = service; } // GET: ProjectController public async Task<ActionResult> Index() { var projects = await _service.GetProjectAsync(); return View(projects); } // GET: ProjectController/Create public ActionResult Create() { return View(); } // POST: ProjectController/Create [HttpPost] [ValidateAntiForgeryToken] public async Task<ActionResult> CreateAsync(ProjectDTO project) { try { await _service.InsertAsync(project); await _service.CompletedAsync(); return RedirectToAction(nameof(Index)); } catch { return View(); } }
  • Index

Replace the Index Page with the following code. It is just simple table

@model IEnumerable<PMS.Service.DTO.ProjectDTO> @{ ViewData["Title"] = "Index"; } <h1>Index</h1> <p> <a asp-action="Create">Create New</a> </p> <table class="table"> <thead> <tr> <th> @Html.DisplayNameFor(model => model.Name) </th> <th> @Html.DisplayNameFor(model => model.Description) </th> </tr> </thead> <tbody> @foreach (var item in Model) { <tr> <td> @Html.DisplayFor(modelItem => item.Name) </td> <td> @Html.DisplayFor(modelItem => item.Description) </td> </tr> } </tbody> </table>
  • Create

Again, simple form to input the project information

@model PMS.Service.DTO.ProjectDTO @{ ViewData["Title"] = "Create"; } <h1>Create</h1> <h4>ProjectDTO</h4> <hr /> <div class="row"> <div class="col-md-4"> @using (Html.BeginForm("Create", "Project", FormMethod.Post)) { <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="Name" class="control-label">Name: </label> @Html.TextBoxFor(m => m.Name) <span asp-validation-for="Name" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Description" class="control-label">Description: </label> @Html.TextBoxFor(m => m.Description) <span asp-validation-for="Description" class="text-danger"></span> </div> <div class="form-group"> <button type="submit" value="Create" class="btn btn-primary" >Create</button> </div> } </div> </div> <div> <a asp-action="Index">Back to List</a> </div> @section Scripts { @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} }
6. Program.cs

We are going to install Entity Framework Core In Memory for testing purpose. However you need to select something like SQL Server for production.

nuget

builder.Services.AddDbContext<PMSDbContext>(options => options.UseInMemoryDatabase("demo")); builder.Services.AddTransient<IUnitOfWork, UnitOfWork>(); builder.Services.AddTransient(typeof(ProjectService), typeof(ProjectService)); var config = new MapperConfiguration(cfg => { cfg.AddProfile(new AutoMapperProfile()); }); var mapper = config.CreateMapper(); builder.Services.AddSingleton(mapper);