ITLab.Template.DevPack.Cqrs
Defines CQRS contracts (commands, queries, notifications) and the base mediator handler abstraction. Built on top of Mediator (martinothamar) — the source-generated mediator. MediatR is no longer referenced.
Histórico: este namespace era
ITLab.Template.DevPack.Mediatoraté a versão 2.0.0. Renomeado paraCqrsna 2.0.1 para evitar colisão com o namespace raizMediator(martinothamar) e com a áreaMessaging/(broker integration). Veja ADR 002.
Componentes
| Tipo | Nome | Responsabilidade |
|---|---|---|
| Interface | ICommand | Marker for a command returning Result |
| Interface | ICommand<TResponse> | Marker for a command returning Result<TResponse> |
| Interface | ICommandHandler<TCommand> | Handler contract for ICommand |
| Interface | ICommandHandler<TCommand, TResponse> | Handler contract for ICommand<TResponse> |
| Interface | IQuery<TResponse> | Marker for a query returning Result<TResponse> |
| Interface | IQueryHandler<TQuery, TResponse> | Handler contract for IQuery<TResponse> |
| Interface | INotificationEvent | Marker for application events dispatched via Mediator.IPublisher |
| Interface | IMediatorHandler | Facade abstraction for publishing domain events |
| Class | MediatorHandler | Default IMediatorHandler implementation wrapping Mediator.IPublisher |
Como usar
// Use ICommand<TResponse>, NOT ICommand<Result<TResponse>>:
// Result<TResponse> wrapping is applied automatically by the handler signature.
public sealed record CreateOrderCommand(Guid CustomerId, decimal Total)
: ICommand<Guid>;
public sealed class CreateOrderCommandHandler : ICommandHandler<CreateOrderCommand, Guid>
{
public ValueTask<Result<Guid>> Handle(CreateOrderCommand cmd, CancellationToken ct)
{
// ...
return ValueTask.FromResult(Result.Success(orderId));
}
}
// Same convention for queries — IQuery<T>, never IQuery<Result<T>>.
public sealed record GetOrderByIdQuery(Guid OrderId) : IQuery<OrderDto>;
The contract pair above — auto-suficiente, verificado pelo smoke check (regression do ICommand<Result<T>>):
public sealed record OrderDto(Guid Id);
public sealed record CreateOrderCommand(Guid CustomerId, decimal Total) : ICommand<Guid>;
public sealed record GetOrderByIdQuery(Guid OrderId) : IQuery<OrderDto>;
Diferenças vs MediatR (até v1.x do DevPack)
ValueTask<T>em todos os handlers e behaviors. SubstituiTask<T>para evitar alocações em fluxos síncronos.- Handlers são descobertos por source generator no projeto consumidor. Substitui
services.AddMediatR(typeof(...))porservices.AddMediator(...)(no projeto consumidor). - Pipeline behaviors abertos requerem registro manual no consumidor (ex.:
services.AddSingleton(typeof(IPipelineBehavior<,>), typeof(ValidationPipelineBehavior<,>))). - Interface
Mediator.IPublishersubstituiMediatR.IMediatorpara o caminho de publicação de domain events.
Convenção de namespaces (sem colisão)
A biblioteca Mediator (martinothamar) declara seus tipos no namespace raiz Mediator. Nossos contratos vivem em ITLab.Template.DevPack.Cqrs. Os dois namespaces podem coexistir no mesmo arquivo sem ambiguidade — using Mediator; (a biblioteca) e using ITLab.Template.DevPack.Cqrs; (nossos markers) não compartilham nomes de tipos.
Convenção do template:
- Arquivos de handlers e command/query records →
using ITLab.Template.DevPack.Cqrs;(ganham nossos markersICommand<TResponse>,IQueryHandler<,>, etc.). - Arquivos de endpoints que injetam
IMediator→using Mediator;(apenas a biblioteca raiz). Não precisam dos nossos markers. - Arquivos que precisam de ambos podem importar livremente — não há colisão.
O que não fazer
- Do not place infrastructure concerns (DB, HTTP) directly in command/query records.
- Do not use
INotificationEventto replace synchronous return values — it is fire-and-observe only. - Do not return
Task<TResponse>from handlers — the contract isValueTask<TResponse>. - Do not declare
ICommand<Result<T>>/IQuery<Result<T>>—Result<>is applied automatically by the handler signature. UseICommand<T>/IQuery<T>. - Do not confuse this namespace with
Messaging/, which targets external broker integration (Service Bus dispositions). The two are independent.
Handler lifetime
Handlers do DevPack tipicamente consomem IAppDbContext (Scoped). O default do martinothamar/Mediator é registrar handlers como Singleton, o que viola container validation com Scoped deps:
Cannot consume scoped service 'IAppDbContext' from singleton 'XHandler'.
Configure Scoped explicitamente em AddMediator:
services.AddMediator(opt =>
{
opt.ServiceLifetime = ServiceLifetime.Scoped;
opt.Namespace = "MyApp.Application.Mediator";
});
Falha sintoma: dotnet ef migrations add ou primeiro run da API lança InvalidOperationException mencionando o handler concreto.
Domain events
IDomainEvent herda de Mediator.INotification — cada evento publicado via IPublisher.Publish(...) exige pelo menos um INotificationHandler<T> (sob pena de MSG0005 do source generator do Mediator). Para política de quando criar evento de domínio + placeholder pattern, ver areas/Domain/README.md § Domain events policy.
Extensão pelo produto
- Products register handlers via
services.AddMediator(...)from theMediatorpackage; the source generator scans the consumer assembly automatically. - Products register pipeline behaviors explicitly. The DevPack provides
services.AddDevPackBehaviors()for the standard set. - Products can introduce additional pipeline behaviors before or after the DevPack defaults.