skills/cqrs/request-response/SKILL.md
Use when designing CQRS request/response contracts with FluentValidation and Result types.
npx skillsauth add faysilalshareef/dotnet-ai-kit request-responseInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
public sealed record CreateOrderCommand(
string CustomerName,
string ShippingEmail,
List<CreateOrderCommand.LineItem> Items)
: IRequest<Result<CreateOrderCommand.Response>>
{
public sealed record LineItem(
Guid ProductId,
int Quantity,
decimal UnitPrice);
public sealed record Response(
Guid OrderId,
decimal Total,
DateTimeOffset CreatedAt);
}
public sealed class CreateOrderCommandValidator
: AbstractValidator<CreateOrderCommand>
{
public CreateOrderCommandValidator()
{
RuleFor(x => x.CustomerName)
.NotEmpty()
.MaximumLength(200)
.WithMessage("Customer name is required (max 200 chars)");
RuleFor(x => x.ShippingEmail)
.NotEmpty()
.EmailAddress()
.WithMessage("Valid shipping email is required");
RuleFor(x => x.Items)
.NotEmpty()
.WithMessage("Order must contain at least one item");
RuleForEach(x => x.Items).ChildRules(item =>
{
item.RuleFor(x => x.ProductId)
.NotEmpty();
item.RuleFor(x => x.Quantity)
.GreaterThan(0)
.LessThanOrEqualTo(1000);
item.RuleFor(x => x.UnitPrice)
.GreaterThan(0);
});
}
}
public sealed class CreateOrderCommandValidator
: AbstractValidator<CreateOrderCommand>
{
public CreateOrderCommandValidator(AppDbContext db)
{
RuleFor(x => x.CustomerName)
.NotEmpty()
.MustAsync(async (name, ct) =>
await db.Customers.AnyAsync(
c => c.Name == name, ct))
.WithMessage("Customer not found");
RuleForEach(x => x.Items).ChildRules(item =>
{
item.RuleFor(x => x.ProductId)
.MustAsync(async (id, ct) =>
await db.Products.AnyAsync(
p => p.Id == id && p.IsActive, ct))
.WithMessage("Product not found or inactive");
});
}
}
public sealed record ListOrdersQuery(
string? CustomerName = null,
OrderStatus? Status = null,
DateTime? FromDate = null,
DateTime? ToDate = null,
string? SortBy = "CreatedAt",
bool SortDescending = true,
int Page = 1,
int PageSize = 20) : IRequest<PagedList<OrderSummaryResponse>>;
// Simple response
public sealed record OrderResponse(
Guid Id,
string CustomerName,
decimal Total,
string Status,
DateTimeOffset CreatedAt,
List<OrderItemResponse> Items);
public sealed record OrderItemResponse(
Guid ProductId,
string ProductName,
int Quantity,
decimal UnitPrice,
decimal LineTotal);
// Paged response
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;
}
// Summary response (for list views)
public sealed record OrderSummaryResponse(
Guid Id,
string CustomerName,
decimal Total,
string Status,
DateTimeOffset CreatedAt);
// Result type
public sealed class Result<T>
{
private Result(T value) { Value = value; IsSuccess = true; }
private Result(Error error) { Error = error; IsSuccess = false; }
public bool IsSuccess { get; }
public T Value { get; } = default!;
public Error Error { get; } = default!;
public static Result<T> Success(T value) => new(value);
public static Result<T> Failure(Error error) => new(error);
public TResult Match<TResult>(
Func<T, TResult> onSuccess,
Func<Error, TResult> onFailure)
=> IsSuccess ? onSuccess(Value) : onFailure(Error);
}
// Handler returning Result
internal sealed class GetOrderHandler(IOrderRepository repo)
: IRequestHandler<GetOrderQuery, Result<OrderResponse>>
{
public async Task<Result<OrderResponse>> Handle(
GetOrderQuery request, CancellationToken ct)
{
var order = await repo.FindAsync(request.OrderId, ct);
if (order is null)
return Result<OrderResponse>.Failure(
Error.NotFound("Order.NotFound",
$"Order {request.OrderId} not found"));
return Result<OrderResponse>.Success(order.ToResponse());
}
}
// Endpoint consuming Result
app.MapGet("/orders/{id}", async (Guid id, ISender sender) =>
{
var result = await sender.Send(new GetOrderQuery(id));
return result.Match(
value => Results.Ok(value),
error => error.Type switch
{
ErrorType.NotFound => Results.NotFound(
error.ToProblemDetails()),
_ => Results.BadRequest(error.ToProblemDetails())
});
});
// Register all validators from assembly
builder.Services.AddValidatorsFromAssembly(
typeof(CreateOrderCommandValidator).Assembly);
// FluentValidation runs automatically via ValidationBehavior
record or readonly)null instead of Result.Failure for expected failuresAbstractValidator< implementationsResult<T> or ErrorOr<T> typesIRequest<>RuleFor, RuleForEach validation rulesFluentValidation package referencedotnet add package FluentValidation.DependencyInjectionExtensionsAddValidatorsFromAssemblydata-ai
Use when about to claim work is complete, fixed, passing, or ready — before committing, creating PRs, or moving to the next task. Requires running verification commands and confirming output before making any success claims.
development
Use when encountering any bug, test failure, build error, or unexpected behavior — before proposing fixes or making changes.
development
Use when checkpointing, wrapping up, or handing off an AI-assisted development session.
development
Use when following the Specification-Driven Development lifecycle from plan through ship.