Requests
A request is a message that goes to exactly one handler and produces one response. Use requests for commands (change state) and queries (read state); Caesar treats both the same way.
Requests with a response
Implement IRequest<TResponse> and handle it with IRequestHandler<TRequest, TResponse>:
public sealed record GetCustomer(Guid Id) : IRequest<Customer?>;
public sealed class GetCustomerHandler(ICustomerRepository repository) : IRequestHandler<GetCustomer, Customer?>
{
public Task<Customer?> Handle(GetCustomer request, CancellationToken cancellationToken)
=> repository.Find(request.Id, cancellationToken);
}
sender.Send(new GetCustomer(id), cancellationToken) returns Task<Customer?>; the response type is inferred from the
request.
Commands without a response
A command that returns nothing implements IRequest, which is shorthand for IRequest<Unit>. Its handler
implements IRequestHandler<TRequest> and returns a plain Task:
public sealed record DeactivateCustomer(Guid Id) : IRequest;
public sealed class DeactivateCustomerHandler(ICustomerRepository repository) : IRequestHandler<DeactivateCustomer>
{
public Task Handle(DeactivateCustomer request, CancellationToken cancellationToken)
=> repository.Deactivate(request.Id, cancellationToken);
}
Unit is the "no value" response. You meet it when a behavior or post-processor sees every request,
including commands: for a command, TResponse is Unit.
One handler per request
Each request type resolves exactly one handler. If scanning finds more than one handler for the same request, the first
one found wins. A handler you register yourself before calling AddCaesar takes precedence over anything scanning
finds, which is useful for replacing a handler in tests.
Runtime-typed dispatch
When the request type is only known at runtime, for example after deserializing a message from a queue, use
Send(object):
// The request type is only known at runtime, e.g. deserialized from a message queue.
public static async Task<object?> Dispatch(ISender sender, object message, CancellationToken cancellationToken)
{
// Returns the handler's response, or Unit.Value for a command that implements IRequest.
return await sender.Send(message, cancellationToken);
}
It returns the handler's response boxed as object?, or Unit.Value for a command. Passing an object that implements
neither IRequest nor IRequest<T> throws ArgumentException. The same pattern exists for notifications
(Publish(object)) and streams (CreateStream(object)).
Performance
The first Send for a request type builds and caches a small wrapper for it. After that, dispatch does no reflection.