mcpField
D
dotnet-specification-pattern

dotnet-specification-pattern

github.com/ronnythedevDevelopment7760 installsUpdated 15w ago
Repository

About dotnet-specification-pattern

A Specification Pattern skill for .NET developers, providing reusable and composable query-criteria templates, with examples for applying specifications to IQueryable and repository queries.

Key features

  1. 01Specification interface and base templates
  2. 02Composable query criteria
  3. 03SpecificationEvaluator example
  4. 04Repository integration example

Use cases

  1. 01Reuse domain query criteria
  2. 02Combine multiple filters
  3. 03Apply specifications to IQueryable
  4. 04Use specifications in repository queries

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/ronnythedev/dotnet-clean-architecture-skills --skill dotnet-specification-pattern

After installing, type/dotnet-specification-patternin Claude Code to confirm it loaded.

Details

From the project README

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

ComponentPurpose
ISpecification<T>Base specification interface
BaseSpecification<T>Abstract implementation
SpecificationEvaluatorApplies 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

  • dotnet-repository-pattern - Repository with specification support
  • dotnet-cqrs-query-generator - Query handlers using specifications
  • dotnet-domain-entity-generator - Entities queried by specifications

Comments

Markdown supported