CQRS & Pipeline Behaviors
Vulthil.SharedKernel.Application implements the CQRS (Command Query Responsibility Segregation) pattern with a lightweight in-process sender and configurable pipeline behaviors.
Concepts
| Abstraction | Description |
|---|---|
ICommand / ICommand<TResponse> |
Write operation that changes state |
ITransactionalCommand / ITransactionalCommand<TResponse> |
Command executed inside a database transaction |
IQuery<TResponse> |
Read operation that returns data |
ICommandHandler<TCommand, TResponse> |
Handles a command |
IQueryHandler<TQuery, TResponse> |
Handles a query |
ISender |
Dispatches requests to their handlers |
IPipelineHandler<TRequest, TResponse> |
Cross-cutting middleware for the handler pipeline |
Registration
builder.Services.AddApplication(options =>
{
// Scan assemblies for handlers
options.RegisterHandlerAssemblies(typeof(Program).Assembly);
// Scan assemblies for FluentValidation validators
options.RegisterFluentValidationAssemblies(typeof(Program).Assembly);
// Add pipeline behaviors (order matters)
options.AddValidationPipelineBehavior();
options.AddRequestLoggingBehavior();
options.AddTransactionalPipelineBehavior();
});
Defining Commands and Handlers
public sealed record CreateUserCommand(string Email) : ICommand<Result<Guid>>;
public sealed class CreateUserCommandHandler(IUserRepository users, IUnitOfWork uow)
: ICommandHandler<CreateUserCommand, Result<Guid>>
{
public async Task<Result<Guid>> HandleAsync(
CreateUserCommand request, CancellationToken cancellationToken)
{
var user = User.Create(request.Email);
users.Add(user);
await uow.SaveChangesAsync(cancellationToken);
return user.Id.Value;
}
}
Defining Queries and Handlers
public sealed record GetUserQuery(Guid UserId) : IQuery<Result<UserDto>>;
public sealed class GetUserQueryHandler(AppDbContext db)
: IQueryHandler<GetUserQuery, Result<UserDto>>
{
public async Task<Result<UserDto>> HandleAsync(
GetUserQuery request, CancellationToken cancellationToken)
{
var user = await db.Users
.Where(u => u.Id == new UserId(request.UserId))
.Select(u => new UserDto(u.Id.Value, u.Email))
.FirstOrDefaultAsync(cancellationToken);
return user is not null
? Result.Success(user)
: Result.Failure<UserDto>(UserErrors.NotFound);
}
}
Pipeline Behaviors
Pipeline behaviors wrap every handler invocation, allowing you to add cross-cutting logic without modifying individual handlers.
Validation
ValidationPipelineBehavior runs all registered IValidator<TCommand> instances before the handler executes. When validation fails, it short-circuits and returns a Result containing a ValidationError:
public sealed class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
{
public CreateUserCommandValidator()
{
RuleFor(x => x.Email)
.NotEmpty()
.EmailAddress();
}
}
No additional wiring is needed – the validator is picked up automatically from the registered assemblies.
Request Logging
RequestLoggingPipelineBehavior logs the start and completion of every request, including execution time.
Transactional
TransactionalPipelineBehavior wraps ITransactionalCommand handlers inside a database transaction. If the handler succeeds, the transaction commits; if it throws, the transaction rolls back.
// Mark a command as transactional
public sealed record TransferFundsCommand(Guid FromAccount, Guid ToAccount, decimal Amount)
: ITransactionalCommand<Result>;
Dispatching Requests
Inject ISender into your API endpoints or controllers:
app.MapPost("/users", async (CreateUserCommand command, ISender sender) =>
{
var result = await sender.SendAsync(command);
return result.ToIResult();
});