Implementing an async Repository and Unit of Work with Entity Framework Core

Implementing the Repository pattern in conjunction with the Unit of Work pattern is a best practice for data access that can bring a number of benefits to your application codebase when implemented correctly.

Given the ease of use and scalability advantages that async .NET code offers, it makes sense to ensure that your data access logic is implemented in an asynchronous fashion whenever possible.

This article explains how to implement the Repository and Unit of Work patterns in .NET Core applications, exposing an asynchronous interface and leveraging the power of Entity Framework Core underneath to communicate with a SQL Server database.

Pattern definitions

Before diving into the implementation details, let me remind you of what the Repository and Unit of Work patterns actually are and the problems that they are designed to solve.

Repository pattern

In regards to the Repository pattern, let’s start with a definition from Martin Fowler’s book, Patterns of Enterprise Application Architecture.

Mediates between the domain and data mapping layers using a collection-like interface for accessing domain objects.

Essentially, the Repository pattern is an abstraction that “isolates domain objects from details of the database access code” resulting in a decoupled architecture.

Let’s look at some of the benefits of the Repository pattern to help explain it further.

Isolation

Isolation of the data access code is a key benefit of the pattern, as it means that we can potentially change the data layer (i.e. persistence framework) without affecting any of our business logic.

Application business logic should not depend on a specific database technology or persistence framework. Instead, the business logic should interface with an abstraction that hides the underlying complexity.

There are lots of persistence frameworks out there and new ones are springing up all the time. Having the capability to switch to a different framework, or upgrade to a new version of an existing framework that includes breaking changes becomes much more practical when the Repository pattern is implemented properly.

Minimal duplication

Another benefit of the Repository pattern is that it helps to minimise code duplication. As an example, imagine we have created a ‘Todos’ tracking app and we want to retrieve the top ‘X’ incomplete todos for display in several places within our app, as follows.

var top10IncompleteTodos = await _dbContext.Todos
    .Where(t => !t.Completed)
    .OrderBy(t => t.Title)
    .Take(10)
.ToListAsync();

Without the repository pattern or some other kind of abstraction, the above logic would potentially be repeated across different parts of our codebase. A better approach would be to encapsulate the logic within a repository and instead call a method like the one shown in the example below.

var todos = await repository.GetTopXIncompleteTodosAsync(10);

This is much cleaner and allows queries to be nicely encapsulated within the Repository.

Testability

A further benefit of the repository pattern is that it can help with code testability.

When the repository pattern is implemented correctly whereby interfaces are used for each repository class, it’s much easier to test data access code.

Saving…

One important thing to point out at this stage is that repository classes should only contain collection-style methods that are similar in concept to Get, Add, Delete or Remove etc.

This means that repositories should not contain a Save method. As an example, think of the .NET List collection type; it doesn’t have a Save method does it?

So how do we save repository changes to the database then?

This is where the Unit of Work pattern comes in.

Unit of Work pattern

Let’s take another definition from Martin Fowler’s book, Patterns of Enterprise Application Architecture, to help define the Unit of Work pattern.

Maintains a list of objects affected by a business transaction and coordinates the writing out of changes and the resolution of concurrency problems.

If we think of the Unit of Work pattern in conjunction with the Repository pattern, this means that if we write a Unit of Work class in our application, we should add all of our repositories as properties attached to the Unit of Work class.

We’ll then need a Save method defined in the Unit of Work class that takes care of saving any referenced repositories that contain changes.

Abstraction benefits

It is important to note that some developers think that because Entity Framework Core has Repository and Unit of Work design pattern characteristics that there is no need to implement another Repository and Unit of Work abstraction on top of it.

The problem with this thinking, however, is that the benefits of the Repository pattern that were laid out in the previous section are lost. For example, without implementing your own abstraction, your codebase will be tightly coupled to Entity Framework Core. You’ll also potentially be duplicating data access code across your application unless you introduce a different abstraction such as a service layer that sits on top of the Entity Framework Core code.

Async implementation

Now that we’ve covered the basics regarding what the Repository and Unit of Work patterns are and their benefits, let’s get stuck into the implementation. The sections that follow look at how we can implement the patterns in an asynchronous fashion using Entity Framework Core as the underlying persistence framework that connects to a SQL Server database.

In order to follow along, you’ll need to install the Entity Framework Core SQL Server NuGet package into your project.

