Repository Pattern and UoW with ASP.NETCore WebAPI


Hello All, I plan to tell you how to implement a unit of work with the repository pattern in ASP.NET Core today. In that case, I'll discuss,

What is the Repository Pattern?

  • Non-generic and Generic Repository Pattern
  • What is the Unit of Work (UoW)?
  • Benefits of UoW
  • How to implement the unit of work with repository pattern in ASP.NET Core Web API

1. What is the Repository Pattern?

The repository pattern creates the abstraction layer between database access and business logic. In that case, Instead of writing the entire data access logic on the controller, it's better to write it in a different class called a repository.

2. Non-Generic and Generic Repository Pattern

Non-Generic Repository Pattern

All database actions related to a given entity are defined using the non-generic repository pattern within a separate class. For Example, suppose we have two entities (Ex-Student and Employee ). In that case, we will create two repositories for each entity with the basic CRUD operations and other operations related to each entity.

Generic Repository Pattern

The generic repository pattern is used to provide common database operations for all database entities in a single class, such as CRUD operations and so on. First, I'll show how to implement the generic repository pattern with the Example.

3. What is the Unit of Work (UoW)?

The Unit of Work is a type of business transaction, and it will aggregate all Repository transactions (CRUD) into a single transaction. Only one commit will be made for all modifications. If any transaction fails to assure data integrity, it will be rolled back. Because it does not block any data table until it commits the modifications, the Unit of Work is commonly referred to as a lazy evaluation transaction.

using UoWDemo.Context;
using UoWDemo.Entities;
using System;
using System.Linq;
namespace UoWDemo.services
{
    public class EmployeeRepository : IEmployeeRepository
    {
        private readonly DBContext _dBContext;
        public AuthService(DBContext dbContext)
        {
            _dBContext = dbContext;
            
        }
        public void Add(Employee employee)
        {
            _dbContext.Employee.Add(employee);
        }
        
        public void Delete(Employee employee)
        {
            _dbContext.Employee.Remove(employee);
        }
        
        public void SaveChanges()
        {
            _dbContext.SaveChanges();
        }
    }
}

The above code example seems to be fine. But the issue will arise when we add another repository for another entity like this repository. For Example, if we add the repository for the Customer entity, we have to maintain its instance of DbContext. Then these repositories will maintain their instance of DbContext. This could lead to problems if each DbContext has its in-memory list of updates to entity records. In this situation, if one repository's SaveChanges fails while the other succeeds, database inconsistency will arise. This is where the UnitOfWork concept comes into play.

High level of UoW and Repository Pattern

Also, You can see the high-level view of the Unit of Work and Repository pattern in the above figure.

4. Benefits of Unit Of Work (UoW)

  1. Reduce the Duplicate queries and codes
  2. Could increase the loosely couple with DI in the application
  3. Easy to do unit testing or test-driven development (TDD)
  4. Increase the abstraction level and maintain the business logic separately.

5. How to implement the unit of work with repository pattern in ASP.NET Core Web API

First, we need to create an ASP.NET Core Web API project. I am not explaining how to create it here. Then run it and check the project.

Then we need to install the required packages for use to SQL server and entity framework core. In that case, you could navigate to Nuget package manager and then install the required packages into our solution.

Microsoft.AspNetCore.Identity.EntityFrameworkCore
Microsoft.EntityFrameworkCore.Sqlite
Microsoft.EntityFrameworkCore.Tools
Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore

Then remove the boilerplate code that has been created when creating the project (WeatherForecast.cs, Controller/WeatherForecastController.cs) after we need to modify the appsettings.json file to add the connection string of our database. So, It's a small task, and it's up to you.

In here, I follow the code-first approach in EF core to create and manage our database. In that case, we need to create a Model (Entity) class. So, first, create the folder called Models in the root directory and then create the Employee model class inside the Model folder as below code.

Employee.cs 

using System;
namespace UOW_101.Models
{
    public class Employee
    {
        public Guid Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
        public string PhoneNumber { get; set; }
    }
}

Then we need to create DbContext for our application. In that case, Create a folder called Data in the root directory and then create DBContext class which implement the DbContext as below code and also need to our Employee model class into DbContext as below code.

DBContext.cs

using Microsoft.EntityFrameworkCore;
using UOW_101.Models;
namespace UOW_101.Data
{
    public class DBContext : DbContext
    {
        public DBContext(DbContextOptions<DBContext> options) : base(options)
        {
        }
        public virtual DbSet<Employee> Employees { get; set; }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
        }
    }
}

You can see the DbContext class with the above code. In that code, the OnModelCreating method provides the ability for us to manage the table properties of the tables in the database. As well, we add our Employee model class. In that case, the DbSet property will help to create the table which adds with it by using the EF Core.

