.NET 8 Series Starting Soon! Join the Waitlist

14 min read

Specification Pattern in ASP.NET Core - Enhancing Generic Repository Pattern

#dotnet

In this article, we will talk about implementing Specification Pattern in ASP.NET Core applications and how it can enhance the already existing Generic Repository Patterns. We will be building from scratch, an ASP.NET Core WebAPI with Generic Repository Pattern, Entity Framework Core and finally implement the Specification Design Pattern. You can find the complete source code of this implementation here. Let’s get started.

Understanding the Specification Pattern: The Why?

Let’s walk through a simple example to understand the need to use Specification Pattern. Below is a class snippet of Developer with the required properties like Name, Email, Experience, and so on.

public class Developer
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public int YearsOfExperience {get;set;}
public decimal EstimatedIncome {get;set;}
public int Followers { get; set; }
}

Now, we would be probably having a service layer that returns dataset from the DB over an abstraction like Entity Framework Core. Here is how it would look like.

public class DeveloperService : IDeveloperService
{
private readonly ApplicationDbContext _context;
public DeveloperService(ApplicationDbContext context)
{
_context = context;
}
public async Task<IEnumerable<Developer>> GetDeveloperCount()
{
// return a count of all developers in the database
}
}

Although you would be getting the count of all developers, a more practical and logical requirement would be to get the count of developers with some kind of filters, agree? For instance, get the count of developers whose estimated income is $100,000 or more, or the developers with 5 years of experience or more. The possibilities are quite limitless.

However, this would end in you having tons of service layer functions like GetDeveloperCountWithSalariesGreaterThan(decimal minSalary), GetDeveloperCountWithExperienceMoreThan(int minExp), and much more. The more requirement comes in, the more is the count of functions you would end up having. And what if you need the count of developers with salaries greater than x and experience higher than y years? That’s yet another challenge that may result in extra methods.

You may argue that you can directly apply these filters to the Entity Framework Core Entity, something like

await _context.Developers.Where(a=>a.Salary > 10000 && a.Experience > 6).ToListAsync()

But, NO. This is nowhere near a clean application codebase you would need. This approach will end up messing up the scalability of your application quite soon and trust me, this is not at all maintainable. Quick Tip, You always need a service layer within your application that sits between your application and Database and is solely responsible to handle the Business Logics.

This is where your application needs to use Specification pattern. Heads up, there are a few limitations to the Generic Repository Pattern which are resolved with the usage of Specification Pattern. We will build up a project and come to a point where you would practically need to use Specification.

What we will Build.

For demonstration of Specification Pattern in ASP.NET Core, we will build a simple WebAPI Application that has 2 endpoints:

  • Returns a Specific Developer Detail
  • Returns a List of Developers

However, we will add a combination of Generic Repository Pattern along with Unit Of Work to the mix to make this implementation a more logical and practical one. We will specifically identify and implement the use case of Specification Pattern here. This will be almost everything that you would need while building complete applications with ASP.NET Core 5.0. Let’s get started.

PS, you can find the entire source code of this implementation here.

Setting up the Project

First up, let’s open up Visual Studio 2019+ and create a new Solution and a WebAPI Project. Note that we will be following a Hexagonal Architecture in this implementation as well, to keep the solution well organized.

To learn more about Hexagonal / Onion Architecture in ASP.NET Core, please refer to this article - Onion Architecture In ASP.NET Core With CQRS – Detailed

specification-pattern-in-aspnet-core

specification-pattern-in-aspnet-core

Once the API Project is added, let’s add 2 more Class Library projects to this solution. Let’s call it Data & Core.

  • Data is where all the implementations related to the database and contexts live.
  • Core is where we will be adding in the interfaces and domain entities.

This is how the solution would look like at this stage.

specification-pattern-in-aspnet-core

Adding the Required Model

As mentioned earlier, in your Core Project, create a new folder named Entities and add 2 classes to it, namely Developer and Address.

public class Address
{
public int Id { get; set; }
public string City { get; set; }
public string Street { get; set; }
}
public class Developer
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public int YearsOfExperience { get; set; }
public decimal EstimatedIncome { get; set; }
public Address Address { get; set; }
}

Adding the DBContext ,Migrations & Required Packages

Now, let’s install the required NuGet packages to the respective projects.

Open up the Package Manager Console and set the Data Project as the default project from the drop-down. Run the following commands to install the required packages.

