- Skill ID
- codewithmukesh/dotnet-claude-kit/vertical-slice
- Publisher
- codewithmukesh
- Repository
- dotnet-claude-kit
- Installs
- 0
- Files
- 1
- Synced
- Sep 23, 2026
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
codewithmukesh/dotnet-claude-kit/vertical-sliceInstalls these files- SKILL.md
What this skill tells the agent
Vertical Slice Architecture (VSA)
Core Principles
- 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.
- Minimize cross-feature coupling — Features should not reference each other directly. Shared concerns go in a
Common/orShared/directory. - 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.
- 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.csPattern 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.csModules communicate via:
- Integration events (preferred) — async, decoupled via Wolverine or MassTransit
- Shared contracts — a
MyApp.Contractsproject 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
More from codewithmukesh
Use this skill in a project
Open any RiverX project, search the Skills panel for codewithmukesh/dotnet-claude-kit/vertical-slice, and install it in one click.
