Repository Pattern with Specification in ASP.NET Core | Free Guide + GitHub
The repository pattern has been at the center of heated debate around the internet since it was introduced back in the ancient times as part of Domain-Driven Design.
The original goal of repository pattern was to provide a layer of abstraction over data persistence operations. This makes the architecture decoupled so that in the future should you ever want to switch to a different ORM, you can do so with minimal disruption. Let’s get real for a moment, does anyone ever do that? Does anyone ever say halfway through developing an ASP.NET application, “hey let’s use Dapper instead of Entity Framework”? No, because you make that decision knowing what kind of app you are building from the start.
Practicality aside, implementing the repository pattern does decouple the architecture. That is, until Entity Framework came out and made things totally redundant. Entity Framework, the official ORM from Microsoft, is the repository pattern on its own. All those DbSets that provide mapping for new entities are repositories and the context is the unit of work.
So let’s not use repository pattern! End of article.
Well, sort of. How about, let’s not use the traditional implementation of repository pattern (repository per entity) but instead look at how we can use a generic repository with specification pattern to make our architecture awesome.
In a generic repository pattern, we can handle low-level repetitive code like pagination and DTO mapping, while keep things flexible with specification. Also, if you plan to build your application with something like clean-architecture, then the decoupled nature is still nice to have.
The complete code for this tutorial is available from the GitHub here
If you are in the business of building apps, then check out the Nano Boilerplate. The patterns and practices you will learn in this tutorial are quite similar, except that the Nano Boilerplate contains so much more! You can create entire SaaS solutions, controller, and services from the command line. Not to mention, you support our efforts J
Repository Per Entity (Traditional)
First let’s look at what not to do, the repository per entity approach; aka the traditional way of implementing the repository pattern. It involves creating a repository class per each entity in your application. So for example, if you have a Product entity in your application, you would create a ProductRepository class which would contain CRUD operations, plus any specific product operations
public class ProductRepository
{
private readonly ApplicationDbContext _context;
public ProductRepository(ApplicationDbContext context)
{
_context = context;
}
// get all
public async Task<IEnumerable<Product>> GetListAsync()
{
List<Product> list = await _context.Products.ToListAsync();
return list;
}
// get by supplier
public async Task<IEnumerable<Product>> GetListBySupplierAsync(Guid supplierId)
{
List<Product> list = await _context.Products.Where(x => x.Supplier.Id == supplierId).ToListAsync();
return list;
}
// create, update, delete, etc...
}
Another entity in your application like Supplier could look similar but with its own specific operations which are quite rigid.
This is the simple example of the repository pattern you find all over the internet. It just creates so much extra code. Whenever you have a slight variation of a Get request, you are forced to create a new method. In a short time, your app will contain tons of repositories with a large number of methods.
Generic Repository
As the name implies, with the generic repository pattern, we’ll only have one repository class which will be used by all entity types. For the ‘entity specific operations’, we’ll use specification pattern together with the repository pattern so that this one generic repository with standard methods can accommodate any kind of data query. All of our methods will be asynchronous.
CRUD Method (Create)
Let’s start with the simplest method Create so we can understand the generic signature. Eventually our generic repository will contain all the standard CRUD operations plus a few extras like pagination. To get things rolling, create a constructor and inject the application context.
private readonly ApplicationDbContext _context;
public RepositoryAsync(ApplicationDbContext context)
{
_context = context;
}
// create method
public async Task<T> CreateAsync<T, TId>(T entity)
where T : BaseEntity<TId>
{
_ = await _context.Set<T>().AddAsync(entity);
return entity;
}
Our T should be represented by a base entity class which can use any kind of type as an ID, in this case we will use a Guid. There’s not much variation in a typical Create request, so we won’t need any specification pattern yet. If we were to use this repository method in some application service, this is how we would do it. By the way, you can download the finished code from the GitHub repo to see everything, including that within the common classes like BaseEntity, Response, etc.
// create new Product
public async Task<Response<Guid>> CreateProductAsync(CreateProductRequest request)
{
try
{
Product response = await _repository.CreateAsync<Product, Guid>(request);
_ = await _repository.SaveChangesAsync(); // save changes to db
return Response<Guid>.Success(response.Id); // return id
}
catch (Exception ex)
{
return Response<Guid>.Fail(ex.Message);
}
}
Unlike Create, in a Get method there usually is some variance, and therefore we would need the flexibility of specifications. We may want to pass some kind of filter criteria depending on our situation. In the traditional repository per entity approach, we would create a separate method for each scenario like GetProductsByName, GetProductsByType and drive ourselves mad with code. With the specification pattern, we will instead pass a specification. The specification would then be evaluated within the generic repository. This will allow our methods to be super flexible and allow us to build any kind of query.
Specification Pattern
Using specification pattern is the key to making everything work within a generic repository, otherwise our generic repository would be too rigid to accommodate different kinds of queries.
With specification pattern, the idea is to pass generic expressions from your services and build up a query within the repository. At minimum you will need a base specification class to construct queries using LINQ with dynamic expressions. These specifications are then converted to a query by a specification evaluator class within the repository.
As you might imagine, the evaluator class, base specification and any helper methods working to make the specification pattern work can get complex quickly. They need to handle all kinds of query types like the expression itself plus includes, ordering, then include, sorting, etc. Earlier versions of the Nano Boilerplate had a home-rolled implementation of the specification pattern, which admittedly resulted in a lot of code and was a bit confusing. Luckily for us though, someone made the Ardalis Specification NuGet library which does all of this for us!
The Ardalis Specification package is complete package, able to handle every kind of query method supported by Entity Framework.
To get working with the specification pattern in ASP.NET, install the Ardalis Specification package. You’ll also want to get the Ardalis Entity Framework Core package since we’re going to use Entity Framework. With this package installed, Specification classes can be used to build queries. These ORM-agnostic directives will get passed to the repository methods where a SpecificationEvaluator class will translate them into queries. The Nano Boilerplate currently uses the Ardalis Specification in the same way we will do here.
The specification pattern will be used in all our methods that retrieve data.
CRUD Method (Get)
Add a using statement for Ardalis Specification
Let’s make a Get method to retrieve a list of T entity. Write your code like this
// get all, return non-paginated list of domain entities
public async Task<IEnumerable<T>> GetListAsync<T, TId>(ISpecification<T> specification = null, CancellationToken cancellationToken = default) where T : BaseEntity<TId>
{
IQueryable<T> query;
if (specification == null)
{
query = _context.Set<T>().AsQueryable();
}
else
{
query = SpecificationEvaluator.Default.GetQuery(
query: _context.Set<T>().AsQueryable(),
specification: specification);
}
List<T> result = await query.ToListAsync(cancellationToken);
return result;
}
Notice that specification parameter is optional, so in the case that no specification is sent, the full list of T entity will be returned. In the case that a specification is sent, a query will be generated using the SpecifcationEvaluator. If we were to dive into what is happening in the SpecificationEvaluator class, we’d find it to be pretty complex. Luckily we don’t need to worry about it.
In an application service like Product service, we can inject the repository and use the GetListAsync method. We’ll pass it a specification to build a query.
// get full List
public async Task<Response<IEnumerable<ProductDTO>>> GetProductsAsync(string keyword = "")
{
ProductSearchList specification = new(keyword); // ardalis specification
IEnumerable<ProductDTO> list = await _repository.GetListAsync<Product, ProductDTO, Guid>(specification); // full list, entity mapped to dto
return Response<IEnumerable<ProductDTO>>.Success(list);
}
Our specification might look like this:
public class ProductSearchList : Specification<Product>
{
public ProductSearchList(string? keyword = "")
{
// filters
if (!string.IsNullOrWhiteSpace(keyword))
{
_ = Query.Where(x => x.Name.Contains(keyword));
}
_ = Query.OrderByDescending(x => x.Priority); // default sort order
}
}
Perhaps we want search an optional keyword and provide some default ordering by Priority. For this we can create a specification called ProductSearchList. If we had related entities we could add Includes or ThenIncludes with lambda expressions to eager load the data.
One of the really useful things about specification pattern is that you can encapsulate the query logic in the specification classes and name them appropriately. As your project grows into a sprawling monstrosity, this is helpful in keeping things understandable, plus these classes are reusable.
CRUD Methods (Update, Delete, Get Paginated)
For Delete and Update, it’s not really necessary to handle a specification. You might want to create methods for Get by ID instead of a list, Exists to check if an entity is present, and perhaps a Create Range, but that’s up to you. These would make sense to have specification.
Adding a Get method for paginated results method would be quite useful within the repository. This way pagination logic won’t need to be repeated anywhere else. The implementation of a pagination method will depend on how your front-end tables expect the data to be shaped. Since we are getting data, this method would also need a specification parameter. Here is how a pagination method could look if you were using Tanstack Table v8, a very popular front-end table library (used in the Nano Boilerplate). TanStack Table is framework agnostic, so you can use it in a React, Vue, or Angular project all the same.
// return paginated list of mapped dtos -- format specific to Tanstack Table v8 (React, Vue)
public async Task<PaginatedResponse<TDto>> GetTanstackPaginatedResultsAsync<T, TId>(int pageNumber, int pageSize, ISpecification<T> specification = null, CancellationToken cancellationToken = default)
where T : BaseEntity<TId>
{
IQueryable<T> query;
if (specification == null)
{
query = _context.Set<T>().AsQueryable();
}
else
{
query = SpecificationEvaluator.Default.GetQuery(
query: _context.Set<T>().AsQueryable(),
specification: specification);
}
List<TDto> pagedResult;
int recordsTotal;
try
{
recordsTotal = await query.CountAsync(cancellationToken);
pagedResult = await query
.Skip((pageNumber - 1) * pageSize).Take(pageSize)
.ToListAsync(cancellationToken);
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
return new PaginatedResponse<T>(pagedResult, recordsTotal, pageNumber, pageSize);
}
As you can see, it’s the same as with the regular Get methods where we pass the specification to build a query. There are additional parameters to specify page size and starting index. The return format is also different.
It’s worth mentioning that there is a repository class included within the Ardalis Specification package. While you could use it, it’s pretty basic, with no pagination or DTO mapping (which we will see next) so you are better off creating your own (or just stealing this code).
DTO Mapping
Finally to make our repository class even more robust, we can add override methods for all of the methods that return data. The additional methods will give us the option to retrieve data as domain entities or DTOs.
Install the AutoMapper library from Nuget. This is a popular library that can take a lot of the busy work out of translating domain entities to DTO. Add a using statement for the package in the repository class and inject the mapper via the constructor.
Here we can see the code for the Get list method. It’s the same as the regular method except that we pass it an additional type for the DTO in the signature.
// get all, return non-paginated list of mapped dtos
public async Task<IEnumerable<TDto>> GetListAsync<T, TDto, TId>(ISpecification<T> specification = null, CancellationToken cancellationToken = default) where T : BaseEntity<TId>
where TDto : IDto
{
IQueryable<T> query;
if (specification == null)
{
query = _context.Set<T>().AsQueryable();
}
else
{
query = SpecificationEvaluator.Default.GetQuery(
query: _context.Set<T>().AsQueryable(),
specification: specification);
}
List<TDto> result = await query
.ProjectTo<TDto>(_mapper.ConfigurationProvider)
.ToListAsync(cancellationToken);
return result;
}
Using AutoMapper’s projection method, we can pass the mapping configuration. A list of DTOs will be returned instead of domain entities. You’ll need to create a class somewhere that implements the mapping profiles for all your entities. Here I’ve created a class and done my DTO mapping like this for the Product entity.
public class MappingProfiles : Profile // automapper mapping configurations
{
public MappingProfiles()
{
// product mappings
_ = CreateMap<Product, ProductDTO>();
_ = CreateMap<CreateProductRequest, Product>();
_ = CreateMap<UpdateProductRequest, Product>();
// add new entity mappings here...
}
}
Apply this same idea across all the Get methods and pagination to offload the majority of your mapping workload to the repository.
With all this in place our repository is pretty capable! In the attached code you can see the full solution with all the methods; pagination and DTO mapping included. Hopefully this gives you a clear understanding of how to use generic repository pattern in ASP.NET core web applications.
Feel free to use this code in your own projects or check out the Nano Boilerplate for a more complete solution. With the Nano Boilerplate, you can generate full SaaS project customizable from the command line. If you’ve followed along in this tutorial, you will already be familiar with the repository pattern in Nano, but there are many more features like multi-tenancy, clean-architecture, authentication / authorization, user management, and multiple UI options which can save you tons of time when kick starting new projects.
Thanks for reading, good luck with your projects.