Table of Contents

Configuration

Everything is configured in the AddCaesar callback, through CaesarServiceConfiguration.

Options

services.AddCaesar(cfg =>
{
    cfg.RegisterServicesFromAssemblyContaining<CreateCustomer>();       // required, at least one assembly
    cfg.TypeEvaluator = type => type.Namespace?.StartsWith("Contoso.Application", StringComparison.Ordinal) == true;

    cfg.Lifetime = ServiceLifetime.Transient;                            // scanned handlers, processors, exception handlers
    cfg.MediatorLifetime = ServiceLifetime.Scoped;                       // IMediator, ISender, IPublisher, INotificationPublisher
    cfg.MediatorImplementationType = typeof(Mediator);
    cfg.NotificationPublisherType = typeof(ForeachAwaitPublisher);
    cfg.AutoRegisterRequestProcessors = true;
    cfg.RequestExceptionActionProcessorStrategy = RequestExceptionActionProcessorStrategy.ApplyForUnhandledExceptions;
});
Option Default Purpose
RegisterServicesFromAssembly* required Assemblies to scan for handlers, processors, exception handlers and actions.
TypeEvaluator accept all Filter scanned types.
Lifetime Transient Lifetime for scanned handlers, processors, exception handlers and actions.
MediatorLifetime Scoped Lifetime for IMediator, ISender, IPublisher and INotificationPublisher.
MediatorImplementationType Mediator Subclass Mediator and override PublishCore to intercept publishes.
NotificationPublisherType / NotificationPublisher ForeachAwaitPublisher Publish strategy by type or instance.
AutoRegisterRequestProcessors true Register scanned pre/post-processors automatically.
RequestExceptionActionProcessorStrategy ApplyForUnhandledExceptions Where exception actions sit relative to exception handlers.
AddBehavior / AddOpenBehavior / AddOpenBehaviors Request behaviors, in execution order.
AddStreamBehavior / AddOpenStreamBehavior Stream behaviors.
AddRequestPreProcessor / AddOpenRequestPreProcessor Explicit pre-processors.
AddRequestPostProcessor / AddOpenRequestPostProcessor Explicit post-processors.

AddCaesar throws ArgumentException when no assembly was given, or when MediatorImplementationType or NotificationPublisherType does not implement the expected interface.

Registration rules

  • Registration is idempotent: calling AddCaesar twice, or adding a processor that scanning also found, does not duplicate it.
  • The first scanned handler for a request type wins.
  • Registrations you make before AddCaesar take precedence over scanning.

Lifetimes

Lifetime and MediatorLifetime are separate because they answer different questions.

Handlers default to Transient: they are stateless, and two Send calls in one scope should not share an instance.

The mediator defaults to Scoped. A transient ISender can be injected into a singleton without the container objecting, and the captured mediator then holds the root provider. With scope validation on, the first request needing a scoped dependency such as a DbContext then fails at runtime, far from the constructor that caused it; with it off, that DbContext is silently resolved from the root and lives for the whole application. With a scoped mediator, the container reports the capturing singleton at startup instead, provided both ValidateScopes and ValidateOnBuild are enabled. ASP.NET Core and the generic host enable both in the Development environment.

The cost is that ISender is no longer resolvable straight from the root provider. Resolve it inside a scope:

using var scope = host.Services.CreateScope();
var sender = scope.ServiceProvider.GetRequiredService<ISender>();
await sender.Send(new DeactivateCustomer(Guid.NewGuid()));

A BackgroundService is a singleton, so inject IServiceScopeFactory and open a scope per unit of work:

// A BackgroundService is a singleton: open a scope per unit of work instead of injecting ISender.
public sealed class NightlyExport(IServiceScopeFactory scopeFactory) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await using (var scope = scopeFactory.CreateAsyncScope())
            {
                var sender = scope.ServiceProvider.GetRequiredService<ISender>();
                await foreach (var page in sender.CreateStream(new ExportCustomers(500), stoppingToken))
                {
                    Console.WriteLine($"Exported {page.Count}");
                }
            }

            await Task.Delay(TimeSpan.FromHours(24), stoppingToken);
        }
    }
}

If you genuinely need a root-resolvable mediator, for example in a short console app with no scoped dependencies, set cfg.MediatorLifetime = ServiceLifetime.Transient.

Open-generic handlers

An open generic class is registered against the open interface when it implements that interface with its own type parameters in declaration order, which is the shape the Microsoft container can close at runtime:

// Registered against INotificationHandler<> and closed by the container for every notification type.
public sealed class AuditEverything<TNotification> : INotificationHandler<TNotification>
    where TNotification : INotification
{
    public Task Handle(TNotification notification, CancellationToken cancellationToken)
    {
        Console.WriteLine($"Published {typeof(TNotification).Name}");
        return Task.CompletedTask;
    }
}

Generic constraints are honoured: the container skips closings that violate them.

An open generic that implements one of Caesar's interfaces in any other shape, with a different arity or with type parameters out of order, can never be closed by the container. AddCaesar throws InvalidOperationException rather than leave a handler that fails on the first request that needs it. This example is intentionally not a working registration:

// Throws at AddCaesar: one type parameter, but IRequestHandler<,> takes two.
public sealed class QueryHandler<T> : IRequestHandler<Query<T>, T> { ... }

Generic types that implement none of Caesar's interfaces are ignored.

Filtering the scan

To keep a type out of the scan, whether a deliberately shaped handler or one you register by hand, exclude it with TypeEvaluator:

services.AddCaesar(cfg =>
{
    cfg.RegisterServicesFromAssemblyContaining<CreateCustomer>();
    cfg.TypeEvaluator = type => type != typeof(CountCustomers);   // keep this handler out of the scan
});