Route messages with Service Bus topic filters
With a topic, every subscription gets every message by default. Filters let each consumer receive only what it needs, without the publisher knowing who's listening.
A queue delivers each message to one consumer. A topic delivers each message to every subscription, and each subscription works like its own queue. That's how one OrderPlaced event reaches billing, shipping and analytics independently.
By default, a new subscription receives every message sent to the topic. Often that's not what you want: the EU warehouse only cares about EU orders, and the fraud check only about orders over a certain value. Filtering in each consumer's code works, but it wastes delivery and processing on messages that are thrown away. Subscription filters do it in the broker.
Filters work on properties, not the body
Service Bus never looks inside the message body. Filters evaluate the message's system properties and its application properties, so the publisher sets those for anything subscribers might route on:
var message = new ServiceBusMessage(BinaryData.FromObjectAsJson(orderPlaced))
{
Subject = "OrderPlaced",
MessageId = $"order-placed-{orderPlaced.OrderId}"
};
message.ApplicationProperties["Region"] = orderPlaced.Region; // "EU", "US", ...
message.ApplicationProperties["Total"] = orderPlaced.Total;
await sender.SendMessageAsync(message, ct);
Two kinds of filter
Correlation filters match exact values on properties. They're the most efficient and cover most cases:
await admin.CreateRuleAsync("orders", "eu-warehouse", new CreateRuleOptions("eu-orders",
new CorrelationRuleFilter { Subject = "OrderPlaced", ApplicationProperties = { ["Region"] = "EU" } }));
SQL filters allow conditions with comparisons, AND, OR, IN and more:
await admin.CreateRuleAsync("orders", "fraud-check", new CreateRuleOptions("large-orders",
new SqlRuleFilter("Total > 5000 AND Region IN ('EU', 'US')")));
admin is a ServiceBusAdministrationClient.
Remove the default rule
Every new subscription starts with a rule named $Default that matches everything. A message is delivered if any rule matches, so adding your filter without removing the default changes nothing:
await admin.DeleteRuleAsync("orders", "eu-warehouse", RuleProperties.DefaultRuleName);
Better still, define subscriptions and their rules in your infrastructure code, such as Bicep or Terraform, so they're created correctly from the start and reviewed like any other change.
Design tips
- Decide routing properties up front and treat them as part of the message contract, like the body.
- Prefer correlation filters. Use SQL filters only when you need comparisons or combinations.
- Watch for messages no subscription wants. If no filter matches, the message is simply dropped. That might be fine, or it might hide a bug.
Takeaway
Use a topic when several consumers need the same events, put routing information in application properties, and give each subscription a filter, removing the $Default rule, so each consumer gets only the messages it actually handles.