How to use the unit of work pattern in ASP.NET Core

Take advantage of the unit of work design pattern to build flexible, extensible, and reusable data access layers in your ASP.NET Core applications.

group of hands holding together multi colored gears

In most any business application, you will store and retrieve data from a data store by performing CRUD operations (create, read, update, and delete). In this regard, there are several technologies and tools you can use. For example, you can choose from the available ORM frameworks such as Entity Framework, Dapper, or NHibernate.

However, the challenges go beyond storing and retrieving data. You want to use a tool or an approach that helps you write reusable, maintainable, and flexible code. For that, you can draw on design patterns such as the repository and unit of work patterns.

We examined the repository design pattern in an earlier article here. In this article, we’ll explore the unit of work design pattern with relevant code examples to illustrate the concepts covered.

To use the code examples provided in this article, you should have Visual Studio 2022 installed in your system. If you don’t already have a copy, you can download Visual Studio 2022 here.

Create an ASP.NET Core 7 Web API project in Visual Studio 2022

First off, let’s create an ASP.NET Core 7 project in Visual Studio 2022. Follow these steps:

  1. Launch the Visual Studio 2022 IDE.
  2. Click on “Create new project.”
  3. In the “Create new project” window, select “ASP.NET Core Web API” from the list of templates displayed.
  4. Click Next.
  5. In the “Configure your new project” window, specify the name and location for the new project.
  6. Optionally check the “Place solution and project in the same directory” check box, depending on your preferences.
  7. Click Next.
  8. In the “Additional Information” window, leave the check box that says “Use controllers (uncheck to use minimal APIs)” checked, since we’ll not be using minimal APIs in this example. Leave the “Authentication Type” as “None” (default).
  9. Ensure that the check boxes “Enable Open API Support,” “Configure for HTTPS,” and “Enable Docker” are unchecked as we won’t be using those features here.
  10. Click Create.

We’ll use this ASP.NET Core 7 Web API project to work with the unit of work design pattern in the sections below.

What is the unit of work design pattern?

The unit of work design pattern guarantees data integrity and consistency in applications. It ensures that all changes made to multiple objects in the application are committed to the database or rolled back. It provides an organized and consistent way to manage database changes and transactions.

The main goal of this design pattern is to maintain consistency and atomicity, ensuring that all changes made to the database are successful or fail together if there’s a problem. As a result, data consistency is maintained, and modifications to the database are always accurate. With the unit of work design pattern in place, developers can save time and effort by focusing on business logic instead of database access and management.

Implementing the unit of work design pattern in C#

To implement the unit of work design pattern, you must specify the operations that the unit of work supports in an interface or a contract and then implement these methods in a concrete class. You can take advantage of any data access technology you would like to use, such as Entity Framework, Dapper, or ADO.NET.

A typical unit of work implementation would comprise the components listed below:

  • An interface for the unit of work. The operations that the unit of work will carry out are defined by a contract or an interface that contains the declaration of all the methods for adding, changing, and deleting data and for committing or rolling back changes to the database.
  • A concrete implementation of the unit of work interface. A concrete implementation of the interface we just described. This implementation will include all the operations that carry out any transactions or data operations together with operations for rolling back or committing changes.
  • A repository class and its interface. The code required to access or modify data is contained in a repository class and its interface. In other words, you should have a repository class that uses each of the interface’s methods and an interface for your repository that specifies the allowed actions.

So, let’s get started!

Create a CustomDbContext class

In Entity Framework or Entity Framework Core, a DbContext represents the gateway to the database. The following is the DbContext class that can be generated automatically when you’re using Entity Framework Core. We’ll use this class several times in the code snippets that follow.

public class CustomDbContext : DbContext
{
    public CustomDbContext(DbContextOptions<CustomDbContext> options)
       : base(options)
    {
    }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
    }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
    }
    public DbSet<Author> Authors { get; set; }
}

Define the interface for the unit of work

Create a new .cs file named IUnitOfWork.cs in your project and enter the following code.

public interface IUnitOfWork : IDisposable
{
    void Commit();
    void Rollback();
    IRepository<T> Repository<T>() where T : class;
}

Implement the unit of work concrete class

Next, you should create the concrete implementation of the IUnitOfWork interface. To do this, create a new class called UnitOfWork in a file having the same name with a .cs extension, and enter the following code.

public class UnitOfWork : IUnitOfWork, IDisposable
{
    private readonly DbContext _dbContext;
    private bool _disposed;
    public UnitOfWork(DbContext dbContext)
    {
        _dbContext = dbContext;
    }
    public void Commit()
    {
        _dbContext.SaveChanges();
    }
    public void Rollback()
    {
        foreach (var entry in _dbContext.ChangeTracker.Entries())
        {
            switch (entry.State)
            {
                case EntityState.Added:
                    entry.State = EntityState.Detached;
                    break;
            }
        }
    }
    public IRepository<T> Repository<T>() where T : class
    {
        return new Repository<T>(_dbContext);
    }
    private bool disposed = false;
    protected virtual void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            if (disposing)
            {
                _dbContext.Dispose();
            }
        }
        this.disposed = true;
    }
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

Define the repository interface

A repository design pattern simplifies data access by abstracting the logic from the other components and modules of the application. Your objects will be persisted without your having to work directly with the logic used to access the underlying database. In other words, a repository encapsulates the logic used to access the data source.

The next step is to define the interface for your repository. This interface should provide the declarations of the methods supported by the repository.

Create a new interface named IRepository in a file named IRepository.cs and replace the default generated code with the following code.

public interface IRepository<T> where T : class
{
    T GetById(object id);
    IList<T> GetAll();
    void Add(T entity);
}

Implement the repository interface

Lastly, you should implement the repository interface we just created. To do this, create a new file named Repository.cs and replace the default generated code with the following code.

public class Repository<T> : IRepository<T> where T : class
{
    private readonly CustomDbContext _dbContext;
    private readonly DbSet<T> _dbSet;
    public T GetById(object id)
    {
        return _dbSet.Find(id);
    }
    public Repository(DbContext dbContext)
    {
        _dbContext = dbContext;
        _dbSet = _dbContext.Set<T>();
    }
    public IList<T> GetAll()
    {
        return _dbSet.ToList();
    }
    public void Add(T entity)
    {
        _dbSet.Add(entity);
    }
}

Register the DbContext

As a bonus, you should register the DbContext as a service in the Program.cs file so that you can take advantage of dependency injection to retrive DbContext instances in the Repository and UnitOfWork classes.

There are two ways to do this: using the AddDbContext method or using the AddDbContextPool method. The AddDbContextPool method is preferred because using a pool of instances will help your application to scale.

builder.Services.AddDbContextPool<CustomDbContext>(o=>o.UseSqlServer("Specify the database connection string here..."));

In this article, we built a generic repository implementation that uses an IRepository interface and the Repository class. You can create a repository for a specific entity by creating a class that implements the generic IRepository<T> interface as shown below.

public class AuthorRepository : IRepository<Author>
{
   //Write code here to implement the members of the IRepository interface
}

Repositories and the unit of work design pattern create an abstraction layer between your business logic and the data access layer, making life much easier for the developer. The unit of work pattern uses a single transaction or a single unit of work for multiple insert, update, and delete operations. These operations either succeed or failure as an entire unit. In other words, all of the operations will be committed as one transaction or rolled back as a single unit.

Whereas the unit of work pattern is used to aggregate several operations in a single transaction, repositories represent types that encapsulate the data access logic, separating that concern from your application.

Copyright © 2023 IDG Communications, Inc.