원클릭으로
vertical-slice
Use when implementing Vertical Slice Architecture with feature folders and co-located code.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Use when implementing Vertical Slice Architecture with feature folders and co-located code.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
Use when adding caching to .NET APIs or optimizing response times with distributed cache, output cache, or ETags.
Use when configuring API response formats, custom formatters, or Accept header handling.
Use when building controller-based REST APIs with action results, model binding, or MediatR integration.
Use when creating RESTful API controllers with MediatR dispatch and ProblemDetails error responses.
Use when designing gRPC services, proto files, or adding gRPC-Web or JSON transcoding.
Use when building minimal API endpoints with route groups, filters, or TypedResults.
SOC 직업 분류 기준
| name | vertical-slice |
| description | Use when implementing Vertical Slice Architecture with feature folders and co-located code. |
| metadata | {"category":"architecture","agent":"dotnet-architect","when-to-use":"When organizing code by feature using Vertical Slice Architecture"} |
Common/ or Shared/ — extract only when duplication is provensrc/{Company}.{Domain}.WebApi/
Features/
Orders/
CreateOrder.cs # Command + Handler + Validator
GetOrder.cs # Query + Handler
ListOrders.cs # Query + Handler + Filter
OrderEndpoints.cs # Endpoint group registration
OrderResponse.cs # Shared response DTO (if reused)
Products/
CreateProduct.cs
GetProduct.cs
ProductEndpoints.cs
Common/
PagedList.cs
ValidationBehavior.cs
Program.cs
// Features/Orders/CreateOrder.cs
namespace {Company}.{Domain}.Features.Orders;
public static class CreateOrder
{
public sealed record Command(
string CustomerName,
List<LineItem> Items) : IRequest<Result>;
public sealed record LineItem(Guid ProductId, int Quantity);
public sealed record Result(Guid OrderId);
public sealed class Validator : AbstractValidator<Command>
{
public Validator()
{
RuleFor(x => x.CustomerName).NotEmpty().MaximumLength(200);
RuleFor(x => x.Items).NotEmpty();
RuleForEach(x => x.Items).ChildRules(item =>
{
item.RuleFor(x => x.Quantity).GreaterThan(0);
});
}
}
internal sealed class Handler(AppDbContext db)
: IRequestHandler<Command, Result>
{
public async Task<Result> Handle(
Command request, CancellationToken ct)
{
var order = new Order
{
CustomerName = request.CustomerName,
Items = request.Items.Select(i => new OrderItem
{
ProductId = i.ProductId,
Quantity = i.Quantity
}).ToList()
};
db.Orders.Add(order);
await db.SaveChangesAsync(ct);
return new Result(order.Id);
}
}
}
// Features/Orders/ListOrders.cs
namespace {Company}.{Domain}.Features.Orders;
public static class ListOrders
{
public sealed record Query(
string? CustomerName,
int Page = 1,
int PageSize = 20) : IRequest<PagedList<OrderSummary>>;
public sealed record OrderSummary(
Guid Id, string CustomerName, decimal Total, DateTime CreatedAt);
internal sealed class Handler(AppDbContext db)
: IRequestHandler<Query, PagedList<OrderSummary>>
{
public async Task<PagedList<OrderSummary>> Handle(
Query request, CancellationToken ct)
{
var query = db.Orders.AsNoTracking();
if (!string.IsNullOrEmpty(request.CustomerName))
query = query.Where(o =>
o.CustomerName.Contains(request.CustomerName));
var totalCount = await query.CountAsync(ct);
var items = await query
.OrderByDescending(o => o.CreatedAt)
.Skip((request.Page - 1) * request.PageSize)
.Take(request.PageSize)
.Select(o => new OrderSummary(
o.Id, o.CustomerName, o.Total, o.CreatedAt))
.ToListAsync(ct);
return new PagedList<OrderSummary>(
items, totalCount, request.Page, request.PageSize);
}
}
}
// Features/Orders/OrderEndpoints.cs
namespace {Company}.{Domain}.Features.Orders;
public sealed class OrderEndpoints : IEndpointGroup
{
public void MapEndpoints(IEndpointRouteBuilder app)
{
var group = app.MapGroup("/orders")
.WithTags("Orders")
.RequireAuthorization();
group.MapPost("/", async (
CreateOrder.Command cmd, ISender sender) =>
{
var result = await sender.Send(cmd);
return TypedResults.Created(
$"/orders/{result.OrderId}", result);
}).WithSummary("Create a new order");
group.MapGet("/", async (
[AsParameters] ListOrders.Query query, ISender sender) =>
{
var result = await sender.Send(query);
return TypedResults.Ok(result);
}).WithSummary("List orders with filtering");
group.MapGet("/{id:guid}", async (
Guid id, ISender sender) =>
{
var result = await sender.Send(new GetOrder.Query(id));
return result is not null
? TypedResults.Ok(result)
: TypedResults.NotFound();
}).WithSummary("Get order by ID");
}
}
// Extract ONLY when the same logic appears in 3+ slices
// Common/Extensions/QueryableExtensions.cs
public static class QueryableExtensions
{
public static async Task<PagedList<T>> ToPagedListAsync<T>(
this IQueryable<T> query, int page, int pageSize,
CancellationToken ct)
{
var count = await query.CountAsync(ct);
var items = await query
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(ct);
return new PagedList<T>(items, count, page, pageSize);
}
}
internal)Features/ folder structure with operation-named filesCommand, Query, HandlerDbContext directlyIEndpointGroup or similar endpoint grouping patternsFeatures/ folder at the project rootCommon/ for truly cross-cutting utilities only| Question | VSA Answer |
|---|---|
| Where does the handler go? | Same file as the command/query |
| Do I need a repository? | No — use DbContext directly |
| When do I extract shared code? | After 3+ duplications |
| Can different slices use different data access? | Yes — EF, Dapper, raw SQL per slice |
| How do I handle cross-cutting? | MediatR pipeline behaviors |
| What about domain logic? | Keep in entity methods, call from handler |