DEV Community

loading...
Cover image for Step by Step - Repository Pattern and Unit of Work with Asp.Net Core 5

Step by Step - Repository Pattern and Unit of Work with Asp.Net Core 5

Mohamad Lawand
Code is Life. Crossfitter. Solution Architect. Just another human walking the earth! https://youtube.com/c/mohamadlawand
・7 min read

Intro

In this article we will be exploring Unit of work and Repository pattern with EF Core and .Net 5.

You can watch the full video on Youtube:

Please find the source code on GitHub:
https://github.com/mohamadlawand087/v33-repo-uow

So what we will cover today:

  • What is Repository Pattern
  • Why do we want to use Repository Pattern
  • What is Unit of Work (UoW)
  • Benefits of UoW
  • Ingredients & Accounts
  • Code and Implementations

As always you will find the source code in the description down below. Please like, share and subscribe if you like the video. It will really help the channel

What is a Repository Pattern

The repository pattern is talked about a lot, especially in the API-and-microservice-heavy world that .net core shines in.

The repository pattern is a strategy for abstracting data access layer. So what is a data layer? it is made up of the code in the application that is responsible of storing and retrieving the data.

Adding, removing, updating, and selecting items from this collection is done through a series of straightforward methods, without the need to deal with database concerns like connections, commands, cursors, or readers. Using this pattern can help achieve loose coupling and can keep domain objects persistence ignorant.

Why use Repository Pattern

There are many reasons why we want to use code absatractions

  • Reduce code duplication: it will allow us to use the DRY design principle, where we write the code once and we can utilise it anywhere we want in our code
  • loose coupling to underlying persistance technology: in case we need to switch our database from MSSQL to PostgreSQL. Only on the data layer implementation changes will need to be made, not where we are consuming the data access layer. This will facilitate the changes, reduce the chance of errors.
  • Testability is much more easier, Repository pattern will allow us to mock our database so we can perform our tests
  • Separation of Concerns: seperate application functionalities based on function, which facilitates evolving and maintaining the code.

What is Unit of Work (UoW)

If the Repository pattern is our abstraction over the idea of persistent storage, the Unit of Work (UoW) pattern is our abstraction over the idea of atomic operations. It will allow us to finally and fully decouple our service layer from the data layer.

The unit of work pattern now manages the database states. Once all updates of the entities in a scope are completed, the tracked changes are played onto the database in a transaction so that the database reflects the desired changes.

Thus, the unit of work pattern tracks a business transaction and translates it into a database transaction, wherein steps are collectively run as a single unit. To ensure that data integrity is not compromised, the transaction commits or is rolled back discretely, thus preventing indeterminate state.

Benefits of Unit of Work (UoW)

  1. Abstract Data Access Layer and Business Access Layer from the Application.
  2. Manage in-memory database operations and later saves in-memory updates as one transaction into database.
  3. Facilitates to make the layers loosely-coupled using dependency injection.
  4. Facilitates to follow unit testing or test-driven development (TDD).

Ingredients

Code time

We will start by checking our dotnet SDK

dotnet --version
Enter fullscreen mode Exit fullscreen mode

Now we need to install the entity framework tool

dotnet tool install --global dotnet-ef
Enter fullscreen mode Exit fullscreen mode

Now we need to create our application

dotnet new webapi -n "PocketBook"
Enter fullscreen mode Exit fullscreen mode

Once the application is created we navigate to our source code in Vs Code, the first thing we do is check that the application build successfully.

We open the terminal if you don't see it open go to View ⇒ Terminal

dotnet build
dotnet run
Enter fullscreen mode Exit fullscreen mode

Now we need to add the required packages to utilise SQLLite and Entity Framework Core

dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
Enter fullscreen mode Exit fullscreen mode

Once we add the packages we need to update the appsettings.json to include the connection string to our database

"ConnectionStrings": {
    "DefaultConnection": "DataSource=app.db;Cache=Shared"
  }
Enter fullscreen mode Exit fullscreen mode

We will start by cleaning up our application from some of the boiler plate code that has been created. We need to delete the following files

  • WeatherForecast.cs
  • Controllers/WeatherForecastController.cs

After the clean up we will start by creating our ApplicationDbContext. We need to create a Data folder in he root directory, and then will create the ApplicationDbContext class

using Microsoft.EntityFrameworkCore;

namespace PocketBook.Data
{
    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Once we add our ApplicationDbContext we need to update the startup class to utilise the DbContext

services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlite(
                    Configuration.GetConnectionString("DefaultConnection")));
            services.AddDatabaseDeveloperPageExceptionFilter();
Enter fullscreen mode Exit fullscreen mode

We will continue by creating our Models, inside our root folder directory. Inside the Models folder we will create a new class called User

public class User
{
    public Guid Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Now we need to add our Model to the application DbContext by adding the code below

public class ApplicationDbContext : DbContext
{
    // The DbSet property will tell EF Core tha we have a table that needs to be created
    public virtual DbSet<User> Users { get; set; }

    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

    // On model creating function will provide us with the ability to manage the tables properties
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
    }
}
Enter fullscreen mode Exit fullscreen mode

Once we update the ApplicationDbContext we need to create a new migration script to prepare the EF Core to update our database

dotnet ef migrations add "Initial migration and Adding the User table"
dotnet ef database update 
Enter fullscreen mode Exit fullscreen mode

After the database update is completed, we can check our sqlite db with the SQLite browser, we can see that the table has been created for us.

