mcpField
D
dotnet-specification-pattern

dotnet-specification-pattern

github.com/ronnythedev开发7760 次安装15 周前更新
代码仓库

关于 dotnet-specification-pattern

面向 .NET 开发者的 Specification Pattern 技能,提供可复用、可组合的查询条件模板,以及将条件应用到 IQueryable 和仓储查询的示例。

主要功能

  1. 01规格接口与基类模板
  2. 02可组合的查询条件
  3. 03SpecificationEvaluator 示例
  4. 04仓储集成示例

适用场景

  1. 01复用领域查询条件
  2. 02组合多个筛选条件
  3. 03在 IQueryable 上应用规格
  4. 04将规格接入仓储查询

安装

复制下面的命令,在终端中运行。

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

安装后在 Claude Code 中输入/dotnet-specification-pattern确认已加载。

详细介绍

Specification Pattern Generator

概述

Specification 模式封装查询逻辑:

  • 可复用的条件 - 定义一次,随处使用
  • 可组合 - 使用 And/Or 组合
  • 可测试 - 独立测试查询逻辑
  • 类型安全 - 编译时检查

快速参考

组件用途
ISpecification<T>基础 Specification 接口
BaseSpecification<T>抽象实现
SpecificationEvaluator将规格应用到 IQueryable

模板:Specification 接口

// 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; }
}

模板:基础 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;
    }
}

模板:具体 Specification

// 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);
    }
}

模板: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;
    }
}

模板:Repository 集成

// 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);
}

在 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();
}

相关技能

  • dotnet-repository-pattern - 支持 Specification 的 Repository
  • dotnet-cqrs-query-generator - 使用 Specification 的查询处理器
  • dotnet-domain-entity-generator - 由 Specification 查询的实体

评论

支持 Markdown