EF Core Database

Repository Pattern & Unit of Work – Entity Framework Core and .NET 6

In this article, I will give a brief introduction to the Repository pattern and the Unit of Work pattern. We will also see a practical example of how to implement these repositories with Entity Framework Core and .Net 6

Introduction to patterns Repository & Unit of Work

What is the repository pattern?

A repository is nothing more than a class with data persistence code coordinated by a unit of work (DBContext in EF Core). This class contains all possible operations on that specific persistence.

So our application won’t care what type of ORM we are using, since everything related to the ORM is handled within a repository layer. This allows us to have a cleaner separation of concerns. The repository pattern is one of the most widely used design patterns to create cleaner solutions.

There may also be cases where we need to use multiple ORMs in a single solution. Probably Dapper to get the data and EFCore to write the data. This is for performance optimizations only.

The repository pattern helps us achieve this by creating an abstraction on top of the data access layer. Now, we no longer have to depend on EF Core or any other ORM for our application.

Why do we want to use repository pattern?

There are many reasons to use this pattern:

  • Reduce code duplication.
  • Loose coupling to the underlying persistence technology.
  • Testability is much easier, the repository pattern will allow us to mock our database so we can perform our tests.
  • Separation of concerns, separate application functionalities based on role, making it easier to evolve and maintain code.

Is the repository pattern dead?

This is one of the most debated topics within the .NET Core community. Microsoft has built EF Core using the Repository pattern and Unit Of Work pattern. 

So why do we need to add another abstraction layer on top of EF Core, which is yet another data access abstraction? Microsoft recommend using repository patterns in complex scenarios to reduce coupling and provide better testability of your solutions. In cases where you want the simplest code possible, you’ll want to avoid the repository pattern.

One more disadvantage of directly using the DbContext directly is that you would be exposing yourself and this is totally unsafe.

What is Unit of Work?

If the repository pattern is our abstraction on the idea of ​​persistent storage, the UoW pattern is our abstraction on the idea of ​​atomic operations. UoW is referred to as a single transaction involving multiple insert, update, delete, etc. operations.

The UoW pattern now manages database states. Once all updates to the entities in a scope are complete, the tracked changes are replicated to the database in one transaction so that the database reflects the desired changes.

Transactions allow you to process various database operations atomically. If the transaction is committed, all operations are successfully applied to the database. If the transaction is rolled back, none of the operations are applied to the database.

Transactions with Entity Framework Core

By default, if the database provider supports transactions, all changes to a call to  SaveChanges are applied to one transaction. If any of the changes fail, the transaction will be rolled back and none of the changes will be applied to the database. This means that it is guaranteed to  SaveChanges complete successfully or leave the database unchanged if an error occurs.

using var context = new ApplicationContext();

// NEW TRANSACTION
var post = context.Posts.Add(new Post() { Title = "My post" });

context.Users.Add(new User()
{
    FirstName = "Alberto",
    PostId = post.Id
});

context.SaveChanges();
// COMMIT

// NEW TRANSACTION
context.Comments.Add(new Comment() { text = "My comment" });
context.SaveChanges();
// COMMIT

Also EF Core allows us multiple SaveChanges in a single transaction , i.e. create or use a single transaction with multiple  SaveChanges():

  • DbContext.Database.BeginTransaction() , creates a new transaction for the underlying database and allows us to commit or roll back the changes made to the database through multiple calls to the SaveChanges method. If we throw an exception after the first call to SaveChanges(). This will execute a catch block where we call the RollBack() method to roll back any changes that have been made to the database.
using var context = new ApplicationContext();

try
{
    // NEW TRANSACTION
    var post = context.Posts.Add(new Post() { Title = "My post" });

    context.Users.Add(new User()
    {
        FirstName = "Alberto",
        PostId = post.Id
    });

    context.SaveChanges();

    // throw exception to test roll back transaction
    throw new Exception();

    context.Comments.Add(new Comment() { text = "My comment" });

    context.SaveChanges();

    transaction.Commit();
    // COMMIT
}
catch (Exception ex)
{
    transaction.Rollback();
    Console.WriteLine("Error occurred.");
}