Install-Package Microsoft.EntityFrameworkCore
Install-Package Microsoft.EntityFrameworkCore.SqlServer
Install-Package Microsoft.EntityFrameworkCore.Tools

Next, set the API project as the default project, and run the following command.

Install-Package Microsoft.EntityFrameworkCore.Design

Before setting up the Application Context class, let’s add in the Connection String. For this, open up the appsettings.json from the API Project and add in the following.

Note that we are currently using SQLServer Local DB for this demonstration.

"ConnectionStrings": {
"DefaultConnection": "Data Source=(localdb)\\mssqllocaldb;Initial Catalog=specification-pattern-demo;Integrated Security=True;MultipleActiveResultSets=True"
},

With that done, let’s create the required context class that will help us to access the database. For this, under the Data Project, add in a new class and name it ApplicationDbContext.

public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions options) : base(options)
{
}
public DbSet<Developer> Developers { get; set; }
public DbSet<Address> Addresses { get; set; }
}

Here, you can see that we are mentioning both the Developer and Address classes to be in included in the Application Db Context.

Next, we need to add this context to our ASP.NET Core Application’s service container and configure the connection details as well. Open up the Startup.cs in the API Project and add in the following under the ConfigureServices method.

services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

Finally, we are ready to add the migrations and update the database as well. Open up the package manager console once again and set the Data project as the default project. Run the following commands:

add-migration initial
update-database

Here is a screenshot demonstrating the same. Note that you may receive warnings regarding the Precision of the mentioned decimal properties. We can ignore it for now.

specification-pattern-in-aspnet-core

With that done, our database should be ready now with the required tables and the corresponding fields. For demo purposes, I am adding in some sample data directly to the database using the SQL Server Object Explorer tool of Visual Studio 2019 IDE.

specification-pattern-in-aspnet-core

specification-pattern-in-aspnet-core

Implementing Generic Repository Pattern

As our requirement is to return result sets of Developers, let’s create a Generic Repository Pattern so that it can use up the ApplicationDbContext to query data from the database. The importance of using a generic repository pattern is that this code can be reused for multiple other entities as well.

To get a detailed understanding of Repository Pattern, I highly recommend that you go through this article - Repository Pattern in ASP.NET Core – Ultimate Guide

For instance, we add a new entity named Product later, you do not necessarily need to add in a new class for Accessing Product data from the database, but you can use the already existing generic repository implementation for most of the use cases. Note that there are a few limitations for generic repository patterns which we will discuss and resolve in the later section of the article.

Under the Core section, Add a new folder and name it Interfaces. Here, add in a new interface, IGenericRepository.

public interface IGenericRepository<T> where T: class
{
Task<T> GetByIdAsync(int id);
Task<List<T>> GetAllAsync();
}

Note that we are only interested in the Query Operations in this Repository Implementation. Hence, we will only look in to the GetById and GetAll Methods here.

Creating Generic Repository Implementation

Now, let’s implement the above created interfaces. Since we are following a Hexagonal / Onion Architecture, we will have to add the implementations outside the Core of the Application. This means, all the data related implementations are to be added in the Data Project.

Here, add a new class, GenericRepository.

Make sure to add the required project references. Data project refers to Core Project. Core Project has 0 project dependencies. API Project depends on Data as well.

public class GenericRepository<T> : IGenericRepository<T> where T : class
{
protected readonly ApplicationDbContext _context;
public GenericRepository(ApplicationDbContext context)
{
_context = context;
}
public async Task<List<T>> GetAllAsync()
{
return await _context.Set<T>().ToListAsync();
}
public async Task<T> GetByIdAsync(int id)
{
return await _context.Set<T>().FindAsync();
}
}

You can see that we are injecting the instance of the ApplicationDbContext to the constructor of this repository Implementation. This instance is further used to read data from the Database.

Finally, in the Startup.cs of the API Project, add in the following to register the IGenericRepository interfaces to the service container of the application.

services.AddScoped(typeof(IGenericRepository<>), (typeof(GenericRepository<>)));

Problem with Generic Repository Pattern: Anti-Pattern?

Generic Repository is considered by some developers as an anti-pattern. If used incorrectly, yes, any pattern will mess up your code. The major complaint about Generic Repository is that a single method could potentially expose the entire database access code to the user. It could also mean the need for multiple methods for each and every combination of requirements (as mentioned in the beginning of this article). For instance, look at the following interface declaration -

