Skip to main content

Multi-Transport Routing

Dispatch supports routing messages to multiple transports based on type or predicate rules. All transport routing is configured through the unified UseRouting() API.

Before You Start​

  • .NET 10.0
  • Install Excalibur.Dispatch plus two or more transport packages:
    dotnet add package Excalibur.Dispatch
    dotnet add package Excalibur.Dispatch.Transport.Kafka
    dotnet add package Excalibur.Dispatch.Transport.RabbitMQ
  • Familiarity with choosing a transport and routing patterns

Quick Start​

builder.Services.AddPluggableSerialization(); // Transports don't seat a default serializer

builder.Services.AddDispatch(dispatch =>
{
dispatch.AddHandlersFromAssembly(typeof(Program).Assembly);

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

// Register each transport with fluent builder pattern
builder.Services.AddKafkaTransport(kafka =>
{
kafka.BootstrapServers("localhost:9092")
.MapTopic<OrderCreatedEvent>("dispatch-events");
});

builder.Services.AddRabbitMQTransport(rmq =>
{
rmq.HostName("localhost")
.Port(5672)
.Credentials("guest", "guest")
.ConfigureExchange(exchange => exchange.Name("dispatch-events"));
});

Bulk Routing with RouteAll​

For scenarios with many message types routed to the same transport, use RouteAll:

dispatch.UseRouting(routing =>
{
// Route multiple types in a single call
routing.Transport.RouteAll("kafka",
typeof(OrderCreatedEvent),
typeof(PaymentProcessedEvent),
typeof(ShipmentDispatchedEvent),
typeof(InventoryUpdatedEvent));

// Mix with individual routing
routing.Transport
.Route<AuditLogEvent>().To("rabbitmq")
.Default("rabbitmq");
});
note

RouteAll validates each type at registration time -- types that don't implement IIntegrationEvent throw ArgumentException immediately. This method uses reflection and carries [RequiresUnreferencedCode].

Transport Names​

Each transport registers with a default name. Use these names in routing rules:

TransportDefault NameRegistration Method
Kafka"kafka"AddKafkaTransport()
RabbitMQ"rabbitmq"AddRabbitMQTransport()
Azure Service Bus"azure-servicebus"AddAzureServiceBusTransport()
AWS SQS"aws-sqs"AddAwsSqsTransport()
Google Pub/Sub"google-pubsub"AddGooglePubSubTransport()
In-Memory"inmemory"AddInMemoryTransport()

You can also register named instances:

builder.Services.AddPluggableSerialization(); // Transports don't seat a default serializer
builder.Services.AddKafkaTransport("kafka-orders", kafka =>
{
kafka.BootstrapServers("localhost:9092")
.MapTopic<OrderCreatedEvent>("orders");
});

builder.Services.AddKafkaTransport("kafka-analytics", kafka =>
{
kafka.BootstrapServers("localhost:9092")
.MapTopic<AnalyticsEvent>("analytics");
});

What a name isolates, and what it does not​

Each named registration configures its own options instance, and that is what the transport under that name reads. Two named registrations of the same transport can carry different queues, topics, regions, credentials, or encryption keys, and neither overwrites the other.

If you registered two of the same transport before, check which configuration you were running

This did not always hold. A named registration used to write the same unnamed options instance as every other registration of that transport, so whichever registered last silently supplied the settings for both — two SQS transports named for different queues ran against one queue, and a per-transport KMS key could not reach its own queue. If you worked around this by splitting hosts, or by keeping the two configurations deliberately identical, you can stop.

What a name does not isolate: the underlying provider client and message bus. Those are registered by type, first-registration-wins, so two named registrations of one transport still share a single client and bus instance. In practice that means:

Per nameShared across every name of that transport
Options — queue/topic, region, endpoints, FIFO selectors, payload limits, encryption keyThe provider SDK client (for example one IAmazonSQS)
The keyed sender and receiver routing resolvesThe message bus singleton
Start-up services that apply configuration to the broker

So two names against different queues on one account work as you expect. Two names that need different credentials, different accounts, or different broker endpoints do not — the shared client is built once, from the first registration. Register the client yourself before the transports if you need to control it, or keep genuinely separate connections in separate hosts.

Routing Strategies​

IIntegrationEvent Constraint

Transport routing only accepts IIntegrationEvent types. Commands and domain events cannot be routed to transports — they are handled locally. See Routing for details.

Route by Message Type​

dispatch.UseRouting(routing =>
{
routing.Transport
.Route<OrderCreatedEvent>().To("kafka")
.Route<PaymentReceivedEvent>().To("kafka")
.Route<NotificationSentEvent>().To("rabbitmq")
.Default("rabbitmq");
});

Conditional Routing​

Route messages based on message content using .When() predicates:

dispatch.UseRouting(routing =>
{
routing.Transport
// Route high-value orders to priority queue
.Route<OrderCreatedEvent>()
.When(order => order.TotalAmount > 10000).To("rabbitmq-priority")
// Route standard orders to regular queue
.Route<OrderCreatedEvent>().To("rabbitmq")
.Default("rabbitmq");
});

Context-aware predicates have access to IMessageContext:

dispatch.UseRouting(routing =>
{
routing.Transport
.Route<OrderCreatedEvent>()
.When((msg, ctx) => ctx.Items.ContainsKey("express")).To("kafka")
.Route<OrderCreatedEvent>().To("rabbitmq")
.Default("rabbitmq");
});

Default Transport​

dispatch.UseRouting(routing =>
{
routing.Transport
// Specific routes evaluated first
.Route<OrderCreatedEvent>().To("kafka")
// Default for all unmatched message types
.Default("rabbitmq");
});

Route Evaluation Order​

Routes are evaluated in this order:

  1. Type-specific routes — Route<T>() matching the exact message type
  2. Conditional routes — .When() predicates evaluated in registration order
  3. Default transport — .Default()

The first matching route determines the target transport.

Real-World Scenarios​

High-Volume Events to Kafka, Default to RabbitMQ​

dispatch.UseRouting(routing =>
{
routing.Transport
// High-volume integration events -> Kafka for streaming/analytics
.Route<OrderCreatedEvent>().To("kafka")
.Route<PaymentProcessedEvent>().To("kafka")
.Route<InventoryUpdatedEvent>().To("kafka")
// Default -> RabbitMQ for all other integration events
.Default("rabbitmq");
});

Environment-Specific Routing​

dispatch.UseRouting(routing =>
{
if (builder.Environment.IsDevelopment())
{
// Local: everything in-memory
routing.Transport.Default("inmemory");
}
else
{
routing.Transport
.Route<OrderCreatedEvent>().To("kafka")
.Default("rabbitmq");
}
});

Combined Transport and Endpoint Routing​

UseRouting() provides both transport selection and endpoint routing in a single configuration:

dispatch.UseRouting(routing =>
{
// Tier 1: Which message bus?
routing.Transport
.Route<OrderCreatedEvent>().To("rabbitmq")
.Route<AnalyticsEvent>().To("kafka")
.Default("rabbitmq");

// Tier 2: Which services receive this?
routing.Endpoints
.Route<OrderCreatedEvent>()
.To("billing-service", "inventory-service");

routing.Fallback.To("dead-letter-queue");
});

See Message Routing for the full UseRouting() API including endpoint routing and fallback configuration.

Google Pub/Sub Routing​

Google Pub/Sub uses the default name "google-pubsub":

builder.Services.AddGooglePubSubTransport("dispatch-events", pubsub =>
{
pubsub.ProjectId("my-gcp-project")
.TopicId("dispatch-events")
.SubscriptionId("dispatch-events-sub");
});

dispatch.UseRouting(routing =>
{
routing.Transport
.Route<OrderCreatedEvent>().To("google-pubsub")
.Default("rabbitmq");
});

Diagnostics​

var router = serviceProvider.GetRequiredService<IDispatchRouter>();

// Check available routes for a message
var routes = router.GetAvailableRoutes(message, context);

// Check if a destination is configured for a message type
var canRoute = router.CanRouteTo(message, "billing-service");

Next Steps​

See Also​