We will talk more in depth about transactions in another post, here we will focus on repository and UoW patterns.

Let’s go to code👇

In our project we will basically implement 2 interfaces (IRepository and IReadRepository) for our repositories:

  • An interface called IRepository, in which we are going to define all the update operations and data queries:
public interface IRepository<TEntity> : IRepositoryBase<TEntity> where TEntity : class { }
public interface IRepositoryBase<TEntity> : IReadRepositoryBase<TEntity> where TEntity : class
{
    IUnitOfWork UnitOfWork { get; }

    TEntity Add(TEntity entity);

    Task<TEntity> AddAsync(TEntity entity, CancellationToken cancellationToken = default);

    ICollection<TEntity> AddRange(ICollection<TEntity> entities);

    Task<int> AddRangeAsync(ICollection<TEntity> entities, CancellationToken cancellationToken = default);

    void Delete(TEntity entity);

    Task<int> DeleteAsync(TEntity entity, CancellationToken cancellationToken = default);

    void DeleteRange(ICollection<TEntity> entities);

    Task<int> DeleteRangeAsync(ICollection<TEntity> entities, CancellationToken cancellationToken = default);

    void Update(TEntity entity);

    Task<int> UpdateAsync(TEntity entity, CancellationToken cancellationToken = default);
}
public interface IReadRepositoryBase<TEntity> where TEntity : class
{
    IQueryable<TEntity> GetAll(bool asNoTracking = true);

    IQueryable<TEntity> GetAllBySpec(Expression<Func<TEntity, bool>> predicate, bool asNoTracking = true);

    Task<TEntity?> GetByIdAsync<TId>(TId id, CancellationToken cancellationToken = default) where TId : notnull;

    Task<TEntity?> GetBySpecAsync<Spec>(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default);

    Task<ICollection<TEntity>> ListAsync(CancellationToken cancellationToken = default);

    Task<ICollection<TEntity>> ListAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default);

    Task<int> CountAsync(CancellationToken cancellationToken = default);

    Task<int> CountAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default);

    Task<bool> AnyAsync(CancellationToken cancellationToken = default);

    Task<bool> AnyAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default);

    IQueryable<TEntity> GetAllIncluding(params Expression<Func<TEntity, object>>[] includeProperties);
}

Second, we’ll create a new IReadRepository interface with only database query operations:

public interface IReadRepository<TEntity> : IReadRepositoryBase<TEntity> where TEntity : class { }

Finally, we will have an IUnitOfWork interface for atomic operations:

public interface IUnitOfWork : IDisposable
{
    Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken));

    bool HasActiveTransaction { get; }

    IDbContextTransaction GetCurrentTransaction();

    Task<IDbContextTransaction> BeginTransactionAsync();

    Task CommitAsync(IDbContextTransaction transaction);
}

In short, we use generic repositories for our project and they can be used by any entity or aggregate. If we need to create a repository with custom operations, we would create a repository that inherits from IRepository or IReadRepository and implement our custom operations. Let’s see an example:

public interface ICustomPostRepository : IRepository<Post>, IReadRepository<Post>
{
    new Post Add(Post entity);

    Task<Post?> GetPostWithCommentsAsync(int id);

    IEnumerable<Post> GetPostWithMoreComments(int count);
}

For example, an option is inherited only from IReadRepository and in this way we restrict repository users from updating data, they could only query:

public interface IPostRepository : IReadRepository<Post>
{
    ...
}

All implementations of these interfaces are available in the GitHub code, which you can download. You will also find examples of use of these repositories.

That’s all for now, I’ll keep updating and adding content in this post. I hope you found it interesting😉

Download

The source code for this article can be found on  GitHub

3
5