cli/dist/skills/20-dotnet-specification-pattern/SKILL.md
Implements the Specification pattern for encapsulating query logic. Enables composable, reusable, and testable query criteria with support for includes, ordering, and pagination.
npx skillsauth add ronnythedev/dotnet-clean-architecture-skills dotnet-specification-patternInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
The Specification pattern encapsulates query logic:
| Component | Purpose |
|-----------|---------|
| ISpecification<T> | Base specification interface |
| BaseSpecification<T> | Abstract implementation |
| SpecificationEvaluator | Applies spec to IQueryable |
// 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; }
}
// 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;
}
}
// 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);
}
}
// 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;
}
}
// 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);
}
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();
}
dotnet-repository-pattern - Repository with specification supportdotnet-cqrs-query-generator - Query handlers using specificationsdotnet-domain-entity-generator - Entities queried by specificationstools
Implements the Options pattern for strongly-typed configuration in .NET. Covers IOptions<T>, IOptionsSnapshot<T>, and IOptionsMonitor<T> with validation and reload support.
tools
SQL Server database design best practices, naming conventions, indexing strategies, and performance optimization for .NET applications using Microsoft.Data.SqlClient and EF Core.
data-ai
PostgreSQL database design best practices, naming conventions, indexing strategies, and performance optimization for .NET applications using Npgsql and EF Core.
development
Implements ASP.NET Core rate limiting middleware for API protection. Covers fixed window, sliding window, token bucket, and concurrency limiters with custom policies.