I also recommend creating a separate ‘Repository’ project and a separate ‘Entities’ project. This will help to keep your data access logic and associated models separated from the rest of your application and will facilitate code reuse.

The full source code shown in this article can be found within the accompanying GitHub repository.

Please note that I highly recommend you check out the source code so that you can see the complete implementation. This includes some aspects that aren’t included as part of the code samples shown in this article, such as using statements, database context classes, a SQL database creation script, and model classes such as Todo etc.

Base repository interface

The first thing we need to do in order to implement the Repository pattern in our application is to define a base repository interface, such as IRepository or IBaseRepository as follows.

/// <summary>
/// Base Repository Interface.
/// </summary>
/// <typeparam name="T">The Type of Entity to operate on</typeparam>
public interface IBaseRepository<T> where T : class
{
    #region Methods
 
    Task<T>              AddAsync(T entity);
    Task                 DeleteAsync(T entity);
    Task                 DeleteManyAsync(Expression<Func<T, bool>> filter);
    Task<IEnumerable<T>> GetAllAsync();
    Task<T>              GetByIdAsync(int id);
    Task<IEnumerable<T>> GetManyAsync(Expression<Func<T, bool>> filter = null,
                                      Func<IQueryable<T>, IOrderedQueryable<T>> orderBy = null,
                                      int? top  = null,
                                      int? skip = null,
                                      params string[] includeProperties);
 
    #endregion
}

The IBaseRepository interface defines a number of different methods that the base repository will need to implement. The interface declares a generic Type parameter that will allow it to work with different entity types.

The above interface represents a small set of methods that can be extended according to your requirements. For example, you may want to consider adding additional methods, such as  aGetCount method that returns the number of items contained in the repository.

Some developers have preferences regarding the naming of repository methods e.g. you may prefer the term Remove instead of Delete. Do feel free to adjust the naming of the methods and parameters according to your personal taste.

Base Repository

Next, we need to create a class that implements the IBaseRepository interface. It can be defined as follows.

/// <summary>
/// Generic Repository class for performing Database Entity Operations.
/// </summary>
/// <typeparam name="T">The Type of Entity to operate on</typeparam>
public class BaseRepository<T> : IBaseRepository<T> where T : class
{
    #region Readonlys
 
    protected readonly DbContext _dbContext;
    protected readonly DbSet<T>  _dbSet;
 
    #endregion
 
    #region Constructor
 
    /// <summary>
    /// Constructor.
    /// </summary>
    /// <param name="dbContext">The Database Context</param>
    public BaseRepository(DbContext dbContext)
    {
        _dbContext = dbContext;
        _dbSet     = dbContext.Set<T>();
    }
 
    #endregion
 
    #region Methods
 
    /// <summary>
    /// Adds an entity.
    /// </summary>
    /// <param name="entity">The entity to add</param>
    /// <returns>The entity that was added</returns>
    public async Task<T> AddAsync(T entity)
    {
        await _dbSet.AddAsync(entity);
        return entity;
    }
 
    /// <summary>
    /// Deletes an entity.
    /// </summary>
    /// <param name="entity">The entity to delete</param>
    /// <returns><see cref="Task"/></returns>
    public Task DeleteAsync(T entity)
    {
        _dbSet.Remove(entity);
        return Task.CompletedTask;
    }
 
    /// <summary>
    /// Deletes entities based on a condition.
    /// </summary>
    /// <param name="filter">The condition the entities must fulfil to be deleted</param>
    /// <returns><see cref="Task"/></returns>
    public Task DeleteManyAsync(Expression<Func<T, bool>> filter)
    {
        var entities = _dbSet.Where(filter);
 
        _dbSet.RemoveRange(entities);
 
        return Task.CompletedTask;
    }
 
    /// <summary>
    /// Gets a collection of all entities.
    /// </summary>
    /// <returns>A collection of all entities</returns>
    public async Task<IEnumerable<T>> GetAllAsync()
    {
        return await _dbSet.ToListAsync();
    }
 
    /// <summary>
    /// Gets an entity by ID.
    /// </summary>
    /// <param name="id">The ID of the entity to retrieve</param>
    /// <returns>The entity object if found, otherwise null</returns>
    public async Task<T> GetByIdAsync(int id)
    {
        return await _dbSet.FindAsync(id);
    }
 
