Saltar al contenido
Marketplace

vertical-slice

de codewithmukesh

Vertical Slice Architecture (VSA) for .NET applications — one of several supported architectures in dotnet-claude-kit. Covers feature folders, endpoint grouping, and handler patterns for Mediator, Wolverine, and raw handler classes. Load this skill when the architecture-advisor recommends VSA, when working in an existing VSA codebase, when adding features to a feature-folder project, or when discussing vertical slice patterns, feature folders, or handler patterns.

Resumen

SKILL.mdLeída en 2330089

Vertical Slice Architecture (VSA)

Core Principles

  1. Organize by feature, not by layer — Each feature is a self-contained vertical slice containing its endpoint, handler, request/response types, and validation. No more jumping between Controllers/, Services/, Repositories/ folders.
  2. Minimize cross-feature coupling — Features should not reference each other directly. Shared concerns go in a Common/ or Shared/ directory.
  3. One file per feature is fine — A simple CRUD endpoint doesn't need 5 files spread across layers. Start with everything in one file, extract only when complexity demands it.
  4. The handler is the unit of work — Each handler does one thing. No god-services with 20 methods.

Patterns

Feature Folder Structure
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
Pattern A: Mediator Handlers (Recommended Default)

Source-generated mediator — MIT licensed, no reflection, Native AOT compatible. Uses IRequest<T> / IRequestHandler<TRequest, TResponse> with pipeline behaviors. Near-identical API to MediatR but faster and free. Package: 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>>();
    }
}
Pattern B: Wolverine Handlers

Convention-based — no interfaces to implement. Wolverine discovers handlers by method signature.

// 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));
    }
}
Pattern C: Raw Handler Classes (No Library)

Direct handler classes with no external dependency. Good for small projects or teams that want full control.

// 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();
});
Adding Module Boundaries (Optional)

For larger applications that grow beyond a single project, introduce module boundaries. Each module is a separate class library with its own features and 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

Modules communicate via:

  • Integration events (preferred) — async, decoupled via Wolverine or MassTransit
  • Shared contracts — a MyApp.Contracts project with DTOs/interfaces (use sparingly)
Shared Concerns

Cross-cutting concerns live outside feature folders:

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

Anti-patterns

Don't Create Layered Abstractions Within a Slice
// 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
Don't Cross-reference Features 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);
Don't Put Everything in One God Feature File
// 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

Decision Guide

ScenarioRecommendation
New project (default)Pattern A — Mediator (source-generated, MIT, fast)
Need mediator + messaging in one libPattern B — Wolverine (also handles events/queues)
Want full control, no dependenciesPattern C — Raw handler classes
Existing MediatR codebase with licenseKeep MediatR if licensed; otherwise migrate to Mediator (near-identical API)
Monolith growing complexAdd module boundaries, keep VSA within each module
Simple CRUD featureSingle file: request + handler + endpoint
Complex feature (saga, events)Multiple files in feature folder, still colocated
Sharing logic between featuresExtract to Common/ — not to another feature

Archivos 1 archivo

Instálala a mano

Deska ejecuta esto por ti, para cada agente que elijas. Para ejecutarlo tú en una terminal:

npx skills add codewithmukesh/dotnet-claude-kit --skill vertical-slice

Más de codewithmukesh

  • clean-architecturecodewithmukeshClean Architecture for .NET applications. Covers the 4-project layout (Domain, Application, Infrastructure, Api), dependency inversion, use case handlers, domain entities with behavior, and infrastructure as a plugin. Load this skill when building a project with Clean Architecture, discussing layered architecture, dependency inversion, use cases, or when the architecture-advisor recommends Clean Architecture.2,200 instalaciones en skills.sh0
  • architecture-advisorcodewithmukeshArchitecture selection advisor for .NET applications. Asks structured questions about domain complexity, team size, system lifetime, compliance, and integration needs, then recommends the best-fit architecture: Vertical Slice, Clean Architecture, DDD + Clean Architecture, or Modular Monolith. Load this skill when the user asks "which architecture", "choose architecture", "set up project", "new project", "architecture decision", "restructure", or "how should I organize". Always load BEFORE any architecture-specific skill.2,100 instalaciones en skills.sh0
  • authenticationcodewithmukeshAuthentication and authorization for ASP.NET Core. Covers JWT bearer tokens, OpenID Connect, ASP.NET Identity, authorization policies, role and claim-based authorization, and API key authentication. Load this skill when implementing login, protecting endpoints, designing authorization rules, or when the user mentions "auth", "JWT", "bearer token", "OIDC", "OpenID Connect", "Identity", "claims", "roles", "authorize", "RequireAuthorization", "API key", or "cookie auth".2,100 instalaciones en skills.sh0
  • ci-cdcodewithmukeshCI/CD pipelines for .NET applications. Covers GitHub Actions and Azure DevOps YAML pipelines with build, test, publish, and deploy stages. Load this skill when setting up continuous integration, automated testing, deployment workflows, or when the user mentions "CI/CD", "pipeline", "GitHub Actions", "Azure DevOps", "workflow", "deploy", "build pipeline", "publish", "NuGet push", "release", or "continuous integration".2,100 instalaciones en skills.sh0
  • configurationcodewithmukeshConfiguration patterns for .NET 10 applications. Covers the Options pattern, IOptionsSnapshot vs IOptions, secrets management, and environment-based configuration. Load this skill when setting up application configuration, managing secrets, binding configuration sections, or when the user mentions "configuration", "appsettings", "Options pattern", "IOptions", "IOptionsSnapshot", "secrets", "user secrets", "environment variables", "connection string", or "config binding".2,100 instalaciones en skills.sh0
Todas las skills de codewithmukesh