Getting started
This page takes you from an empty solution to a request that is sent, handled and returns a result. It assumes a layered solution with an Application project and a host (API, worker or console) project, but everything works in a single project too.
Install
Reference the contracts from the Application layer and the implementation from the host:
dotnet add src/MyApp.Application package Caesar.Abstractions
dotnet add src/MyApp.Api package Caesar
Caesar.Abstractions has no dependencies, so the Application layer stays free of infrastructure. Caesar depends only
on Microsoft.Extensions.DependencyInjection.Abstractions.
Define a request and its handler
A request is a message with exactly one handler. Implement IRequest<TResponse> with the response type, and IRequestHandler<TRequest, TResponse> to handle it:
public sealed record CreateCustomer(string Name, string Email) : IRequest<Guid>;
public sealed class CreateCustomerHandler(ICustomerRepository repository) : IRequestHandler<CreateCustomer, Guid>
{
public async Task<Guid> Handle(CreateCustomer request, CancellationToken cancellationToken)
{
var customer = new Customer(Guid.NewGuid(), request.Name, request.Email);
await repository.Save(customer, cancellationToken);
return customer.Id;
}
}
A command that returns nothing implements IRequest and is handled by IRequestHandler<TRequest>:
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);
}
Register Caesar
Call AddCaesar in the composition root and point it at the assemblies that contain your handlers. Scanning finds
handlers, notification handlers, processors and exception handlers; behaviors are added explicitly because their order
matters.
services.AddCaesar(cfg =>
{
cfg.RegisterServicesFromAssemblyContaining<CreateCustomer>();
cfg.NotificationPublisherType = typeof(TaskWhenAllPublisher);
cfg.AddOpenBehavior(typeof(LoggingBehavior<,>)); // outermost
cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
});
Every option is described in Configuration.
Send
public sealed class CustomerEndpoints(ISender sender)
{
public Task<Guid> Create(CreateCustomer command, CancellationToken cancellationToken)
=> sender.Send(command, cancellationToken);
public Task Deactivate(Guid id, CancellationToken cancellationToken)
=> sender.Send(new DeactivateCustomer(id), cancellationToken);
}
Inject the narrowest interface a component needs:
| Interface | Use it when the component |
|---|---|
| ISender | only sends requests or creates streams |
| IPublisher | only publishes notifications |
| IMediator | does both |
Resolve inside a scope
The mediator is registered as scoped by default, so it is resolved from a request scope, which ASP.NET Core creates for you. Outside a request, for example in a console app or a test, create the scope yourself:
using var scope = host.Services.CreateScope();
var sender = scope.ServiceProvider.GetRequiredService<ISender>();
await sender.Send(new DeactivateCustomer(Guid.NewGuid()));
Configuration → Lifetimes explains why, and how to change it.