    /// <summary>
    /// Gets a collection of entities based on the specified criteria.
    /// </summary>
    /// <param name="filter">The condition the entities must fulfil to be returned</param>
    /// <param name="orderBy">The function used to order the entities</param>
    /// <param name="top">The number of records to limit the results to</param>
    /// <param name="skip">The number of records to skip</param>
    /// <param name="includeProperties">Any other navigation properties to include when returning the collection</param>
    /// <returns>A collection of entities</returns>
    public async Task<IEnumerable<T>> GetManyAsync(
Expression<Func<T, bool>> filter = null,         Func<IQueryable<T>, IOrderedQueryable<T>> orderBy = null,         int? top  = null,         int? skip = null,         params string[] includeProperties)     {        IQueryable<T> query = _dbSet;         if (filter != null)         {             query = query.Where(filter);         }         if (includeProperties.Length > 0)         {            query = includeProperties.Aggregate(query, (theQuery, theInclude) => theQuery.Include(theInclude));         }         if (orderBy != null)         {            query = orderBy(query);         }         if (skip.HasValue)         {            query = query.Skip(skip.Value);         }         if (top.HasValue)         {            query = query.Take(top.Value);         }         return await query.ToListAsync();     }     #endregion }

As you can see from the above code, the constructor of the BaseRepository class accepts a DbContext instance that is assigned as a readonly field. This allows the context to be accessed from the base repository methods and it is marked as protected so that sub-classes can also access it when needed.

In the constructor, the specific DbSet to use is created from the DbContext and its type is based on the generic type parameter of the BaseRepository class.

For the most part, the rest of the methods are simple wrappers around the Entity Framework Core DbSet methods.

However, you’ll notice that the GetManyAsync method is a little bit more involved. It provides the flexibility to filter, order, and page results (using the top and skip parameters). The includeProperties parameter is a params string[] type that allows one or more related entities to be included in the result set.

Design considerations

As noted previously, it’s best practice to keep repository methods as closely aligned as possible to the kind of methods you would see in an in-memory collection. However, it’s also important to be pragmatic with design patterns and having the ability to include additional related objects from a database for an entity is a very important consideration, both for the practicality it offers and the performance benefits.

The base repository is generic in the sense that it can be reused across different applications and isn’t tied to a specific database context. Another thing to note is that the methods that return collections are returning the IEnumerable interface, not IQueryable. Returning IQueryable would give the wrong impression to the upper layers of our application, creating a leaky abstraction and allowing custom queries to be built up and mixed into the business logic. This violates the core principles of the Repository pattern. Repository methods are intended to encapsulate your queries so that they are not repeated.

Before moving on, if you look closely, you’ll notice that some of the methods have an ‘Async’ suffix even though they aren’t marked as async methods. This is because they aren’t using the await keyword. However, since these methods return a Task we can still await them from our calling code.

It’s down to personal preference whether you want to keep the interface like this. However, it does have an advantage whereby if a future version of Entity Framework Core offers an asynchronous means of deleting an entity (if that made sense in this context) then the calling code would remain the same and only the base repository method implementation would need to be changed.

Custom Repository interface

Now that we have a base repository to work from, let’s look at what’s involved in creating a custom repository that uses the base repository methods to help minimise duplicate code.

We’ll start off with the interface declaration for the custom repository, as follows.

/// <summary>
/// Todo Repository Interface.
/// </summary>
public interface ITodoRepository : IBaseRepository<Todo>
{
    #region Methods
 
    Task<IEnumerable<Todo>> GetTopXIncompleteTodosAsync(int x);
 
    #endregion
}

The ITodoRepository stipulates that an implementing class must implement the IBaseRepository interface. This means that whatever class implements ITodoRepository will have all of the same public methods as IBaseRepository plus any additional methods we have defined.

For the sake of simplicity, the interface defines a single additional method that must be implemented to get the top ‘X’ incomplete Todo items from the repository.

To implement the Repository pattern correctly you should make sure to create a separate repository interface for each entity that you need to interact with. Each repository interface should define the methods that are specific to that repository and are not already part of the generic base repository.

Custom Repository

The last part to cover for the Repository pattern is the implementation of the concrete custom repository. This can be defined as follows.

/// <summary>
/// Todo Repository.
/// </summary>
public class TodoRepository : BaseRepository<Todo>, ITodoRepository
{
    #region Constructor
 
