I originally posted an extended version of this post on my blog a long time ago in a galaxy far, far away.
Repositories are the least SOLID part of our codebases.
When we work with Domain-Driven Design, we take care of our business domain and forget about our data-access layer. We end up dumping, in a single interface, every combination of methods and parameters to retrieve our entities from the database. This way, we break the Single Responsibility Principle and Interface Segregation Principle. You see? They're the least SOLID part.
Our repositories become so bloated that to use one specific method from a repository, we end up depending on a huge interface with lots of other single-use methods. GetOrdersById
, GetOrdersByDate
, GetLineItemsByOrderId
...
The Specification Pattern Simplifies Repositories
With the Specification pattern, we extract the "query logic" to another object and away from our repositories.
Instead of making our repositories more specific by adding more methods (_repo.GetOrderById(123456)
), the Specification pattern makes repositories more general (_repo.FirstOrDefault(new OrderById(123456))
).
Think of a Specification as the query logic and the query parameters to retrieve objects.
Specifications make more sense when using Domain-Driven Design. With Specifications, we encapsulate the LINQ queries, scattered all over our code, inside well-named objects that we keep inside our Domain layer.
Here's how to use the Ardalis.Specification NuGet package to create a specification and retrieve a list of movies by their release year:
using Ardalis.Specification; // π
using Ardalis.Specification.EntityFrameworkCore; // π
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped(typeof(Repository<>)); // π
builder.Services.AddDbContext<MoviesContext>(options =>
options.UseInMemoryDatabase("MovieDb"));
var app = builder.Build();
app.MapGet("/movies/{releaseYear}", async (int releaseYear, Repository<Movie> repo) =>
{
var byReleaseYear = new MoviesByReleaseYear(releaseYear); // π
var movies = await repo.ListAsync(byReleaseYear); // π
// As an alternative, with Ardalis.Specification we can
// use a specification directly with a DbContext:
//var movies = await aDbContext.Movies
// .WithSpecification(byReleaseYear)
// .ToListAsync();
return Results.Ok(movies);
});
app.MapPost("/movies", async (Movie movie, Repository<Movie> repo) =>
{
await repo.AddAsync(movie);
await repo.SaveChangesAsync();
// Or, simply with a DbContext:
//await aDbContext.Movies.AddAsync(movie);
//await anyDbContext.SaveChangesAsync();
return Results.Created($"/movies/{movie.Id}", movie);
});
app.Run();
public class MoviesByReleaseYear : Specification<Movie>
// πππ
{
public MoviesByReleaseYear(int releaseYear)
{
Query
.Where(m => m.ReleaseYear == releaseYear)
.OrderBy(m => m.Name);
}
}
public record Movie(int Id, string Name, int ReleaseYear);
public class Repository<T> : RepositoryBase<T> where T : class
// πππ
{
public Repository(MoviesContext dbContext) : base(dbContext)
{
}
}
public class MoviesContext : DbContext
{
public MoviesContext(DbContextOptions<MoviesContext> options)
: base(options)
{
}
public DbSet<Movie> Movies { get; set; }
}
Ardalis.Specification provides a RepositoryBase<T>
class that wraps our DbContext
object and exposes the database operations using Specification objects. The ListAsync()
receives a specification, not an IQueryable
object, for example.
Our Repository<T>
is simply a class definition without query logic. Just a couple of lines of code.
Now the query logic is inside our MoviesByReleaseYear
. Ardalis.Specification translates those filtering and ordering conditions to the right chain of Entity Framework Core methods.
Our repositories are way simpler and the query logic is abstracted to another object.
VoilΓ ! That's how to use the Specification pattern to make our repositories more SOLID.
With the Specification pattern, our repositories have a slim interface and a single responsibility: to turn specifications into database calls.
Starting out or already on the software engineering journey? Join my free 7-day email course to refactor your coding career and save years and thousands of dollars' worth of career mistakes.
Top comments (1)
@canro91 Nice article! and a great way of explanation too!
I tried to write on Specification Pattern in my own words here The Other side of Specification pattern
Have a look and add your thoughts.