Pular para o conteúdo principal
Versão: 10.4.1

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.Mediator até a versão 2.0.0. Renomeado para Cqrs na 2.0.1 para evitar colisão com o namespace raiz Mediator (martinothamar) e com a área Messaging/ (broker integration). Veja ADR 002.

Componentes

TipoNomeResponsabilidade
InterfaceICommandMarker for a command returning Result
InterfaceICommand<TResponse>Marker for a command returning Result<TResponse>
InterfaceICommandHandler<TCommand>Handler contract for ICommand
InterfaceICommandHandler<TCommand, TResponse>Handler contract for ICommand<TResponse>
InterfaceIQuery<TResponse>Marker for a query returning Result<TResponse>
InterfaceIQueryHandler<TQuery, TResponse>Handler contract for IQuery<TResponse>
InterfaceINotificationEventMarker for application events dispatched via Mediator.IPublisher
InterfaceIMediatorHandlerFacade abstraction for publishing domain events
ClassMediatorHandlerDefault 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. Substitui Task<T> para evitar alocações em fluxos síncronos.
  • Handlers são descobertos por source generator no projeto consumidor. Substitui services.AddMediatR(typeof(...)) por services.AddMediator(...) (no projeto consumidor).
  • Pipeline behaviors abertos requerem registro manual no consumidor (ex.: services.AddSingleton(typeof(IPipelineBehavior<,>), typeof(ValidationPipelineBehavior<,>))).
  • Interface Mediator.IPublisher substitui MediatR.IMediator para 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 recordsusing ITLab.Template.DevPack.Cqrs; (ganham nossos markers ICommand<TResponse>, IQueryHandler<,>, etc.).
  • Arquivos de endpoints que injetam IMediatorusing 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 INotificationEvent to replace synchronous return values — it is fire-and-observe only.
  • Do not return Task<TResponse> from handlers — the contract is ValueTask<TResponse>.
  • Do not declare ICommand<Result<T>> / IQuery<Result<T>>Result<> is applied automatically by the handler signature. Use ICommand<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 the Mediator package; 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.