원클릭으로
cqrs-basics
Use when implementing CQRS with separate read/write models and MediatR pipeline behaviors.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Use when implementing CQRS with separate read/write models and MediatR pipeline behaviors.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
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.
| name | cqrs-basics |
| description | Use when implementing CQRS with separate read/write models and MediatR pipeline behaviors. |
| metadata | {"category":"architecture","agent":"dotnet-architect","when-to-use":"When implementing CQRS patterns with separate read and write models"} |
// Commands represent intent to change state
// Naming: {Verb}{Noun}Command
public sealed record CreateOrderCommand(
string CustomerName,
List<CreateOrderCommand.OrderItemDto> Items) : IRequest<Result<Guid>>
{
public sealed record OrderItemDto(Guid ProductId, int Quantity);
}
public sealed record UpdateOrderStatusCommand(
Guid OrderId,
OrderStatus NewStatus) : IRequest<Result>;
public sealed record DeleteOrderCommand(Guid OrderId) : IRequest<Result>;
// Queries represent requests for data — never modify state
// Naming: Get{Noun}Query or List{Noun}Query
public sealed record GetOrderQuery(Guid OrderId)
: IRequest<OrderResponse?>;
public sealed record ListOrdersQuery(
string? CustomerName,
OrderStatus? Status,
int Page = 1,
int PageSize = 20) : IRequest<PagedList<OrderSummaryResponse>>;
internal sealed class CreateOrderCommandHandler(
IOrderRepository repository,
IUnitOfWork unitOfWork,
ILogger<CreateOrderCommandHandler> logger)
: IRequestHandler<CreateOrderCommand, Result<Guid>>
{
public async Task<Result<Guid>> Handle(
CreateOrderCommand request, CancellationToken ct)
{
var order = Order.Create(request.CustomerName);
foreach (var item in request.Items)
order.AddItem(item.ProductId, item.Quantity);
repository.Add(order);
await unitOfWork.SaveChangesAsync(ct);
logger.LogInformation("Order {OrderId} created", order.Id);
return Result<Guid>.Success(order.Id);
}
}
internal sealed class ListOrdersQueryHandler(AppDbContext db)
: IRequestHandler<ListOrdersQuery, PagedList<OrderSummaryResponse>>
{
public async Task<PagedList<OrderSummaryResponse>> Handle(
ListOrdersQuery request, CancellationToken ct)
{
var query = db.Orders.AsNoTracking();
if (!string.IsNullOrEmpty(request.CustomerName))
query = query.Where(o =>
o.CustomerName.Contains(request.CustomerName));
if (request.Status.HasValue)
query = query.Where(o => o.Status == request.Status.Value);
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 OrderSummaryResponse(
o.Id, o.CustomerName, o.Total, o.Status.ToString()))
.ToListAsync(ct);
return new PagedList<OrderSummaryResponse>(
items, totalCount, request.Page, request.PageSize);
}
}
public sealed record OrderResponse(
Guid Id,
string CustomerName,
decimal Total,
string Status,
DateTimeOffset CreatedAt,
List<OrderItemResponse> Items);
public sealed record OrderSummaryResponse(
Guid Id, string CustomerName, decimal Total, string Status);
public sealed record OrderItemResponse(
Guid ProductId, string ProductName, int Quantity, decimal UnitPrice);
public sealed record PagedList<T>(
List<T> Items,
int TotalCount,
int Page,
int PageSize)
{
public int TotalPages =>
(int)Math.Ceiling(TotalCount / (double)PageSize);
public bool HasNext => Page < TotalPages;
public bool HasPrevious => Page > 1;
}
// Program.cs
builder.Services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
// Pipeline behaviors (order matters: first = outermost)
cfg.AddBehavior(typeof(IPipelineBehavior<,>),
typeof(LoggingBehavior<,>));
cfg.AddBehavior(typeof(IPipelineBehavior<,>),
typeof(ValidationBehavior<,>));
});
builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly);
// Minimal API
app.MapPost("/orders", async (
CreateOrderCommand cmd, ISender sender, CancellationToken ct) =>
{
var result = await sender.Send(cmd, ct);
return result.Match(
id => Results.Created($"/orders/{id}", new { id }),
error => Results.BadRequest(error.ToProblemDetails()));
});
app.MapGet("/orders", async (
[AsParameters] ListOrdersQuery query,
ISender sender, CancellationToken ct) =>
{
return Results.Ok(await sender.Send(query, ct));
});
// Controller
[ApiController]
[Route("api/orders")]
public sealed class OrdersController(ISender sender) : ControllerBase
{
[HttpPost]
public async Task<IActionResult> Create(
CreateOrderCommand command, CancellationToken ct)
{
var result = await sender.Send(command, ct);
return result.Match<IActionResult>(
id => CreatedAtAction(nameof(Get), new { id }, null),
error => BadRequest(error.ToProblemDetails()));
}
[HttpGet("{id:guid}")]
public async Task<IActionResult> Get(Guid id, CancellationToken ct)
{
var result = await sender.Send(new GetOrderQuery(id), ct);
return result is not null ? Ok(result) : NotFound();
}
}
// Clean Architecture + CQRS
Application/
Orders/
Commands/
CreateOrder/
CreateOrderCommand.cs
CreateOrderCommandHandler.cs
CreateOrderCommandValidator.cs
Queries/
GetOrder/
GetOrderQuery.cs
GetOrderQueryHandler.cs
ListOrders/
ListOrdersQuery.cs
ListOrdersQueryHandler.cs
// VSA + CQRS
Features/
Orders/
CreateOrder.cs # Command + Handler + Validator in one file
GetOrder.cs # Query + Handler in one file
ListOrders.cs # Query + Handler in one file
IRequest< and IRequestHandler< implementationsCommand and Query suffixes in class namesMediatR package reference in .csprojCommands/ and Queries/ folder structureIPipelineBehavior< implementations{Verb}{Noun}Command / Get{Noun}QueryAsNoTracking() in all query handlers| Scenario | Use |
|---|---|
| Creating/updating data | Command |
| Reading/listing data | Query |
| Cross-cutting validation | Pipeline behavior |
| Cross-cutting logging | Pipeline behavior |
| Transaction wrapping | Pipeline behavior on commands only |