Skip to main content

Configuration: Advanced

Advanced configuration topics including startup validation, builder API reference, and complete configuration patterns.

Before You Start

Configuration Validation

Validate configuration at startup:

builder.Services.AddOptions<DispatchOptions>()
.Bind(builder.Configuration.GetSection("Dispatch"))
.Validate(options =>
{
if (options.DefaultTimeout <= TimeSpan.Zero)
return false;
return true;
}, "DefaultTimeout must be positive")
.ValidateOnStart();

Builder Pattern Reference

IDispatchBuilder -- Core Methods

MethodPurposeExample
ConfigurePipeline()Named pipeline setupdispatch.ConfigurePipeline("default", p => ...)
RegisterProfile()Pipeline profiledispatch.RegisterProfile(new MyProfile())
AddBinding()Transport bindingdispatch.AddBinding(b => ...)
UseMiddleware<T>()Global middlewaredispatch.UseMiddleware<LoggingMiddleware>()
ConfigureOptions<T>()Options configurationdispatch.ConfigureOptions<DispatchOptions>(o => ...)

IDispatchBuilder -- Transport Extensions (Use prefix)

MethodPackageExample
UseRabbitMQ()Excalibur.Dispatch.Transport.RabbitMQdispatch.UseRabbitMQ(rmq => ...)
UseKafka()Excalibur.Dispatch.Transport.Kafkadispatch.UseKafka(kafka => ...)
UseAwsSqs()Excalibur.Dispatch.Transport.AwsSqsdispatch.UseAwsSqs(sqs => ...)
UseAzureServiceBus()Excalibur.Dispatch.Transport.AzureServiceBusdispatch.UseAzureServiceBus(asb => ...)
UseGooglePubSub()Excalibur.Dispatch.Transport.GooglePubSubdispatch.UseGooglePubSub(pubsub => ...)

All transport methods support named overloads: dispatch.UseKafka("analytics", kafka => ...).

IDispatchBuilder -- Cross-Cutting Extensions (Add prefix)

MethodPackageExample
AddObservability()Excalibur.Dispatch.Observabilitydispatch.AddObservability(obs => ...)
AddResilience()Excalibur.Dispatch.Resilience.Pollydispatch.AddResilience(res => ...)
AddCaching()Excalibur.Dispatch.Cachingdispatch.AddCaching()
AddSecurity()Excalibur.Dispatch.Securitydispatch.AddSecurity(configuration)

Standalone IServiceCollection Methods

These standalone methods remain available for consumers who prefer direct registration:

MethodPurposePackage
AddDispatch()Core Dispatch servicesExcalibur.Dispatch
AddRabbitMQTransport()RabbitMQ transportExcalibur.Dispatch.Transport.RabbitMQ
AddKafkaTransport()Kafka transportExcalibur.Dispatch.Transport.Kafka
AddAwsSqsTransport()AWS SQS transportExcalibur.Dispatch.Transport.AwsSqs
AddAzureServiceBusTransport()Azure Service BusExcalibur.Dispatch.Transport.AzureServiceBus
AddGooglePubSubTransport()Google Pub/SubExcalibur.Dispatch.Transport.GooglePubSub
AddDispatchObservability()ObservabilityExcalibur.Dispatch.Observability
AddDispatchResilience()Resilience (Polly)Excalibur.Dispatch.Resilience.Polly
AddDispatchCaching()CachingExcalibur.Dispatch.Caching
AddDispatchSecurity()SecurityExcalibur.Dispatch.Security
AddMemoryPackInternalSerialization()MemoryPack serializationExcalibur.Dispatch.Serialization.MemoryPack
AddMessagePackSerialization()MessagePack serializationExcalibur.Dispatch.Serialization.MessagePack

Common Configuration Patterns

Minimal API

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDispatch(dispatch =>
{
dispatch.AddHandlersFromAssembly(typeof(Program).Assembly);
});
var app = builder.Build();

app.MapPost("/orders", async (CreateOrderAction action, IDispatcher dispatcher, CancellationToken ct) =>
await dispatcher.DispatchAsync(action, ct));

app.Run();
var builder = WebApplication.CreateBuilder(args);

// Unified Dispatch registration -- transports + cross-cutting through the builder
builder.Services.AddDispatch(dispatch =>
{
dispatch.AddHandlersFromAssembly(typeof(Program).Assembly);

// Transports (Use prefix)
dispatch.UseKafka(kafka => kafka.BootstrapServers(builder.Configuration["Kafka:Servers"]));
dispatch.UseRabbitMQ(rmq => rmq.HostName("localhost"));

// Cross-cutting concerns (Add prefix)
dispatch.AddObservability();
dispatch.AddResilience(res => res.DefaultRetryCount = 3);
dispatch.AddCaching();
dispatch.AddSecurity(builder.Configuration);

// Global middleware
dispatch.UseMiddleware<LoggingMiddleware>();
dispatch.UseMiddleware<ValidationMiddleware>();
dispatch.UseMiddleware<AuthorizationMiddleware>();

// Options
dispatch.ConfigureOptions<DispatchOptions>(options =>
{
options.DefaultTimeout = TimeSpan.FromSeconds(30);
});

// Multi-transport routing
dispatch.UseRouting(routing =>
{
routing.Transport
.Route<OrderCreatedEvent>().To("kafka")
.Default("rabbitmq");
});
});

// Serialization
builder.Services.AddMemoryPackInternalSerialization();

// Health checks
builder.Services.AddHealthChecks()
.AddTransportHealthChecks();

