Dependency Injection
Dispatch integrates with Microsoft.Extensions.DependencyInjection, providing automatic handler discovery, middleware registration, and flexible configuration options.
Before You Start
- .NET 10.0
- Install the required packages:
dotnet add package Excalibur.Dispatch
- Familiarity with Microsoft.Extensions.DependencyInjection
Basic Setup
var builder = WebApplication.CreateBuilder(args);
// Discover handlers from current assembly (recommended pattern)
builder.Services.AddDispatch(dispatch =>
{
dispatch.AddHandlersFromAssembly(typeof(Program).Assembly);
});
Registration Methods
AddDispatch (Recommended)
The primary registration method with fluent configuration:
// Simple: Basic registration with no configuration
builder.Services.AddDispatch();
// With configuration (recommended)
builder.Services.AddDispatch(dispatch =>
{
// Handlers are auto-registered with DI container (Scoped by default)
dispatch.AddHandlersFromAssembly(typeof(Program).Assembly);
// Configure middleware and pipelines
dispatch.UseMiddleware<LoggingMiddleware>();
dispatch.UseMiddleware<ValidationMiddleware>();
// Configure options
dispatch.ConfigureOptions<DispatchOptions>(options =>
{
options.DefaultTimeout = TimeSpan.FromSeconds(30);
});
});
Automatic Handler DI Registration
When using AddHandlersFromAssembly, handlers are automatically registered with the DI container. You no longer need separate registrations:
// All handler types are scanned and registered automatically:
// - IDispatchHandler<>, IActionHandler<>, IActionHandler<,>
// - IEventHandler<>, IDocumentHandler<>
// - IStreamingDocumentHandler<,>, IStreamConsumerHandler<>
// - IStreamTransformHandler<,>, IProgressDocumentHandler<>
builder.Services.AddDispatch(dispatch =>
{
// This single call registers handlers with both Dispatch AND the DI container
dispatch.AddHandlersFromAssembly(typeof(Program).Assembly);
});
// No longer needed - handlers are auto-registered:
// builder.Services.AddScoped<CreateOrderHandler>(); // REMOVED
Customizing Handler Lifetime
Control handler service lifetime with optional parameters:
builder.Services.AddDispatch(dispatch =>
{
// Default: Scoped lifetime
dispatch.AddHandlersFromAssembly(typeof(Program).Assembly);
// Custom lifetime
dispatch.AddHandlersFromAssembly(
typeof(Infrastructure).Assembly,
lifetime: ServiceLifetime.Transient);
// Skip DI registration (advanced: when you manage registration separately)
dispatch.AddHandlersFromAssembly(
typeof(Legacy).Assembly,
registerWithContainer: false);
});
Handler Lifetimes
By default, handlers are registered as scoped (one instance per request).
Lifetime Guidelines
| Lifetime | Use When |
|---|---|
| Scoped | Handler depends on scoped services (IUnitOfWork, IDb) |
| Transient | Handler is stateless and lightweight |
| Singleton | Handler is thread-safe with no scoped dependencies |
Service Injection
Inject services into handlers through constructor injection:
public class CreateOrderHandler : IActionHandler<CreateOrderAction, Guid>
{
private readonly IOrderRepository _repository;
private readonly ILogger<CreateOrderHandler> _logger;
private readonly IMessageContextAccessor _contextAccessor;
public CreateOrderHandler(
IOrderRepository repository,
ILogger<CreateOrderHandler> logger,
IMessageContextAccessor contextAccessor)
{
_repository = repository;
_logger = logger;
_contextAccessor = contextAccessor;
}
public async Task<Guid> HandleAsync(
CreateOrderAction action,
CancellationToken cancellationToken)
{
var correlationId = _contextAccessor.MessageContext?.CorrelationId;
_logger.LogInformation(
"Creating order for {CustomerId}, CorrelationId: {CorrelationId}",
action.CustomerId,
correlationId);
return await _repository.CreateAsync(action, cancellationToken);
}
}
Multiple Assemblies
Register handlers from multiple assemblies:
builder.Services.AddDispatch(
typeof(DomainHandlers).Assembly,
typeof(InfrastructureHandlers).Assembly,
typeof(IntegrationHandlers).Assembly);
Or with the builder pattern using the params Assembly[] overload:
builder.Services.AddDispatch(dispatch =>
{
// Single call with multiple assemblies (params overload)
dispatch.AddHandlersFromAssembly(
typeof(DomainHandlers).Assembly,
typeof(InfrastructureHandlers).Assembly,
typeof(IntegrationHandlers).Assembly);
});
Manual Registration
For fine-grained control, register handlers manually:
// Auto-discover most handlers
builder.Services.AddDispatch(dispatch =>
{
dispatch.AddHandlersFromAssembly(typeof(Program).Assembly);
});
// Override specific handlers
builder.Services.AddScoped<IActionHandler<CreateOrderAction, Guid>, CustomCreateOrderHandler>();
// Register with specific lifetime
builder.Services.AddSingleton<IActionHandler<GetConfigAction, Config>, CachedConfigHandler>();
Middleware Registration
Register custom middleware using the configuration builder:
builder.Services.AddDispatch(dispatch =>
{
dispatch.AddHandlersFromAssembly(typeof(Program).Assembly);
// Configure middleware via builder
dispatch.ConfigurePipeline("Default", pipeline =>
{
pipeline.Use<LoggingMiddleware>();
pipeline.Use<ValidationMiddleware>();
pipeline.Use<AuthorizationMiddleware>();
});
});
Decorator Pattern
Wrap handlers with cross-cutting concerns using decorators. The Decorate<>() method requires the Scrutor package:
dotnet add package Scrutor
// Register the handler
builder.Services.AddScoped<IActionHandler<CreateOrderAction>, CreateOrderHandler>();
// Decorate with logging (requires Scrutor)
builder.Services.Decorate<IActionHandler<CreateOrderAction>, LoggingHandlerDecorator<CreateOrderAction>>();
// Decorate with retry (requires Scrutor)
builder.Services.Decorate<IActionHandler<CreateOrderAction>, RetryHandlerDecorator<CreateOrderAction>>();
Keyed Services
Use keyed services for named implementations:
// Register keyed handlers
builder.Services.AddKeyedScoped<IOrderProcessor, StandardOrderProcessor>("standard");
builder.Services.AddKeyedScoped<IOrderProcessor, ExpressOrderProcessor>("express");
// Inject by key
public class OrderHandler
{
public OrderHandler(
[FromKeyedServices("express")] IOrderProcessor expressProcessor)
{
// ...
}
}
Excalibur subsystem packages (EventSourcing, Outbox, Inbox, Saga, LeaderElection, Persistence) register their primary stores as keyed singletons under "default". Non-keyed convenience aliases are registered automatically, so you can inject IEventStore, IOutboxStore, ISagaStore, etc. directly — no [FromKeyedServices] attribute required:
// Just inject the store directly — the non-keyed alias forwards to keyed "default"
public class OrderService(IEventStore eventStore, IOutboxStore outboxStore)
{
// ...
}
Use [FromKeyedServices("key")] only when you register multiple named implementations of the same interface.
Startup Prerequisite Validation
Every Excalibur subsystem registers an internal IHostedService prerequisite validator that runs during IHost.StartAsync. If you call an Add* method (e.g., AddEventSourcing(...)) without selecting a concrete provider (e.g., .UseSqlServer(...)), the host fails immediately with an actionable error message instead of failing later at first use:
Excalibur event sourcing is missing the required IEventStore implementation.
Call a provider extension inside AddEventSourcing(...) — for example
es => es.UseSqlServer(sql => sql.ConnectionString(...)),
es => es.UsePostgres(...), or es => es.UseCosmosDb(...)
— before host startup.
Prerequisite validators are registered for:
| Subsystem | Required Interface | Add Method |
|---|---|---|
| Event Sourcing | IEventStore | AddEventSourcing(...) |
| Outbox | IOutboxStore | AddOutbox(...) |
| Inbox | IInboxStore | AddInbox(...) |
| Saga | ISagaStore | AddSagas(...) |
| Leader Election | ILeaderElection | AddLeaderElection(...) |
| Persistence | IPersistenceProvider | AddPersistence(...) |
These validators are AOT-safe (no reflection) and invisible to consumers — they are registered transparently by each subsystem's DI extension.
Transport and Cross-Cutting Registration
The AddDispatch() builder also supports transport and cross-cutting concern registration through extension methods:
builder.Services.AddDispatch(dispatch =>
{
dispatch.AddHandlersFromAssembly(typeof(Program).Assembly);
// Transports (Use prefix — pluggable infrastructure)
dispatch.UseRabbitMQ(rmq => rmq.HostName("localhost"));
dispatch.UseKafka(kafka => kafka.BootstrapServers("localhost:9092"));
// Cross-cutting (Add prefix — additive features)
dispatch.UseObservability();
dispatch.UseResilience(res => res.DefaultRetryCount = 3);
dispatch.UseCaching();
dispatch.UseSecurity(builder.Configuration);
});
See Configuration for full builder pattern reference.
Excalibur Subsystem Registration
The unified AddExcalibur() entry point registers Dispatch primitives with sensible defaults:
Feature methods are package-owned: .AddEventSourcing(...) comes from Excalibur.EventSourcing, .AddOutbox(...) from Excalibur.Outbox, and .AddSagas(...) from Excalibur.Saga.
// Simple — Dispatch defaults are sufficient
builder.Services.AddExcalibur(excalibur =>
{
excalibur
.AddEventSourcing(es => es.UseEventStore<SqlServerEventStore>())
.AddOutbox(outbox => outbox.UseSqlServer(opts => opts.ConnectionString = connectionString))
.AddSagas();
});
Excalibur with Custom Dispatch Configuration
When you need transports, pipeline profiles, or middleware, call AddDispatch with a builder action. Both orderings are safe because all Dispatch registrations use TryAdd internally:
// 1. Configure Dispatch with transports and middleware
builder.Services.AddDispatch(dispatch =>
{
dispatch.AddHandlersFromAssembly(typeof(Program).Assembly);
dispatch.UseRabbitMQ(rmq => rmq.HostName("localhost"));
dispatch.UseObservability();
dispatch.ConfigurePipeline("default", p => p.UseValidation());
});
// 2. Configure Excalibur subsystems
builder.Services.AddExcalibur(excalibur =>
{
excalibur
.AddEventSourcing(es => es.UseEventStore<SqlServerEventStore>())
.AddOutbox(outbox => outbox.UseSqlServer(opts => opts.ConnectionString = connectionString));
});
Common Services
Dispatch registers these services automatically:
| Service | Lifetime | Purpose |
|---|---|---|
IDispatcher | Scoped | Message dispatching |
IMessageContextAccessor | Scoped | Access current message context |
IMessageContextFactory | Scoped | Create new contexts |
IPipelineProfileRegistry | Singleton | Pipeline profile lookup |
Testing Configuration
Override services for testing:
public class TestFixture : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// Replace real services with test doubles
services.RemoveAll<IOrderRepository>();
services.AddScoped<IOrderRepository, InMemoryOrderRepository>();
// Replace external services
services.RemoveAll<IPaymentGateway>();
services.AddSingleton<IPaymentGateway, FakePaymentGateway>();
});
}
}
What's Next
You've covered all the core concepts. Start building with Dispatch:
- Handlers - Advanced handler patterns
- Pipeline - Middleware and behaviors
- Transports - Configure message transport for production
- Event Sourcing - Build event-sourced applications
See Also
- Configuration — Builder pattern reference, options binding, and environment-specific setup
- Test Harness — DispatchTestHarness for integration testing with service overrides
- Middleware — Register and configure middleware in the DI pipeline
- Custom Middleware — Build your own middleware with constructor-injected services