Configuration
Builder Pattern
var messaging = builder.Services.AddMessaging(options =>
{
// Connection
options.ConnectionString = "...";
// OR
options.FullyQualifiedNamespace = "mybus.servicebus.windows.net";
options.Credential = new DefaultAzureCredential();
// Defaults for all processors
options.Defaults.MaxConcurrentCalls = 16;
options.Defaults.AutoComplete = true;
options.Defaults.PrefetchCount = 0;
options.Defaults.MaxAutoLockRenewalDuration = TimeSpan.FromMinutes(5);
// Serialization
options.Serializer = new SystemTextJsonSerializer(new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
// Retry policy
options.RetryOptions.MaxRetries = 3;
options.RetryOptions.Delay = TimeSpan.FromSeconds(1);
options.RetryOptions.MaxDelay = TimeSpan.FromSeconds(30);
options.RetryOptions.Mode = ServiceBusRetryMode.Exponential;
});Handler Configuration
Handler options (concurrency, prefetch, auto-complete) are configured via a delegate on the handler method. Error handling is chained on the returned builder:
messaging.MapQueue<OrderCreated>("orders")
.MapHandler(
options => options
.WithConcurrency(maxConcurrentCalls: 10)
.WithPrefetch(count: 20)
.WithAutoComplete(enabled: false)
.WithAutoLockRenewal(duration: TimeSpan.FromMinutes(10))
.WithDeadLetterOnFailure(reason: "ProcessingFailed"),
handler)
.MapErrorHandler(async (ex, ctx, ct) =>
{
// Custom error handling
await ctx.DeadLetterAsync("ProcessingFailed", ex.Message, ct);
});
// Process dead letter queue
messaging.MapQueue<OrderCreated>("orders")
.MapDeadLetterHandler(handler);Infrastructure Topology Introspection
While this library doesn't create or manage infrastructure, it does know what infrastructure it requires. Exposing this information helps with:
- Validation: Check at startup that required entities exist
- Testing: Verify handler registrations are wired correctly
- Documentation: Generate infrastructure requirements
- IaC Generation: Feed into Bicep/Terraform/Pulumi templates
API
Inject IServiceBusTopology to inspect the registered queues, topics, and subscriptions at runtime. Each requirement record includes the entity name, whether sessions are required, the message type, and whether it's a dead-letter processor.
Usage
var topology = app.Services.GetRequiredService<IServiceBusTopology>();
// Log what we need
foreach (var queue in topology.Queues)
{
logger.LogInformation("Requires queue: {Queue} (sessions: {Sessions})",
queue.QueueName, queue.RequiresSessions);
}
// Validate at startup
foreach (var queue in topology.Queues)
{
if (!await adminClient.QueueExistsAsync(queue.QueueName))
throw new InvalidOperationException($"Required queue '{queue.QueueName}' does not exist");
}Built-in Validation (Optional)
var messaging = builder.Services.AddMessaging(options =>
{
options.ValidateTopologyOnStartup = true; // Throws if entities don't exist
});TIP
Topology validation checks that entities exist and that their configuration matches what the library expects. For example, if you register a session handler via MapSessionHandler, validation will fail if the Azure queue does not have RequiresSession = true. Key properties validated:
| Library Feature | Required Azure Entity Property |
|---|---|
MapSessionHandler / MapSessionSubscription | RequiresSession = true (immutable - must recreate entity) |
MapDeadLetterHandler | Entity must exist (dead-letter sub-queue is automatic) |
Request/reply (RequestAsync) | Reply queue with RequiresSession = true |
| High-concurrency handlers | Consider increasing LockDuration (default 30s, max 5min) |
WithDeadLetterOnFailure | DeadLetteringOnMessageExpiration or MaxDeliveryCount on entity |
Entity Settings
QueueBuilder<T> and TopicBuilder<T> expose fluent methods to declare entity-level settings. These settings are captured in the topology and surface through IServiceBusTopology requirement records, enabling export to emulator config, Bicep, ARM, or runtime provisioning.
// Queue with custom settings
messaging.MapQueue<OrderCreated>("orders")
.WithMaxDeliveryCount(5)
.WithLockDuration(TimeSpan.FromMinutes(2))
.WithDefaultMessageTimeToLive(TimeSpan.FromHours(24));
// Session queue
messaging.MapQueue<GatewayCommand>("gateway-commands")
.WithRequiresSession();
// Topic settings apply to subscriptions
messaging.MapTopic<DomainEvent>("events")
.WithMaxDeliveryCount(3)
.MapSubscription("audit", handler);Settings are nullable - null means "use platform default". The requirement records (QueueRequirement, SubscriptionRequirement) expose these as int? MaxDeliveryCount, TimeSpan? LockDuration, and TimeSpan? DefaultMessageTimeToLive.
| Method | Applies to | Default (Azure) |
|---|---|---|
WithMaxDeliveryCount(int) | Queue, Subscription | 10 |
WithLockDuration(TimeSpan) | Queue, Subscription | 30 seconds |
WithDefaultMessageTimeToLive(TimeSpan) | Queue, Subscription | Max (infinite) |
WithRequiresSession() | Queue only | false |
Topology Export
IServiceBusTopology includes an extension method for exporting to the Azure Service Bus Emulator JSON format. This is useful for integration testing - no separate config file or builder needed:
var topology = app.Services.GetRequiredService<IServiceBusTopology>();
// Export to emulator JSON string
string json = topology.ToEmulatorConfigJson();
// Export to UTF-8 bytes (for Testcontainers resource mapping)
byte[] bytes = topology.ToEmulatorConfigBytes();Null entity settings are filled with sensible defaults for the emulator: MaxDeliveryCount = 10, LockDuration = PT1M, DefaultMessageTimeToLive = PT1H.
See Testing for a complete example of using topology export with Testcontainers.
Topology Declarations (Sender-Only Services)
With the entity-first API, sender-only entities are declared simply by calling MapQueue<T> or MapTopic<T> without attaching a handler. This automatically registers both topology requirements and a typed IMessageSender<T>:
var messaging = builder.Services.AddMessaging(options =>
{
options.ConnectionString = "...";
});
// Sender-only - no handler attached, but topology is tracked
// and IMessageSender<OrderCreated> is registered in DI
messaging.MapQueue<OrderCreated>("orders");
messaging.MapTopic<DomainEvent>("events");This ensures IServiceBusTopology includes entities used for sending, not just receiving.