// OpenTelemetry
builder.Services.AddOpenTelemetry()
.WithTracing(t => t.AddSource("Excalibur.Dispatch.Observability"))
.WithMetrics(m => m.AddDispatchMetrics());

var app = builder.Build();
app.MapHealthChecks("/health");
app.Run();

Combined with Excalibur

When using Excalibur subsystems alongside Dispatch, call AddDispatch for transport and pipeline configuration, then AddExcalibur for domain infrastructure. AddExcalibur registers Dispatch primitives with defaults, so both orderings are safe:

// Dispatch -- transports, middleware, pipelines
builder.Services.AddDispatch(dispatch =>
{
dispatch.AddHandlersFromAssembly(typeof(Program).Assembly);
dispatch.UseKafka(kafka => kafka.BootstrapServers("localhost:9092"));
dispatch.AddObservability();
dispatch.ConfigurePipeline("default", p => p.UseValidation());
});

// Excalibur -- event sourcing, outbox, sagas
builder.Services.AddExcalibur(excalibur =>
{
excalibur
.AddEventSourcing(es => es.UseEventStore<SqlServerEventStore>())
.AddOutbox(outbox => outbox.UseSqlServer(opts => opts.ConnectionString = connectionString))
.AddSagas();
});

Options Validation with ValidateOnStart

Excalibur uses ValidateOnStart() to catch configuration errors at application startup rather than at first use. This follows the Microsoft.Extensions.Options validation pattern.

How It Works

When you call ValidateOnStart(), the framework validates all IOptions<T> registrations during IHost.StartAsync(). If any validation fails, the application throws OptionsValidationException immediately -- before handling any requests.

// This is what happens inside Excalibur's DI extensions:
services.AddOptions<LeaderElectionOptions>()
.ValidateDataAnnotations() // Validates [Required], [Range], etc.
.ValidateOnStart(); // Runs validation at startup, not first use

Built-In Validators

Excalibur provides IValidateOptions<T> validators for cross-property constraint checking. These go beyond [DataAnnotations] to validate relationships between properties.

Example: LeaderElectionOptionsValidator

public sealed class LeaderElectionOptionsValidator : IValidateOptions<LeaderElectionOptions>
{
public ValidateOptionsResult Validate(string? name, LeaderElectionOptions options)
{
if (options.RenewInterval >= options.LeaseDuration)
{
return ValidateOptionsResult.Fail(
$"RenewInterval ({options.RenewInterval}) must be less than " +
$"LeaseDuration ({options.LeaseDuration}).");
}

if (options.GracePeriod >= options.LeaseDuration)
{
return ValidateOptionsResult.Fail(
$"GracePeriod ({options.GracePeriod}) must be less than " +
$"LeaseDuration ({options.LeaseDuration}).");
}

return ValidateOptionsResult.Success;
}
}

Packages with ValidateOnStart

The following packages register ValidateOnStart() + ValidateDataAnnotations() for their Options classes. Many also include cross-property IValidateOptions<T> validators:

PackageOptions ClassCross-Property Validator
Excalibur.DispatchDispatchTelemetryOptions, CircuitBreakerOptions, TimePolicyOptions, OutboxOptionsYes
Excalibur.Dispatch.ObservabilityContextObservabilityOptions, TelemetrySanitizerOptionsYes
Excalibur.Dispatch.SecurityJwtAuthenticationOptions, SigningOptionsYes
Excalibur.Dispatch.Resilience.PollyPollyResilienceOptionsYes
Excalibur.Dispatch.CachingCacheOptionsYes
Excalibur.Dispatch.ComplianceErasureOptionsYes
Excalibur.Dispatch.PatternsClaimCheckOptionsYes
Excalibur.Dispatch.Transport.RabbitMQRabbitMqTransportOptionsYes
Excalibur.Dispatch.Transport.GooglePubSubStreamingPullOptions, OrderingKeyOptionsYes
Excalibur.Dispatch.LeaderElectionLeaderElectionOptionsYes
Excalibur.EventSourcingMaterializedViewOptions, SnapshotUpgradingOptionsYes
Excalibur.SagaSagaOptionsYes
Excalibur.Saga.SqlServerSqlServerSagaStoreOptions, SqlServerSagaTimeoutStoreOptionsYes
Excalibur.CdcCdcProcessingOptionsYes
Various data providersSqlServerPersistenceOptions, PostgresPersistenceOptions, CDC state store optionsYes

Writing Custom Validators

Create an IValidateOptions<T> implementation for cross-property validation in your own Options classes:

public sealed class MyFeatureOptionsValidator : IValidateOptions<MyFeatureOptions>
{
public ValidateOptionsResult Validate(string? name, MyFeatureOptions options)
{
ArgumentNullException.ThrowIfNull(options);

if (options.RetryCount > 0 && options.RetryDelay <= TimeSpan.Zero)
{
return ValidateOptionsResult.Fail(
"RetryDelay must be positive when RetryCount > 0.");
}

return ValidateOptionsResult.Success;
}
}

Register your validator alongside your Options:

services.AddOptions<MyFeatureOptions>()
.ValidateDataAnnotations()
.ValidateOnStart();

services.TryAddEnumerable(
ServiceDescriptor.Singleton<IValidateOptions<MyFeatureOptions>, MyFeatureOptionsValidator>());
Why ValidateOnStart Matters

Without ValidateOnStart(), misconfigured options are only detected when IOptions<T>.Value is first accessed -- which could be hours into production under a specific code path. ValidateOnStart() fails fast at startup, before any traffic is served.

See Also