    /// <summary>
    /// Constructor.
    /// </summary>
    /// <param name="dbContext">The Database Context</param>
    public TodoRepository(AppDbContext dbContext) : base(dbContext)
    {
    }
 
    #endregion
 
    #region Methods
 
    /// <summary>
    /// Gets the top X incomplete Todo items.
    /// </summary>
/// <param name="x">The maximum number of Todo items to return</param>      /// <returns>A collection of Todo items</returns>     public async Task<IEnumerable<Todo>> GetTopXIncompleteTodosAsync(int x)     {         return await GetManyAsync(             filter : t => !t.Completed,              orderBy: t => t.OrderBy(t => t.Title),              top    : x);     }     #endregion }

The TodoRepository class inherits from the BaseRepository. This provides access to the underlying generic methods of the base repository and means that the TodoRepository will implement the IBaseRepository interface required by the ITodoRepository interface.

In the GetTopXIncompleteTodosAsync method, the GetManyAsync method of the BaseRepository is used to filter, order, and select the top ‘X’ Todo items from the repository.

By deriving from the BaseRepository class, we are avoiding potential code duplication and make it easier to implement any custom methods that our TodoRepository class exposes.

Some developers like to work with their own application-specific context directly within custom repository classes. To facilitate this, the following property can be added to the TodoRepository class.

private AppDbContext AppDbContext => _dbContext as AppDbContext;

The AppDbContext can then be used to access DbSet objects and build queries as required.

Unit of Work interface

Next up is the implementation of the Unit of Work pattern.

By creating a Unit of Work abstraction, we’ll be able to make updates to more than one repository and then commit all of the associated changes to the database in a transactional manner.

Below is the definition of the Unit of Work interface.

/// <summary>
/// Unit of Work Interface.
/// </summary>
public interface IUnitOfWork : IAsyncDisposable
{
    #region Properties
 
    ITodoRepository TodoRepository { get; }
    IUserRepository UserRepository { get; }
 
    #endregion
 
    #region Methods
 
    Task CompleteAsync();
 
    #endregion
}

Note that the above IUnitOfWork interface is application-specific since it needs to include properties that hold references to custom repository interfaces.

In the above code, a repository interface property is defined for each repository that makes up our application database. As mentioned previously, by using interfaces we can more easily mock up a Unit of Work and the associated Repositories for unit testing.

The CompleteAsync method represents the method that will commit all repository changes to the database.

Unit of Work

The last part to cover in regards to the Unit of Work pattern is the implementation of the concrete UnitOfWork class. This can be defined as follows.

/// <summary>
/// Encapsulates all repository transactions.
/// </summary>
public class UnitOfWork : IUnitOfWork
{
    #region Properties
 
    private TodoRepository _todoRepository;
    public ITodoRepository TodoRepository => _todoRepository ?? (_todoRepository = new TodoRepository(_dbContext));
 
    private UserRepository _userRepository;
    public IUserRepository UserRepository => _userRepository ?? (_userRepository = new UserRepository(_dbContext));
 
    #endregion
 
    #region Readonlys
 
    private readonly AppDbContext _dbContext;
 
    #endregion
 
    /// <summary>
    /// Constructor.
    /// </summary>
    /// <param name="dbContext">The Database Context</param>
    public UnitOfWork(AppDbContext dbContext)
    {
        _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
    }
 
    #region Methods
 
    /// <summary>
    /// Completes the unit of work, saving all repository changes to the underlying data-store.
    /// </summary>
    /// <returns><see cref="Task"/></returns>
    public async Task CompleteAsync() => await _dbContext.SaveChangesAsync();
 
    #endregion
 
    #region Implements IDisposable
 
    #region Private Dispose Fields
 
    private bool _disposed;
 
    #endregion
 
