Specification Pattern Generator
Overview
The Specification pattern encapsulates query logic:
-
Reusable criteria - Define once, use everywhere
-
Composable - Combine with And/Or
-
Testable - Test query logic in isolation
-
Type-safe - Compile-time checking
Quick Reference
Component Purpose
ISpecification<T>
Base specification interface
BaseSpecification<T>
Abstract implementation
SpecificationEvaluator
Applies spec to IQueryable
Template: Specification Interface
// src/{name}.domain/Abstractions/ISpecification.cs using System.Linq.Expressions;
namespace {name}.domain.abstractions;
public interface ISpecification<T> { Expression<Func<T, bool>>? Criteria { get; } List<Expression<Func<T, object>>> Includes { get; } List<string> IncludeStrings { get; } Expression<Func<T, object>>? OrderBy { get; } Expression<Func<T, object>>? OrderByDescending { get; } int? Take { get; } int? Skip { get; } bool IsPagingEnabled { get; } }
Template: Base Specification
// src/{name}.domain/Abstractions/BaseSpecification.cs using System.Linq.Expressions;
namespace {name}.domain.abstractions;
public abstract class BaseSpecification<T> : ISpecification<T> { public Expression<Func<T, bool>>? Criteria { get; private set; } public List<Expression<Func<T, object>>> Includes { get; } = new(); public List<string> IncludeStrings { get; } = new(); public Expression<Func<T, object>>? OrderBy { get; private set; } public Expression<Func<T, object>>? OrderByDescending { get; private set; } public int? Take { get; private set; } public int? Skip { get; private set; } public bool IsPagingEnabled { get; private set; }
protected void AddCriteria(Expression<Func<T, bool>> criteria) => Criteria = criteria;
protected void AddInclude(Expression<Func<T, object>> include) => Includes.Add(include);
protected void AddInclude(string include) => IncludeStrings.Add(include);
protected void ApplyOrderBy(Expression<Func<T, object>> orderBy) => OrderBy = orderBy;
protected void ApplyOrderByDescending(Expression<Func<T, object>> orderBy) => OrderByDescending = orderBy;
protected void ApplyPaging(int skip, int take)
{
Skip = skip;
Take = take;
IsPagingEnabled = true;
}
}
Template: Concrete Specifications
// src/{name}.domain/{Aggregate}/Specifications/Active{Entities}Specification.cs namespace {name}.domain.{aggregate}.specifications;
public sealed class Active{Entities}Specification : BaseSpecification<{Entity}> { public Active{Entities}Specification() { AddCriteria(e => e.IsActive); ApplyOrderBy(e => e.Name); } }
public sealed class {Entities}ByOrganizationSpecification : BaseSpecification<{Entity}> { public {Entities}ByOrganizationSpecification(Guid organizationId, bool includeChildren = false) { AddCriteria(e => e.OrganizationId == organizationId && e.IsActive);
if (includeChildren)
{
AddInclude(e => e.Children);
}
ApplyOrderBy(e => e.Name);
}
}
public sealed class {Entity}ByIdSpecification : BaseSpecification<{Entity}> { public {Entity}ByIdSpecification(Guid id, bool includeAll = false) { AddCriteria(e => e.Id == id);
if (includeAll)
{
AddInclude(e => e.Children);
AddInclude(e => e.Organization);
AddInclude("Children.SubItems"); // String-based deep include
}
}
}
public sealed class Paged{Entities}Specification : BaseSpecification<{Entity}> { public Paged{Entities}Specification(int pageNumber, int pageSize, string? searchTerm = null) { if (!string.IsNullOrEmpty(searchTerm)) { AddCriteria(e => e.Name.ToLower().Contains(searchTerm.ToLower())); } else { AddCriteria(e => e.IsActive); }
ApplyOrderByDescending(e => e.CreatedAt);
ApplyPaging((pageNumber - 1) * pageSize, pageSize);
}
}
Template: Specification Evaluator
// src/{name}.infrastructure/Specifications/SpecificationEvaluator.cs using Microsoft.EntityFrameworkCore; using {name}.domain.abstractions;
namespace {name}.infrastructure.specifications;
public static class SpecificationEvaluator { public static IQueryable<T> GetQuery<T>( IQueryable<T> inputQuery, ISpecification<T> specification) where T : class { var query = inputQuery;
if (specification.Criteria is not null)
{
query = query.Where(specification.Criteria);
}
foreach (var include in specification.Includes)
{
query = query.Include(include);
}
foreach (var includeString in specification.IncludeStrings)
{
query = query.Include(includeString);
}
if (specification.OrderBy is not null)
{
query = query.OrderBy(specification.OrderBy);
}
else if (specification.OrderByDescending is not null)
{
query = query.OrderByDescending(specification.OrderByDescending);
}
if (specification.IsPagingEnabled)
{
query = query.Skip(specification.Skip!.Value).Take(specification.Take!.Value);
}
return query;
}
}
Template: Repository Integration
// src/{name}.infrastructure/Repositories/{Entity}Repository.cs public async Task<IReadOnlyList<{Entity}>> GetAsync( ISpecification<{Entity}> specification, CancellationToken cancellationToken = default) { return await SpecificationEvaluator .GetQuery(_dbContext.Set<{Entity}>(), specification) .ToListAsync(cancellationToken); }
public async Task<{Entity}?> GetFirstOrDefaultAsync( ISpecification<{Entity}> specification, CancellationToken cancellationToken = default) { return await SpecificationEvaluator .GetQuery(_dbContext.Set<{Entity}>(), specification) .FirstOrDefaultAsync(cancellationToken); }
public async Task<int> CountAsync( ISpecification<{Entity}> specification, CancellationToken cancellationToken = default) { return await SpecificationEvaluator .GetQuery(_dbContext.Set<{Entity}>(), specification) .CountAsync(cancellationToken); }
Usage in Handler
public async Task<Result<IReadOnlyList<{Entity}Response>>> Handle( Get{Entities}Query request, CancellationToken cancellationToken) { var specification = new {Entities}ByOrganizationSpecification( request.OrganizationId, includeChildren: true);
var entities = await _{entity}Repository.GetAsync(specification, cancellationToken);
return entities.Select(e => new {Entity}Response(e)).ToList();
}
Related Skills
-
repository-pattern
-
Repository with specification support
-
cqrs-query-generator
-
Query handlers using specifications
-
domain-entity-generator
-
Entities queried by specifications