List<T> FindAsync(Expression<Func<T, bool>> query);

This method can be a part of the Generic Repository Pattern to combat the issue we have. But since the method is too generalized, it’s not possible for the Generic Repository to know about the expressions we pass to it. Another idea could be to remove this method from IGenericRepository interface and use it in a new interface, let’s say, IDeveloperRepository which derives from IGenericRepository. This could work, but considering the future additions of entities and requirement changes, this change would not be a smart one to proceed with.

Imagine having 20-30 new entities and having to create tons of new repositories? Not a good idea, yeah? Think about having multiple methods in IDevloperRepository and its implementation like GetDevelopersWithSalariesGreaterThan(decimal salary) & GetDevelopersWithExperienceLessThan(int years), not clean, yeah?

What if there is a much cleaner way to tackle this requirement? This is exactly where Specification Pattern comes handy.

Enhancing Repository Pattern with Specification Pattern in ASP.NET Core

Specification Pattern may feel complex at the first glance. I felt it too. But once you have added certain Base Classes and Evaluators, all you have to do is to create Specification Classes which are usually 2 - 10 lines depending on your requirement. Let’s get started with Specification Pattern In ASP.NET Core.

Under the Core Project, add a new folder and name it Specifications. This is where all the specification related interfaces would go to.

Create a new Interface and name it, ISpecification.cs

public interface ISpecification<T>
{
Expression<Func<T, bool>> Criteria { get; }
List<Expression<Func<T, object>>> Includes { get; }
Expression<Func<T, object>> OrderBy { get; }
Expression<Func<T, object>> OrderByDescending { get; }
}

This is a minimal implementation only. Let me explain each of the declared method definitions.

  • Criteria - This is where you can add in the expressions based on your entity.
  • Includes - If you want to include foreign keyed table data, you could add it using this method.
  • OrderBy and OrderByDescending are quite self-explanatory.

Next, in the same folder, add in a new class, BaseSpecifcation. This will be the implementation of the ISpecification Interface.

public class BaseSpecifcation<T> : ISpecification<T>
{
public BaseSpecifcation()
{
}
public BaseSpecifcation(Expression<Func<T, bool>> criteria)
{
Criteria = criteria;
}
public Expression<Func<T, bool>> Criteria { get; }
public List<Expression<Func<T, object>>> Includes { get; } = new List<Expression<Func<T, object>>>();
public Expression<Func<T, object>> OrderBy { get; private set; }
public Expression<Func<T, object>> OrderByDescending { get; private set; }
protected void AddInclude(Expression<Func<T, object>> includeExpression)
{
Includes.Add(includeExpression);
}
protected void AddOrderBy(Expression<Func<T, object>> orderByExpression)
{
OrderBy = orderByExpression;
}
protected void AddOrderByDescending(Expression<Func<T, object>> orderByDescExpression)
{
OrderByDescending = orderByDescExpression;
}
}

Here, we will be adding 3 essential methods and a constructor.

  • Adds the expressions to the Includes property
  • Adds the expressions to OrderBy Property
  • Adds the expressions to OrderByDescending Property
  • You can note that we also have a constructor that takes in criteria. Criteria can be like ( x=>x.Salary > 100 ) and so on. You get the point, yeah?

Upgrading the Generic Repository.

To get started, let’s add a method in our IGenericRepository interface.

IEnumerable<T> FindWithSpecificationPattern(ISpecification<T> specification = null);

Next, let’s implement the new method in the GenericRepository class.

public IEnumerable<T> FindWithSpecificationPattern(ISpecification<T> specification = null)
{
return SpecificationEvaluator<T>.GetQuery(_context.Set<T>().AsQueryable(), specification);
}

Now, the idea behind setting all this up is to create separate specification classes that can return specific result sets. Each of these new specification classes will inherit from the BaseSpecification class. Get the idea? Let’s create those Specification classes now so that it makes sense ;)

So, let’s draw out 2 requirements / specifications:

  1. A specification to return list of developers in the decreasing order of salary.
  2. Another specification that returns a list of developers with an experience of N or above along with their addresses.

Under the same Specification folder of the Core project, let’s add our first specification class, DeveloperByIncomeSpecification

