mcpField
V
vertical-slice

vertical-slice

github.com/codewithmukesh開発7913.0k 回インストール12 週間前に更新
リポジトリ

vertical-slice について

機能ごとにコードをまとめ、スライス間の結合を抑える.NET開発者向けのVertical Slice Architectureスキルです。ハンドラーの選択肢、モジュール境界、共通処理の扱いを説明します。

主な機能

  1. 01機能単位でコードを構成
  2. 02単純な機能を1ファイルに集約
  3. 033種類のハンドラーパターンを比較
  4. 04必要に応じてモジュール境界を追加
  5. 05共通処理を一元化

活用シーン

  1. 01CRUD APIの機能構造を設計
  2. 02拡大するアプリをモジュールに分割
  3. 03Mediator、Wolverine、素のハンドラーから選択

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/codewithmukesh/dotnet-claude-kit --skill vertical-slice

インストール後、Claude Code で/vertical-sliceを入力して読み込まれたか確認します。

詳細

Vertical Slice Architecture(VSA)

コア原則

  1. レイヤーではなく機能ごとに整理する — 各機能は、エンドポイント、ハンドラー、リクエスト/レスポンス型、バリデーションを含む独立した垂直スライスです。Controllers/、Services/、Repositories/ の各フォルダー間を行き来する必要はありません。
  2. 機能間の結合を最小限に抑える — 機能同士が直接参照し合わないようにします。共有する関心事は Common/ または Shared/ ディレクトリに配置します。
  3. 機能ごとに1ファイルでも問題ありません — シンプルな CRUD エンドポイントを、レイヤーに分散した5つのファイルに分ける必要はありません。まずはすべてを1つのファイルにまとめ、複雑さが増した場合にのみ切り出します。
  4. ハンドラーを作業単位にする — 各ハンドラーは1つの処理を行います。20個のメソッドを持つ巨大なサービスは作りません。

パターン

機能フォルダーの構成
src/
  MyApp.Api/
    Features/
      Orders/
        CreateOrder.cs          # Request, Handler, Response, Endpoint — all in one file
        GetOrder.cs
        ListOrders.cs
        CancelOrder.cs
        Shared/
          OrderMapper.cs        # Shared within the Orders feature only
      Products/
        CreateProduct.cs
        GetProduct.cs
    Common/
      Behaviors/
        ValidationBehavior.cs   # Cross-cutting Mediator pipeline behavior
      Persistence/
        AppDbContext.cs
      Extensions/
        ServiceCollectionExtensions.cs
    Program.cs
パターンA:メディエーターハンドラー(デフォルトとして推奨)

ソース生成型メディエーターです。MIT ライセンスで、リフレクションを使用せず、Native AOT に対応しています。パイプライン動作とともに IRequest<T> / IRequestHandler<TRequest, TResponse> を使用します。MediatR とほぼ同じ API ですが、より高速で無料です。パッケージ:Mediator.Abstractions + Mediator.SourceGenerator。

// Features/Orders/CreateOrder.cs

public static class CreateOrder
{
    public record Command(string CustomerId, List<OrderItemDto> Items) : IRequest<Result<OrderResponse>>;

    public record OrderItemDto(string ProductId, int Quantity);

    public record OrderResponse(Guid Id, decimal Total, DateTime CreatedAt);

    public class Validator : AbstractValidator<Command>
    {
        public Validator()
        {
            RuleFor(x => x.CustomerId).NotEmpty();
            RuleFor(x => x.Items).NotEmpty();
            RuleForEach(x => x.Items).ChildRules(item =>
            {
                item.RuleFor(x => x.ProductId).NotEmpty();
                item.RuleFor(x => x.Quantity).GreaterThan(0);
            });
        }
    }

    internal sealed class Handler(AppDbContext db, TimeProvider clock) : IRequestHandler<Command, Result<OrderResponse>>
    {
        public async ValueTask<Result<OrderResponse>> Handle(Command request, CancellationToken ct)
        {
            var order = Order.Create(request.CustomerId, request.Items, clock.GetUtcNow());
            db.Orders.Add(order);
            await db.SaveChangesAsync(ct);

            return Result.Success(new OrderResponse(order.Id, order.Total, order.CreatedAt));
        }
    }
}

// Registration in Program.cs or module DI
builder.Services.AddMediator();

// Features/Orders/OrderEndpoints.cs — auto-discovered via IEndpointGroup
public sealed class OrderEndpoints : IEndpointGroup
{
    public void Map(IEndpointRouteBuilder app)
    {
        var group = app.MapGroup("/api/orders").WithTags("Orders");

        group.MapPost("/", async (CreateOrder.Command command, ISender sender, CancellationToken ct) =>
        {
            var result = await sender.Send(command, ct);
            return result.IsSuccess
                ? TypedResults.Created($"/api/orders/{result.Value.Id}", result.Value)
                : result.ToProblemDetails();
        })
        .WithName("CreateOrder").Produces<CreateOrder.OrderResponse>(201)
        .ProducesValidationProblem()
        .AddEndpointFilter<ValidationFilter<CreateOrder.Command>>();
    }
}
パターンB:Wolverine ハンドラー

規約ベースで、実装するインターフェイスはありません。Wolverine はメソッドのシグネチャからハンドラーを検出します。

// Features/Orders/CreateOrder.cs

public static class CreateOrder
{
    public record Command(string CustomerId, List<OrderItemDto> Items);

    public record OrderItemDto(string ProductId, int Quantity);

    public record OrderResponse(Guid Id, decimal Total, DateTime CreatedAt);