    /// <summary>
    /// Cleans up any resources being used.
    /// </summary>
/// <returns><see cref="ValueTask"/></returns>
    public async ValueTask DisposeAsync()     {         await DisposeAsync(true);         // Take this object off the finalization queue to prevent          // finalization code for this object from executing a second time.         GC.SuppressFinalize(this);     }
    /// <summary>
/// Cleans up any resources being used.
/// </summary>
/// <param name="disposing">Whether or not we are disposing</param> 
/// <returns><see cref="ValueTask"/></returns>     protected virtual async ValueTask DisposeAsync(bool disposing)     {         if (!_disposed)         {
       if (disposing)         
{
// Dispose managed resources.
await _dbContext.DisposeAsync();
}
            // Dispose any unmanaged resources here...             _disposed = true;         }     }     #endregion }

The UnitOfWork class holds references to each of our repositories, as per the interface definition. The above code uses a lazy-loading technique such that a repository instance is only created the first time it is accessed. As you can see from the code sample, all repositories use the same database context instance to allow for the transactional posting of changes to the underlying entities.

The UnitOfWork class constructor accepts a AppDbContext instance that is assigned as a readonly field. Because the UnitOfWork class is specific to our application, the database context that is passed in should be the context referencing the specific DbSet properties that are used by our application.

The CompleteAsync method delegates to the SaveChangesAsync method of the DbContext class to persist any unsaved changes to the database.

Lastly, the IAsyncDisposable interface is implemented by means of the DisposeAsync methods. The code uses a standardised pattern to take care of disposing resources i.e. the database context.

Example usage

To wrap things up, let’s take a quick look at how this implementation can be used in a .NET application.

.NET Core

In any .NET Core application, you can write code similar to be following within an async method.

var connectionstring = Configuration.GetConnectionString("DefaultConnection");
 
var builder = new DbContextOptionsBuilder<AppDbContext>();
builder.UseSqlServer(connectionstring);
 
await using (var unitOfWork = new UnitOfWork(new AppDbContext(builder.Options)))
{
    var todos = await unitOfWork.TodoRepository.GetTopXIncompleteTodosAsync(10);
 
    await unitOfWork.TodoRepository.DeleteAsync(todos.First());
 
    await unitOfWork.CompleteAsync();
}

In the above code, a SQL Server connection is built by getting a connection string from the Configuration object and using an instance of the DbContextOptionsBuilder class to create the options that we need to pass to the database context.

A UnitOfWork instance is then instantiated within a using block, passing in a new AppDbContext with the required options. The UnitOfWork instance with the underlying AppDbContext will be disposed of asynchronously when the code reaches the end of the using block.

Within the using block, the code is simply retrieving the top 10 incomplete Todo items from the repository and is then deleting the first item found in the todos collection. Lastly, the CompleteAsync method is called to persist the changes to the database.

ASP.NET Core

If you are developing an ASP.NET Core application, you can leverage the built-in IOC (Inversion of Control) container to inject the IUnitOfWork as a dependency.

In the ConfigureServices method of the Startup class within your application, register the IUnitOfWork as a dependency as follows.

services.AddScoped<IUnitOfWork, UnitOfWork>();

You can now add the IUnitOfWork interface as a parameter to either your MVC controller or Razor Pages model class.

public TodosModel(IUnitOfWork unitOfWork)
{
    _unitOfWork = unitOfWork;
}

In the above example, the constructor of a Razor Pages model class assigns the IUnitOfWork interface to a private readonly field named _unitOfWork.

After this, you can use the injected UnitOfWork instance to access your repositories as follows.

var todos = await _unitOfWork.TodoRepository.GetTopXIncompleteTodosAsync(10);

Now you should have a fully functioning asynchronous Unit of Work and associated asynchronous Repositories that you can use to keep your business logic independent of Entity Framework Core and minimise repeated code.

Summary

In this article, I started off by recapping what the Repository and Unit of Work patterns are and the key benefits that they bring to the table.

I proceeded to demonstrate how to implement asynchronous Repository and Unit of Work interfaces and classes that use Entity Framework Core underneath to connect to a SQL Server database.

I then walked through how to use the Unit of Work and Repositories that were implemented within .NET Core and ASP.NET Core applications by either creating a Unit of Work explicitly or leveraging dependency injection respectively.

In closing, don’t forget to check out the accompanying GitHub repository.


I hope you enjoyed this post! Comments are always welcome and I respond to all questions.

If you like my content and it helped you out, please check out the button below 🙂

Comments

Florian

Hi!
I assume this can be realized with Entity Framework 6 as well? Any hints or pointers on differences I should consider?

May 2, 2024

Jonathan Crozier

Hi Florian,

Yes, this will work for Entity Framework 6. The solution presented in the article is pretty generic and providing you are using the latest available Entity Framework 6 package, the only thing I can think of that won’t compile in the BaseRepository is the await _dbSet.AddAsync(entity); line of code, as there is only a synchronous Add method available on the EF6 DbSet.

If you have any other questions please let me know!

May 2, 2024