When developing software, ensuring that your code is maintainable, flexible, and readable is crucial. One effective way to achieve this is by implementing the Specification pattern. This pattern allows for clear and modular encapsulation of business rules and query criteria, promoting the separation of concerns and enhancing the overall quality of your code.
Let's take the example below of adding/removing items to/from a customer's order. When looking up the customer we need to include the current OrderItems and ensure the order is not complete. This logic is duplicated in both the AddItem and RemoveItem methods, which violates the DRY principle.
Even worse, if the logic changes, we need to update it in multiple places, increasing the risk of bugs and inconsistencies. Below we correctly check the orders status when adding items, but not when removing them which is a bug.
public class OrderService(ApplicationDbContext dbContext){public async Task AddItem(Guid orderId, OrderItem item){var order = dbContext.Orders.Include(o => o.OrderItems).FirstOrDefault(o => o.Id == orderId && o.Status != OrderStatus.Complete);order.AddItem(item);await dbContext.SaveChangesAsync();}public void RemoveItem(int age){// Duplicated logic and bug introduced by not checking OrderStatusvar order = dbContext.Orders.Include(o => o.OrderItems).FirstOrDefault(o => o.Id == orderId);order.RemoveItem(item);await dbContext.SaveChangesAsync();}}
❌ Figure: Bad example - Duplicated query logic to fetch the customer
The Specification pattern is a design pattern used to define business rules in a reusable and composable way. It encapsulates the logic of a business rule into a single unit, making it easy to test, reuse, and combine with other specifications.
Steve Smith (aka ["Ardalis"])(https://github.com/ardalis) has created an excellent library called Ardalis.Specifications that integrates well with EF Core.
To use the Specification pattern, follow these steps:
public sealed class TeamByIdSpec : SingleResultSpecification<Team>{public TeamByIdSpec(TeamId teamId){Query.Where(t => t.Id == teamId).Include(t => t.Missions).Include(t => t.Heroes);}}
var teamId = new TeamId(request.TeamId);var team = dbContext.Teams.WithSpecification(new TeamByIdSpec(teamId)).FirstOrDefault();
For an end-to-end example of the specification pattern see the SSW.CleanArchitecture Template.
Re-visiting the example above, we can apply the Specification pattern as follows:
public sealed class OrderByIdSpec : SingleResultSpecification<Order>{public IncompleteOrderByIdSpec(Guid orderId){Query.Include(o => o.OrderItems).Where(o => o.Id == orderId && o.Status != OrderStatus.Complete);}}public class OrderService(ApplicationDbContext dbContext){public async Task AddItem(Guid orderId, OrderItem item){var order = dbContext.Orders.WithSpecification(new OrderByIdSpec(orderIdorderId)).FirstOrDefaultAsync();order.AddItem(item);await dbContext.SaveChangesAsync();}public void RemoveItem(int age){var order = dbContext.Orders.WithSpecification(new IncompleteOrderByIdSpec(orderIdorderId)).FirstOrDefaultAsync();order.RemoveItem(item);await dbContext.SaveChangesAsync();}}
✅ Figure: Good example - Specification used to keep Order query logic DRY