    // Wolverine discovers this by convention (static Handle method)
    public static async Task<Result<OrderResponse>> Handle(
        Command command,
        AppDbContext db,
        TimeProvider clock,
        CancellationToken ct)
    {
        var order = Order.Create(command.CustomerId, command.Items, clock.GetUtcNow());
        db.Orders.Add(order);
        await db.SaveChangesAsync(ct);
        return Result.Success(new OrderResponse(order.Id, order.Total, order.CreatedAt));
    }
}
パターンC:ハンドラークラスを直接使用する(ライブラリなし)

外部依存関係を使わず、ハンドラークラスを直接使用します。小規模なプロジェクトや、完全な制御を求めるチームに適しています。

// Features/Orders/CreateOrder.cs

public static class CreateOrder
{
    public record Command(string CustomerId, List<OrderItemDto> Items);

    public record OrderItemDto(string ProductId, int Quantity);

    public record OrderResponse(Guid Id, decimal Total, DateTime CreatedAt);

    internal class Handler(AppDbContext db, TimeProvider clock)
    {
        public async Task<Result<OrderResponse>> ExecuteAsync(Command command, CancellationToken ct)
        {
            var order = Order.Create(command.CustomerId, command.Items, clock.GetUtcNow());
            db.Orders.Add(order);
            await db.SaveChangesAsync(ct);

            return Result.Success(new OrderResponse(order.Id, order.Total, order.CreatedAt));
        }
    }
}

// Endpoint wiring — Result maps to HTTP response
group.MapPost("/", async (CreateOrder.Command command, CreateOrder.Handler handler, CancellationToken ct) =>
{
    var result = await handler.ExecuteAsync(command, ct);
    return result.IsSuccess
        ? TypedResults.Created($"/api/orders/{result.Value.Id}", result.Value)
        : result.ToProblemDetails();
});
モジュール境界の追加(任意)

単一のプロジェクトを超えて成長する大規模なアプリケーションでは、モジュール境界を導入します。各モジュールは独自の機能と DbContext を持つ、独立したクラスライブラリです。

src/
  MyApp.Api/                      # Host — wires modules together
    Program.cs
    Modules/
      ModuleExtensions.cs         # app.MapOrderModule(), app.MapCatalogModule()
  MyApp.Orders/                   # Module — own features, own DbContext
    Features/
      CreateOrder.cs
    Persistence/
      OrdersDbContext.cs
    OrdersModule.cs               # IServiceCollection + IEndpointRouteBuilder extensions
  MyApp.Catalog/                  # Module
    Features/
      CreateProduct.cs
    Persistence/
      CatalogDbContext.cs
    CatalogModule.cs

モジュール間の通信方法:

  • 統合イベント(推奨) — 非同期で、Wolverine または MassTransit を介して疎結合にします
  • 共有コントラクト — DTO/インターフェイスを含む MyApp.Contracts プロジェクト(使用は必要最小限にします)
共有する関心事

横断的な関心事は、機能フォルダーの外に配置します。

// Common/Behaviors/ValidationBehavior.cs (Mediator pipeline)
public sealed class ValidationBehavior<TRequest, TResponse>(IEnumerable<IValidator<TRequest>> validators)
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IMessage
{
    public async ValueTask<TResponse> Handle(
        TRequest request,
        MessageHandlerDelegate<TRequest, TResponse> next,
        CancellationToken ct)
    {
        var context = new ValidationContext<TRequest>(request);
        var failures = validators
            .Select(v => v.Validate(context))
            .SelectMany(r => r.Errors)
            .Where(f => f is not null)
            .ToList();

        if (failures.Count > 0)
            throw new ValidationException(failures);

        return await next(request, ct);
    }
}

アンチパターン

スライス内にレイヤー化された抽象化を作らない
// BAD — a feature folder with its own service layer and repository
Features/
  Orders/
    CreateOrder.cs
    IOrderService.cs         # unnecessary abstraction
    OrderService.cs          # unnecessary abstraction
    IOrderRepository.cs      # unnecessary abstraction
    OrderRepository.cs       # unnecessary abstraction

// GOOD — handler talks directly to DbContext
Features/
  Orders/
    CreateOrder.cs           # handler uses AppDbContext directly
機能同士を直接相互参照させない
// BAD — CreateOrder directly calls GetProduct handler
var product = await _getProductHandler.Handle(new GetProduct.Query(productId));

// GOOD — query the database directly or use a shared read model
var product = await db.Products.FindAsync(productId, ct);
すべてを1つの巨大な機能ファイルに詰め込まない
// BAD — 500-line file with CRUD + business logic + mapping
public static class Orders
{
    // Create, Read, Update, Delete, Cancel, Refund, Export...
}

// GOOD — one file per operation
Features/Orders/CreateOrder.cs
Features/Orders/GetOrder.cs
Features/Orders/CancelOrder.cs

判断ガイド

シナリオ推奨事項
新規プロジェクト(デフォルト)パターンA — メディエーター(ソース生成型、MIT、軽量)
1つのライブラリでメディエーターとメッセージングが必要パターンB — Wolverine(イベント/キューにも対応)
完全な制御を求め、依存関係を避けたいパターンC — ハンドラークラスを直接使用
ライセンスがある既存の MediatR コードベースライセンスがあれば MediatR を継続して使用し、なければ Mediator に移行します(API はほぼ同じです)
モノリスが複雑化しているモジュール境界を追加し、各モジュール内で VSA を維持します
シンプルな CRUD 機能1ファイル:リクエスト + ハンドラー + エンドポイント
複雑な機能(サガ、イベント)機能フォルダー内に複数のファイルを配置し、引き続き同じ場所にまとめます
機能間でロジックを共有するCommon/ に切り出します。別の機能には配置しません

コメント