Now we need to start by creating our repositories. Inside the root directory of our application let us create a new folder called Core, inside the core folder will create another folder called IRepositories. Inside the IRepositories folder will create a new interface called IGenericRepository and we populate the interface as following

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);
}
Enter fullscreen mode Exit fullscreen mode

Now we need a new interface called IUserRepository

public interface IUserRepository : IGenericRepository<User>
{

}
Enter fullscreen mode Exit fullscreen mode

Now inside the Core folder we need to create a new folder called IConfiguration where the UoW configs will be. In side the IConfiguration we need to create an interface called IUnitOfWork

public interface IUnitOfWork
{
    IUserRepository Users { get; }

    Task CompleteAsync();
}
Enter fullscreen mode Exit fullscreen mode

Then we need to create a Repository folder inside the Core folder, inside the Repository folder we need to create GenericRepository class and utilise it as follow

public class GenericRepository<T> : IGenericRepository<T> where T : class
{
    protected ApplicationDbContext context;
    internal DbSet<T> dbSet;
    public readonly ILogger _logger;

    public GenericRepository(
        ApplicationDbContext context,
        ILogger logger)
    {
        this.context = context;
        this.dbSet = context.Set<T>();
        _logger = logger;
    }

    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 Task<bool> Delete(Guid id)
    {
        throw new NotImplementedException();
    }

    public virtual Task<IEnumerable<T>> All()
    {
        throw new NotImplementedException();
    }

    public async Task<IEnumerable<T>> Find(Expression<Func<T, bool>> predicate)
    {
        return await dbSet.Where(predicate).ToListAsync();
    }

    public virtual Task<bool> Upsert(T entity)
    {
        throw new NotImplementedException();
    }
}
Enter fullscreen mode Exit fullscreen mode

Then we need to create our UserRepository in the Repository folder as well

public class UserRepository : GenericRepository<User>, IUserRepository
{
    public UserRepository(ApplicationDbContext context, ILogger logger) : base(context, logger) { }

    public override async Task<IEnumerable<User>> All()
    {
        try
        {
            return await dbSet.ToListAsync();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "{Repo} All function error", typeof(UserRepository));
            return new List<User>();
        }
    }

    public override async Task<bool> Upsert(User 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;

            return true;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "{Repo} Upsert function error", typeof(UserRepository));
            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(UserRepository));
            return false;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Once the Repository has been created now we need to create our UnitofWork class inside the Data folder.

public class UnitOfWork : IUnitOfWork, IDisposable
{
    private readonly ApplicationDbContext _context;
    private readonly ILogger _logger;

    public IUserRepository Users { get; private set; }

    public UnitOfWork(ApplicationDbContext context, ILoggerFactory loggerFactory)
    {
        _context = context;
        _logger = loggerFactory.CreateLogger("logs");

        Users = new UserRepository(context, _logger);
    }

    public async Task CompleteAsync()
    {
        await _context.SaveChangesAsync();
    }

    public void Dispose()
    {
        _context.Dispose();
    }
}
Enter fullscreen mode Exit fullscreen mode

Now that the Unit of work is created we need to update the startup class, so it will be injected in our dependency injection framework. To do this we need to go to the startup class in the root folder and add the following in the ConfigureServices method.

services.AddScoped<IUnitOfWork, UnitOfWork>();
Enter fullscreen mode Exit fullscreen mode

Now let us create our controller inside our controller folder, create a new class called UsersController.cs

[ApiController]
  [Route("[controller]")]
  public class UsersController : ControllerBase
  {
      private readonly ILogger<UsersController> _logger;
      private readonly IUnitOfWork _unitOfWork;

      public UsersController(
          ILogger<UsersController> logger,
          IUnitOfWork unitOfWork)
      {
          _logger = logger;
          _unitOfWork = unitOfWork;
      }

      [HttpGet]
      public async Task<IActionResult> Get()
      {
          var users = await _unitOfWork.Users.All();
          return Ok(users);
      }

      [HttpGet("{id}")]
      public async Task<IActionResult> GetItem(Guid id)
      {
          var item = await _unitOfWork.Users.GetById(id);

          if(item == null)
              return NotFound();

          return Ok(item);
      }

      [HttpPost]
      public async Task<IActionResult> CreateUser(User user)
      {
          if(ModelState.IsValid)
          {
                  user.Id = Guid.NewGuid();

              await _unitOfWork.Users.Add(user);
              await _unitOfWork.CompleteAsync();

              return CreatedAtAction("GetItem", new {user.Id}, user);
          }

          return new JsonResult("Somethign Went wrong") {StatusCode = 500};
      }

      [HttpPut("{id}")]
      public async Task<IActionResult> UpdateItem(Guid id, User user)
      {
          if(id != user.Id)
              return BadRequest();

          await _unitOfWork.Users.Upsert(user);
          await _unitOfWork.CompleteAsync();

          // Following up the REST standart on update we need to return NoContent
          return NoContent();
      }

      [HttpDelete("{id}")]
        public async Task<IActionResult> DeleteItem(Guid id)
        {
            var item = await _unitOfWork.Users.GetById(id);

            if(item == null)
                return BadRequest();

            await _unitOfWork.Users.Delete(id);
            await _unitOfWork.CompleteAsync();

            return Ok(item);
        }
  }
Enter fullscreen mode Exit fullscreen mode

Discussion (0)