Then we have to create our database. In that case, navigate to the package manager console of visual studio and then use the following commands to create the database. First, enter the add-migration to create the migration by adding the migration snapshot and then enter update-database to create and update the database by using the last migration snapshot.

add-migration initialMigration
update-database

Now we created our database with our ASP.NET Core Web API project.

Now we have to implement the repository pattern. In that case, I follow the generic repository pattern to create the repository pattern for our project. First, we need to create a folder called services in the root directory and then create the IGenericRepository interface inside the service folder as the following code.

IGenericRepository.cs

using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Threading.Tasks;
namespace UOW_101.Services
{
    public interface IGenericRepository<T> where T : class
    {
        Task<IEnumerable<T>> All();
        Task<T> GetById(Guid id);
        Task<bool> Add(T entity);
        Task<bool> Delete(Guid id);
        Task<bool> Upsert(T entity);
        Task<IEnumerable<T>> Find(Expression<Func<T, bool>> predicate);
    }
}

I explained the Generic repository above. Here, T is a specific class, and the functions will depend on our requirements and preference. In this case, I added 6 functions that are commonly used on other repositories.

Now implement this IGenericRepository. Create a new class called GenericRepository, which implements IGenericRepository inside the services folder as following code.

GenericRepository.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using UOW_101.Data;
namespace UOW_101.Services
{
    public class GenericRepository<T> : IGenericRepository<T> where T : class
    {
        protected DBContext context;
        internal DbSet<T> dbSet;
        protected readonly ILogger _logger;
        public GenericRepository(DBContext context, ILogger logger)
        {
            this.context = context;
            this.dbSet = context.Set<T>();
            this._logger = logger;
        }
        public virtual Task<IEnumerable<T>> All()
        {
            throw new NotImplementedException();
        }
        public virtual async Task<T> GetById(Guid id)
        {
            return await dbSet.FindAsync(id);
        }
        public virtual  async Task<bool> Add(T entity)
        {
            await dbSet.AddAsync(entity);
            return true;
        }
        public virtual  async Task<bool> Delete(Guid id)
        {
            throw new NotImplementedException();
        }
        public virtual Task<bool> Upsert(T entity)
        {
            throw new NotImplementedException();
        }
        public virtual async Task<IEnumerable<T>> Find(Expression<Func<T, bool>> predicate)
        {
            return await dbSet.Where(predicate).ToListAsync();
        }
    }
}

This class will implement the IGenericRepository Interface. The DBContext also will be injected here. In this approach, all operations connected to the dbContext object are hidden within Repository Classes. However, we have not yet committed/updated/saved the database modifications in any way. This is something that should not be done in a Repository Class. We'll discuss that with the Unit of Work.

Now we have to create an interface called IEmployeeRepository.

IEmployeeRepository.cs

using UOW_101.Models;
namespace UOW_101.Services
{
    public interface IEmployeeRepository: IGenericRepository<Employee>
    {
    }
}

Here, We need to inherit IGenericRepository and add the new functions related to IEmployeeRepository. For Example, we could add the functions called GetEmployeeAttendance() because it's not a common operation that is not suitable for the IGenericRepository.

Now we have to implement this IEmployeeRepository. Create a class called UserRepository inside the services folder as the following code.

EmployeeRepository.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using UOW_101.Data;
using UOW_101.Models;
namespace UOW_101.Services
{
    public class EmployeeRepository : GenericRepository<Employee>, IEmployeeRepository
    {
        public EmployeeRepository(DBContext context, ILogger logger) : base(context, logger)
        {
        }
        public override async Task<IEnumerable<Employee>> All()
        {
            try
            {
                return await dbSet.ToListAsync();
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "{Repo} All function error", typeof(EmployeeRepository));
                return new List<Employee>();
            }
        }
        public override async Task<bool> Upsert(Employee entity)
        {
            try
            {
                var existingUser = await dbSet.Where(x => x.Id == entity.Id)
                                                    .FirstOrDefaultAsync();
                if (existingUser == null)
                    return await Add(entity);
                existingUser.FirstName = entity.FirstName;
                existingUser.LastName = entity.LastName;
                existingUser.Email = entity.Email;
                existingUser.PhoneNumber = entity.PhoneNumber;
                return true;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "{Repo} Upsert function error", typeof(EmployeeRepository));
                return false;
            }
        }
        public override async Task<bool> Delete(Guid id)
        {
            try
            {
                var exist = await dbSet.Where(x => x.Id == id)
                                        .FirstOrDefaultAsync();
                if (exist == null) return false;
                dbSet.Remove(exist);
                return true;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "{Repo} Delete function error", typeof(EmployeeRepository));
                return false;
            }
        }
    }
}

