Welcome to today’s post.
In today’s post I will be discussing the repository pattern and how we can use it within .NET Core.
As we know, the use of the Repository pattern is to provide a layer of abstraction for the data context for our ORM library. In this case, for .NET Core the ORM library is Entity Framework Core.
I will demonstrate the following useful tasks that allow us to prepare and implement a Repository pattern within .NET Core:
- Determine the Repository members.
- Implement the Repository interface using a generic type.
- Implement the generically typed Repository as a concrete type.
- Implement a concrete Repository for the target data type.
- Configure the repository interfaces and repository concrete types at application start up.
- Inject the interfaces into our custom service classes.
With Entity Framework Core, the DbSet class implements a repository with a unit of work. For this reason, EF Core data operations involving multiple tables within a schema will need to be explicitly wrapped into their own database transactions with commits and rollbacks. Distributed database transactions involving multiple tables are not supported in the same way we see with Entity Framework.
Implementation of a Generic Repository
For this discussion I will show how to create a basic generic repository which I will then inject into a custom service using Dependency Injection.
The first step is to decide how we will design the repository. As with all Web API service applications, we will include the following features:
- The repository should be re-useable for any table within the DBSet of our data context.
- The CRUD data operations add, update, delete.
- The CRUD operations will all be asynchronous.
- Instances of the repository will be stateless and transient.
Given our repository supports any table within our data context, the interface will be type generically typed. This is shown below:
using System;
using System.Threading.Tasks;
using System.Collections.Generic;
namespace BookLoan.Catalog.API.Repository
{
public interface IRepository<T>
{
Task<List<T>> GetAll();
Task<T> GetByIdAsync(int id);
Task<T> AddAsync(T entity);
Task<T> UpdateAsync(T entity);
Task<T> DeleteAsync(T entity);
}
}
As our data operations will be asynchronous, we will be using the threading libraries to make us of the async and await pattern and the Task<> return type for asynchronous methods.
Implementation of a Generic Repository with Entity Framework Core
The generic repository implementation includes using the EF Core methods that cover basic asynchronous CRUD functionality. These include:
FindAsync(), Set(), AddAsync(), Update(), Remove() and SaveChangesAsync().
The implementation for a generic repository is shown below:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
namespace BookLoan.Catalog.API.Repository
{
public class Repository<T>: IRepository<T> where T: class, new()
{
protected readonly ApplicationDbContext _db;
public Repository(ApplicationDbContext db)
{
_db = db;
}
public async Task<T> GetByIdAsync(int id)
{
return await _db.FindAsync<T>(id);
}
public async Task<List<T>> GetAll()
{
return await _db.Set<T>().ToListAsync();
}
public async Task<T> AddAsync(T entity)
{
if (entity == null)
throw new ArgumentNullException("AddAsync(): entity parameters is null.");
try
{
await _db.AddAsync(entity);
await _db.SaveChangesAsync();
return entity;
}
catch (Exception ex)
{
throw new Exception($"AddAsync(): cannot insert data : {ex.Message}");
}
}
public async Task<T> UpdateAsync(T entity)
{
if (entity == null)
throw new ArgumentNullException("UpdateAsync(): entity parameters is null.");
try
{
_db.Update(entity);
await _db.SaveChangesAsync();
return entity;
}
catch (Exception ex)
{
throw new Exception($"UpdateAsync(): cannot update data : {ex.Message}");
}
}
public async Task<T> DeleteAsync(T entity)
{
if (entity == null)
throw new ArgumentNullException("DeleteAsync(): entity parameters is null.");
try
{
_db.Remove(entity);
await _db.SaveChangesAsync();
return entity;
}
catch (Exception ex)
{
throw new Exception($"DeleteAsync(): cannot delete data : {ex.Message}");
}
}
}
}
Note that when we declare a generically typed class and we want to ensure the class satisfies the following constraints:
- It cannot be instantiated without a type.
- The instantiated type must be a class.
- The class must have a public constructor.
- The constructor must be parameter less.
The constraint to provide for the above conditions is this part of the declaration:
where T: class, new()
There are other ways we can impose constraints on our classes, and these are explained in detail for C# generics and types on the Microsoft site.
Implementation of a Concrete Repository with the Target Type
The repository interface for our concrete BookView includes additional custom methods that are not included within the generic repository interface and implementation. The interface for our table is shown below:
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using BookLoan.Models;
namespace BookLoan.Catalog.API.Repository
{
public interface IBookRepository
{
Task<List<BookViewModel>> GetAll();
Task<BookViewModel> GetByIdAsync(int id);
Task<BookViewModel> AddAsync(BookViewModel entity);
Task<BookViewModel> UpdateAsync(BookViewModel entity);
Task<BookViewModel> DeleteAsync(BookViewModel entity);
Task<BookViewModel> GetByTitle(string title);
}
}
The implementation for the Book repository is implemented from our book view interface and the generic repository typed on the BookView model. The core repository members are extended from the generic IRepository interface and the additional member method GetByTitle() from IBookRepository is implemented below in the concrete implementation of the Book repository:
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using BookLoan.Models;
using BookLoan.Data;
namespace BookLoan.Catalog.API.Repository
{
public class BookRepository : Repository<BookViewModel>, IBookRepository
{
public BookRepository(ApplicationDbContext db) : base(db) {}
public async Task<BookViewModel> GetByTitle(string title)
{
return await _db.Books.Where(b => b.Title == title).SingleOrDefaultAsync();
}
}
}
Before we can use our repository as an interface that can be injected within our .NET Core application, we will need to set up the repository instance mapping by adding them into the service collection in ConfigureServices() within our startup.cs:
public void ConfigureServices(IServiceCollection services)
{
…
services.AddTransient<IBookService, BookService>();
services.AddTransient(typeof(IRepository<>), typeof(Repository<>));
services.AddTransient<IBookRepository, BookRepository>();
…
}
To inject the repository within a custom service class we just include the interface declaration within the constructor and assign it to a private interface variable within our class. The DI binding does the rest.
using BookLoan.Catalog.API.Repository;
…
namespace BookLoan.Services
{
public class BookService: IBookService
{
readonly ApplicationDbContext _db;
private readonly IBookRepository _bookRepository;
public BookService(ApplicationDbContext db,
IBookRepository bookRepository)
{
_db = db;
_bookRepository = bookRepository;
}
public async Task<List<BookViewModel>> GetBooks()
{
return await _bookRepository.GetAll();
}
…
}
}
Before we would have had direct references to the data context and the operations would be like that shown below:
return await _db.Books.ToListAsync();
Once we have entirely replaced all our lookup and CRUD operations with repository methods, then we can eliminate the data context declaration for ApplicationDbContext from our classes. This will clean up our code and make it more testable and decoupled from the data context.
Should we follow the above steps, run, and build our application then any data operations will go through the repository. We can then proceed to write unit tests for our repository and test those in isolation of the service classes. This effectively abstracts the data context away from the custom classes and makes them comply more with the separation of concerns.
That is all for this post.
I hope you found this post useful and informative.
![](http://andrewhalil.com/wp-content/uploads/2021/03/Avatar-Pic.jpg)
Andrew Halil is a blogger, author and software developer with expertise of many areas in the information technology industry including full-stack web and native cloud based development, test driven development and Devops.