dotnet-specification-pattern について
.NET 開発者向けの Specification Pattern スキルです。再利用・合成可能なクエリ条件のテンプレートと、IQueryable やリポジトリクエリへの適用例を提供します。
主な機能
- 01Specification インターフェースと基底クラスのテンプレート
- 02合成可能なクエリ条件
- 03SpecificationEvaluator の例
- 04リポジトリ連携の例
活用シーン
- 01ドメインのクエリ条件の再利用
- 02複数のフィルターの結合
- 03IQueryable への仕様適用
- 04リポジトリクエリでの仕様利用
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ ronnythedev/ dotnet-clean-architecture-skills --skill dotnet-specification-patternインストール後、Claude Code で/dotnet-specification-patternを入力して読み込まれたか確認します。
詳細
Specificationパターンジェネレーター
概要
Specificationパターンはクエリロジックをカプセル化します。
- 再利用可能な条件 - 一度定義すれば、どこでも使用できます
- 合成可能 - And/Orで組み合わせられます
- テスト可能 - クエリロジックを個別にテストできます
- 型安全 - コンパイル時にチェックされます
クイックリファレンス
| コンポーネント | 目的 |
|---|---|
ISpecification<T> | 仕様の基本インターフェイス |
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);
}
}
テンプレート:SpecificationEvaluator
// 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- Specificationをサポートするリポジトリdotnet-cqrs-query-generator- Specificationを使用するクエリハンドラーdotnet-domain-entity-generator- Specificationでクエリされるエンティティ
コメント
Markdown 対応