Skip to content

Content-Based Routing

Content-based routing lets you inspect an incoming message and dispatch it to a different consumer depending on what it contains. This is useful when a single topic carries multiple logical event types, or when you want to shard work across handlers based on a field value.

AddRouter

Call AddRouter on the consumer group instead of AddConsumer:

topic.ConsumerGroup("pizza-kitchen", group =>
{
group.AddRouter(
identifier: "crust-type-router",
selector: ctx => ctx.Message.CrustType,
configure: router =>
{
router.Route<ThinCrustHandler>("thin");
router.Route<DeepDishHandler>("deep-dish");
router.Route<GlutenFreeHandler>("gluten-free");
});
});

The selector receives the full ConsumeContext<TValue> and returns the route key. That key is matched against the registered routes, and the matching consumer is resolved from the DI container and invoked.

Route key types

The route key type is inferred from the selector return type. It must be notnull. Common patterns:

// String discriminator field
selector: ctx => ctx.Message.EventType
// Enum value
selector: ctx => ctx.Message.OrderKind
// Derived value
selector: ctx => ctx.Message.CorrelationId.StartsWith("B2B") ? "b2b" : "b2c"

Per-route middleware

Each route can have its own middleware layer, wrapping only that handler:

router.Route<DeepDishHandler>("deep-dish", route =>
{
route.Use<PriorityTracingMiddleware>();
});

Unmatched messages

If the selector returns a key with no registered route, an UnmatchedRouteException is thrown. Configure how this is handled via the group’s OnError policy:

group.OnError(err =>
{
err.WhenRouteUnmatched(action => action.Discard());
err.Default(action => action
.Retry(3, Backoff.Fixed(TimeSpan.FromSeconds(1)))
.DeadLetter());
});

If the selector returns null, the message is silently skipped.

Accessing the matched route in middleware

After routing, the matched route key is tagged on the current Activity when tracing is enabled:

var routeKey = Activity.Current?.GetTagItem("emit.route.key")?.ToString();

The consumer type name and kind are baked into the tracing middleware at build time and do not need to be read manually.

Router vs. multiple consumer groups

Use a router when all messages arrive on the same topic and you need to dispatch them differently based on content. Use multiple consumer groups when you need independent Kafka offsets: different replay positions, separate lag tracking, or independent scaling.