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 (Use prefix)​

MethodPackageExample
UseObservability()Excalibur.Dispatch.Observabilitydispatch.UseObservability(obs => ...)
UseResilience()Excalibur.Dispatch.Resilience.Pollydispatch.UseResilience(res => ...)
UseCaching()Excalibur.Dispatch.Cachingdispatch.UseCaching()
UseSecurity()Excalibur.Securitydispatch.UseSecurity(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
UseCaching()Caching (on IDispatchBuilder)Excalibur.Dispatch.Caching
UseSecurity()Security (on IDispatchBuilder)Excalibur.Security
AddMessagePackSerializer()MessagePack serializationExcalibur.Dispatch.Serialization.MessagePack
AddMemoryPackSerializer()MemoryPack serializationExcalibur.Dispatch.Serialization.MemoryPack

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 (Use prefix)
dispatch.UseObservability();
dispatch.UseResilience(res => res.DefaultRetryCount = 3);
dispatch.UseCaching();
dispatch.UseSecurity(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
// JSON is the default with AddDispatch(). For binary serializers, install the provider package:

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

// OpenTelemetry
builder.Services.AddOpenTelemetry()
.WithTracing(t => t.AddSource("Excalibur.Dispatch"))
.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.UseObservability();
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

Advertised capabilities are wired or fail loud​

Excalibur holds a structural invariant: any configuration option the API advertises is either backed by a live implementation, or it fails fast at startup with a clear OptionsValidationException. A configured strategy or capability never silently degrades to a no-op at runtime.

In practice this means:

  • Selecting a strategy or provider that the active package cannot service is rejected during IHost.StartAsync(), not ignored on the hot path.

  • Provider registration paths fail fast on invalid options. For example, every serverless-hosting registration overload (AddServerlessHosting(...)) wires ValidateOnStart(), so misconfiguration surfaces at startup regardless of which overload you call.

  • A capability that a given store does not implement (for example, retry-backoff scheduling on a store that lacks it) falls back to the documented default behavior rather than throwing — fail-open for optional cross-cutting features, fail-loud for advertised configuration.

  • Store durability is verified, not assumed. Wiring a production host with a volatile (in-memory) audit, grant, key, or schedule store is rejected at startup — a volatile store would report every write as saved and then lose it on restart, which is exactly the failure a fail-fast gate exists to prevent. Each subsystem exposes an explicit opt-out for development and test hosts (AuditLoggingOptions.AllowVolatileAuditStore, GrantDurabilityOptions.AllowVolatileGrantStore, KeyDurabilityOptions.AllowVolatileKeyProvider, ScheduleDurabilityOptions.AllowVolatileScheduleStore); the default is the protective value you get by saying nothing. These gates run from host startup — a host-less container must call ValidateStartupGates() to trigger them.

    Where the schedule-store gate applies. It is installed by the compositions that start the scheduler runtime — AddTimeAwareScheduling, AddAdaptiveTimeAwareScheduling, AddLightweightTimeAwareScheduling and AddThroughputTimeAwareScheduling — and not by AddDispatchScheduling on its own. That split is deliberate. Running the scheduler is what turns a schedule from a request into a promise: the host accepts a delivery now and owes it later, and a volatile store breaks that promise on restart after having reported the schedule accepted, so the failure appears only as an absence. A host that composes scheduling without ever starting the runtime — a development host, or one replacing an in-process mediator — is not made to justify a store it never schedules against. Register a durable store with AddDurableScheduleStore<T>(), which replaces the in-memory registration in either composition order.

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.SecurityJwtAuthenticationOptions, SigningOptionsYes
Excalibur.Dispatch.Resilience.PollyPollyResilienceOptionsYes
Excalibur.Dispatch.CachingCacheOptionsYes
Excalibur.ComplianceErasureOptionsYes
Excalibur.Dispatch.PatternsClaimCheckOptionsYes
Excalibur.Dispatch.Transport.RabbitMQRabbitMqTransportOptionsYes
Excalibur.Dispatch.Transport.GooglePubSubOrderingKeyOptionsYes
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​