public class DeveloperByIncomeSpecification : BaseSpecifcation<Developer>
{
public DeveloperByIncomeSpecification()
{
AddOrderByDescending(x => x.EstimatedIncome);
}
}

Here, you can see that we are deriving from the BaseSpecification class and using the AddOrderByDescending method within the constructor. This specification would ideally return a list of developers with the decreasing order of their income.

Next, let’s add another class, DeveloperWithAddressSpecification

public class DeveloperWithAddressSpecification : BaseSpecifcation<Developer>
{
public DeveloperWithAddressSpecification(int years) : base(x=>x.EstimatedIncome > years)
{
AddInclude(x => x.Address);
}
}

So, here we are passing the query expression to the base of the Specification Class which is the BaseSpecification’s constructor, which in turn will add it to the Criteria property that we had created earlier. Quite simple, actually.

Now, with our specification classes ready, let’s add the api endpoints.

Under the API project, add a new API Controller under the Controllers folder and name it DevelopersController.

public class DevelopersController : ControllerBase
{
public readonly IGenericRepository<Developer> _repository;
public DevelopersController(IGenericRepository<Developer> repository)
{
_repository = repository;
}
[HttpGet]
public async Task<IActionResult> GetAll()
{
var developers = await _repository.GetAllAsync();
return Ok(developers);
}
[HttpGet("{id}")]
public async Task<IActionResult> GetById(int id)
{
var developer = await _repository.GetByIdAsync(id);
return Ok(developer);
}
[HttpGet("specify")]
public async Task<IActionResult> Specify()
{
var specification = new DeveloperWithAddressSpecification(3);
//var specification = new DeveloperByIncomeSpecification();
var developers = _repository.FindWithSpecificationPattern(specification);
return Ok(developers);
}
}

Lines 3 - 7: Injecting IGenericRepository to the constructor of the Controller.
Lines 8 - 19: Standard endpoints that make use of the repository instance to return all the developers and the one with specific Id.

Lines 20 - 27: This is the most interesting part of the controller. Here Line 23 and 24 as you can see are the 2 specification classes we created earlier. This is just to demonstrate that any such Specification instance can be created at either the Controller or wherever the GenericRepository is being consumed. We’ll use the DeveloperWithAddressSpecification(3) for demonstration.

Let’s run the application now and check the result from the specify endpoint.

specification-pattern-in-aspnet-core

You can see that the Address data is also returned. Now, go back to the controller, comment out line 24 and let’s use the DeveloperByIncomeSpecification for now. Again, run the application.

specification-pattern-in-aspnet-core

Now you can notice that there no Address data being returned. Why? Simple, because we used a different specification that does not mention the addition of the Address entity. Rather this specification returns the set of developers in the decreasing order of their income. Simple, yet neat right? This is probably one of the coolest design patterns to have on your ASP.NET Core applications.

Weird, but this is actually when you can understand what Specification pattern is :P According to Wikipedia - In computer programming, the specification pattern is a particular software design pattern, whereby business rules can be recombined by chaining the business rules together using boolean logic. The pattern is frequently used in the context of domain-driven design.

Makes much more sense now, yeah? Business Rules (our requirement to return developers with a certain level of experience or higher) are combined by chaining criteria (this happened in the DeveloperWithAddressSpecification class) which is a boolean logic. Very simple, yet too powerful ;)

Moving forward, the possibilities with this pattern is quite endless and helps very much in scaling up the application. This pattern could potentially support Data-Shaping and Pagination as well. Pretty powerful pattern with a very little learning curve, yeah? That’s a wrap for this article.

Summary

In this article, we have covered Specification Pattern in ASP.NET Core Applications and how it enhances Generic Repository Pattern by giving it an upper hand. We have also built a complete web API application that follows onion architecture for clean code management. You can also find the complete source code on my Github here. Have any suggestions or questions? Feel free to leave them in the comments section below. Thanks and Happy Coding! 😀

Source Code ✌
Grab the source code of the entire implementation by clicking here. Do Follow me on GitHub .
Support ❀
If you have enjoyed my content and code, do support me by buying a couple of coffees. This will enable me to dedicate more time to research and create new content. Cheers!
Share this Article
Share this article with your network to help others!
What's your Feedback?
Do let me know your thoughts around this article.

Boost your .NET Skills

I am starting a .NET 8 Zero to Hero Series soon! Join the waitlist.

Join Now

No spam ever, we are care about the protection of your data. Read our Privacy Policy