In here, We implement the Employee Repository by inhering the IGenericRepository and IEmployeeRespository. In that case, we could override functions that do not implement the database logic with the Generic Repository and then could add the operations for that functions as above code.

Now we have to implement the Unit of Work in our project. Basically, I explained what unit of work in the above section is. Now we have to learn it with the Example.

In that case, I create the Configuration folder in the root directory of the project folder. Then we need to create the IUnitOfWork interface inside the configuration folder as following code.

IUnitOfWork.cs

using System.Threading.Tasks;
using UOW_101.Services;
namespace UOW_101.Configuration
{
    public interface IUnitOfWork
    {
        IEmployeeRepository Employee { get; }
        Task CompleteAsync();
        void Dispose();
    }
}

Now we have built one repository with the Generic Repository pattern. We could inject the repositories that we implement in our project and then could access the data. But when we have a lot of repositories, we need to inject all of them. It's not good practice. In that case, we could use the unit of work to wrap all the repositories into a single object.

The Unit of Work is in control of exposing the relevant Repositories and Committing Changes to the DataSource in order to ensure a complete transaction with no data loss.

Now we have to implement this interface. In that case, create UnitOfWork class inside the configuration folder as the following code.

UnitOfWork.cs 

using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;
using UOW_101.Data;
using UOW_101.Services;
namespace UOW_101.Configuration
{
    public class UnitOfWork : IUnitOfWork, IDisposable
    {
        private readonly DBContext _context;
        private readonly ILogger _logger;
        public IEmployeeRepository Employee { get; private set; }
        public UnitOfWork(DBContext context, ILoggerFactory loggerFactory)
        {
            _context = context;
            _logger = loggerFactory.CreateLogger("logs");
            Employee = new EmployeeRepository(context, _logger);
        }
        public async Task CompleteAsync()
        {
            await _context.SaveChangesAsync();
        }
        public void Dispose()
        {
            _context.Dispose();
        }
    }
}

We are injecting the DBContext and ILogger and implementing the CompleteAsync and the Dispose functions to use when the commit/update/remove operations with the database were.

Now navigate to startup class and need to register IUnitOfWork interface in our project as follows.

// Adding the Unit of work to the DI container

services.AddScoped<IUnitOfWork, UnitOfWork>();

We have only to register IUnitOfWork. We don't need to register other repositories as we are doing in the basic repository pattern. Because we inject all of the repositories in the IUnitOfWork, then we could access them through it.

Then we have to implement the controller in our project. In that case, We have to create the controller called EmployeeController inside the Controllers folder, as below code.

EmployeeController.cs 

using Microsoft.AspNetCore.Mvc;
using System;
using System.Threading.Tasks;
using UOW_101.Configuration;
using UOW_101.Models;
namespace UOW_101.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class EmployeeController : ControllerBase
    {
        
        private readonly IUnitOfWork _unitOfWork;
        public EmployeeController(IUnitOfWork unitOfWork)
        {
            _unitOfWork = unitOfWork;
        }
        [HttpGet]
        public async Task<IActionResult> Get()
        {
            var users = await _unitOfWork.Employee.All();
            return Ok(users);
        }
        [HttpGet("{id}")]
        public async Task<IActionResult> GetEmployee(Guid id)
        {
            var item = await _unitOfWork.Employee.GetById(id);
            if (item == null)
                return NotFound();
            return Ok(item);
        }
        [HttpPost]
        public async Task<IActionResult> CreateEmployee(Employee employee)
        {
            if (ModelState.IsValid)
            {
                employee.Id = Guid.NewGuid();
                await _unitOfWork.Employee.Add(employee);
                await _unitOfWork.CompleteAsync();
                return CreatedAtAction("GetEmployee", new { employee.Id }, employee);
            }
            return new JsonResult("Somethign Went wrong") { StatusCode = 500 };
        }
        [HttpDelete("{id}")]
        public async Task<IActionResult> DeleteEmployee(Guid id)
        {
            var item = await _unitOfWork.Employee.GetById(id);
            if (item == null)
                return BadRequest();
            await _unitOfWork.Employee.Delete(id);
            await _unitOfWork.CompleteAsync();
            return Ok(item);
        }
    }
}

We inject the IUnitOfWork as the object in this controller. Then we have to use the unit of work to add the services and logic to the related methods in this controller.

Now we have to run this project. And then we could access our ASP.NET Core Web API as below figure.

We discuss some points such as Repository pattern, Non-Generic and Generic repository pattern, Unit Of Work and its benefits, and how to implement a unit of work with the repository pattern in the ASP.NET Core Web API project to get the basic idea about it with this article.

#viastudy

Post a Comment

0 Comments