ワンクリックで
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 designing gRPC services, proto files, or adding gRPC-Web or JSON transcoding.
Use when writing async code, propagating CancellationTokens, or fixing async/await pitfalls.
Use when applying or enforcing C# coding style — namespaces, sealed classes, var usage, XML docs.
Use when applying modern C# idioms — records, pattern matching, primary constructors, collection expressions.
Use when registering services, choosing lifetimes, or implementing DI patterns like decorator or keyed services.
Use when selecting or implementing design patterns in C# — factory, builder, strategy, decorator, or mediator.
| 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 |