Table of Contents

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.