Azure Service Bus Transport Upgrade Version 4 to 5

Upgrading Azure Service Bus transport from version 4 to version 5 is a major upgrade and requires careful planning. Read the entire upgrade guide before beginning the upgrade process.

Choosing a migration path

Version 5 offers two paths to the topic-per-event topology. The right choice depends on how many events you handle and whether your namespace is under entity-quota pressure.

  • Migrate event by event. Use the migration topology and follow the migration steps to move one event at a time while staying backward compatible with endpoints still on the previous single-topic topology. This path provisions one topic per migrated event. Choose it when you can afford a topic for each event type.
  • Skip the migration topology. Go straight to TopicTopology.Default and point a fallback topic at your existing single topic. Unmapped events keep flowing through the shared topic, and you carve out dedicated topics only where you need them. This avoids per-event mapping work and keeps topic count low. It requires version 6.4 or later of the transport, so it may mean migrating from version 4 directly to version 6 rather than to version 5. Choose it when the namespace is near its entity cap or most events are low-volume. See Blending topic-per-event with a fallback topic.

Both paths end at the topic-per-event topology. They differ in how much you do during the migration and how soon you spend entity headroom.

Polymorphic dispatch

Polymorphic dispatch using message type inheritance no longer auto subscribes to all descendant types. Polymorphic dispatch is still supported but requires that each descendant type in the inheritance tree is configured with an explicit type subscription on the topology object.

Topologies

Version 5 of the transport introduces the concept of choosing a topic topology. The following two topologies are supported:

  • Migration topology
  • Topic-per-event type topology

The topology selection must be explicitly passed into the constructor of the transport when the transport is being created.

Topic-per-event type topology

This topology is the default and preferred choice for new endpoints that do not require backward compatibility with previous versions of the transport. It optimizes event routing, reduces filter overhead, aligns with industry best practices, and improves observability in the event routing path.

The topology represents each event as its own topic. Subscribers add their subscription with forwarding enabled under the topic of the events they are interested in. These subscriptions do not require any filtering rules.

flowchart TD; Orders -->|Publishes|OrderAccepted; Shipping --> ShippingSubscription -->|Subscribes| OrderAccepted; Shipping -->|ForwardsTo| ShippingQueue; Sales --> SalesSubscription -->|Subscribes| OrderAccepted; Sales -->|ForwardsTo| SalesQueue;

Least-privilege

Subscribing and unsubscribing to events requires management rights to the Azure Service Bus namespace because subscriptions need to be created or deleted. It is possible to run the transport with least-privilege access by deploying the necessary subscriptions as part of the endpoint deployment. This can be done by briefly enabling installers, using the provided tool, or utilizing infrastructure-as-code tools such as Bicep, Terraform, or Pulumi.

Migration topology

The migration topology is a hybrid design that allows transitioning from the previously used topology to the topic-per-event-type topology on an event-by-event basis, avoiding the need for a big-bang migration process.

The migration topology should be used by endpoints that require backward compatibility with endpoints using the previous topology.

In this topology, each event type must be explicitly mapped as either "to be migrated" or "migrated". Events yet to be migrated are published or subscribed in the backward-compatible way, while migrated events follow the topic-per-event-type topology.

Least-privilege

Subscribing and unsubscribing to a "to be migrated" event at runtime is supported even when connected endpoints do not have management rights to the Azure Service Bus namespace. This ensures that the migration topology remains backward-compatible from a privilege mode perspective.

For migrated events, subscribing and unsubscribing requires management rights since subscriptions need to be created or deleted. It is possible to run the transport with least-privilege access by deploying the necessary subscriptions during the endpoint deployment using the provided tool or infrastructure-as-code tools such as Bicep, Terraform, or Pulumi.

Migrating existing endpoints

While it is possible to migrate events individually, it is currently not supported to partially migrate a delivery path of a single event. This means that when an event is migrated, both the publisher and all the subscribers must migrate in one step. If a partial migration approach is necessary, be sure to reach out to support.

The following endpoint configuration snippets demonstrate how a migration could take place, assuming the following scenario:

  • Publisher1 publishes Event1 which is subscribed by Subscriber1 and Subscriber2
  • Publisher1 also publishes Event2 which is subscribed by Subscriber1 and Subscriber3
  • Publisher2 publishes Event3 which is subscribed by Subscriber3 and Subscriber4

To use the migration topology, both publishers and subscribers must be on NServiceBus 9 or higher. It is not required to upgrade every endpoint to the new version of the transport as long as events are correctly mapped to be published or subscribed in a backward-compatible way where necessary.

For example, if Subscriber4 cannot be upgraded to a newer version of NServiceBus and the transport, Publisher2 can either:

  • Stay on the older version of the transport.
  • Upgrade but explicitly mark Event3 to be published in a backward-compatible way:
var topology = TopicTopology.MigrateFromSingleDefaultTopic();
// Publishes and/or subscribes using the “old” single-topic (here bundle-1) approach.
topology.EventToMigrate<Event3>();

If Subscriber3 is upgraded to the new version of the transport, it must map Event2 and Event3, while Event3 may need to remain marked as "to be migrated" until Subscriber4 can be upgraded:

var topology = TopicTopology.MigrateFromSingleDefaultTopic();
topology.EventToMigrate<Event2>();
topology.EventToMigrate<Event3>();

and the Publisher1 configuration

var topology = TopicTopology.MigrateFromSingleDefaultTopic();
topology.EventToMigrate<Event1>();
topology.EventToMigrate<Event2>();

assuming Subscriber1 and Subscriber2 can be migrated the Publisher1 configuration could be switched to

var topology = TopicTopology.MigrateFromSingleDefaultTopic();
// Publishes this event using the new “topic per event” approach (here to a Namespace.Event1 topic).
topology.MigratedPublishedEvent<Event1>();
topology.EventToMigrate<Event2>();

the Subscriber1 configuration

var topology = TopicTopology.MigrateFromSingleDefaultTopic();
topology.MigratedSubscribedEvent<Event1>();
topology.EventToMigrate<Event2>();

and the Subscriber2 configuration

var topology = TopicTopology.MigrateFromSingleDefaultTopic();
// Subscribes to this event using the new “topic per event” approach (here to a Namespace.Event1 topic)
topology.MigratedSubscribedEvent<Event1>();

or directly using the topic per event type topology since it only ever subscribes to Event1 which is only published in the new way.

var topology = TopicTopology.Default;

The Subscriber3 configuration would for a period of time look like

var topology = TopicTopology.MigrateFromSingleDefaultTopic();
topology.EventToMigrate<Event2>();
topology.EventToMigrate<Event3>();

until Publisher1 switches Event2 to be published in the new way

var topology = TopicTopology.MigrateFromSingleDefaultTopic();
topology.MigratedPublishedEvent<Event1>();
topology.MigratedPublishedEvent<Event2>();

or directly using the topic per event type topology since it only ever publishes to Event1 and Event2 now in the new way.

var topology = TopicTopology.Default;

Order of migration

Generally, it does not matter whether the publisher or the subscriber is upgraded first, as long as the migration topology settings align with the subscribers' requirements. If a publisher is upgraded before all subscribers, it must be configured to publish events in a backward-compatible way. If the subscribers are upgraded first, they must subscribe to events in a backward-compatible way.

Switching the event delivery path to the new topic-per-event-type approach is a two-step process.

First, ensure that the infrastructure for the event delivery (topic and all subscriptions) is created. This can be done in a number of ways:

  • If endpoints have installers enabled, the subscribers can be restarted after the event is marked as "migrated" in the topology configuration. This means that during the startup process the necessary infrastructure for the event is created. The old infrastructure (subscription on the common topic) still exists and is being used to deliver the events
  • Using the provided tool
  • Using infrastructure-as-code tools such as Bicep, Terraform, or Pulumi

The second step is to mark the event as "migrated" in the publisher configuration and re-deploy the endpoint.

To reduce CPU and memory overhead, subscriber endpoints should disable the AutoSubscribe feature for the specific event to prevent unnecessary old subscriptions or the deletion of no-longer-used filter rules.

All endpoints using TopicTopology.Default can be considered fully migrated.

Version 6.4 enhancements

When upgrading to version 6.4 or later of the transport, additional capabilities become available on the topic-per-event topology:

Fallback topic

The fallback topic provides a catch-all destination for events that are not explicitly mapped. This simplifies polymorphic dispatch scenarios and reduces the number of explicit mappings required when subscribing through base contracts or interfaces.

Built-in filtering modes

Topic routing modes provide built-in support for CorrelationFilter and SqlLikeFilter on multiplexed topics, eliminating the need for manual OutgoingNativeMessageCustomization when selective consumption is required.

Cleanup of no longer used entities on Azure Service Bus

If the migration takes a long time, it may be desirable to delete old subscriptions or rules that are no longer needed to reduce CPU and memory overhead on the single topic still used by some endpoints.

Once all events have been migrated, the old single topic can be deleted.

Migrating from non-default topics or hierarchies

Use either TopicTopology.MigrateFromNamedSingleTopic(string topicName) or TopicTopology.MigrateFromTopicHierarchy(string topicToPublishTo, string topicToSubscribeOn).

The default topic name is bundle-1. In case that one is used create the migration topology with TopicTopology.MigrateFromSingleDefaultTopic().

Migrating subscription name customizations

Previous versions of the transport allowed mapping from queue names to subscription names using function delegates. While flexible, this approach made it difficult to store logic in application configuration.

//Use the first 9 characters so QueueNameThatIsLongerThanFiftyCharactersAndStillValid becomes QueueName
transport.SubscriptionNamingConvention(x => x.Substring(0, 9));

Starting with v5 of the transport, subscription names can be assigned directly:

topology.OverrideSubscriptionNameFor("QueueNameThatIsLongerThanFiftyCharactersAndStillValid", "QueueName");

For more advanced scenarios, mappings can be stored in configuration:

{
  ...
  "QueueNameToSubscriptionNameMap": {
    "QueueNameThatIsLongerThanFiftyCharactersAndStillValid": "QueueName"
  }
}

The assumption is that any previous delegate invocation would needed to be idempotent to create reliable runtime behavior. Subscription names must adhere to the limits outlined in the Microsoft documentation on subscription creation and are automatically validated during startup.

For example, if previously an MD5 hash was used as the sanitization function it might be required to preserve the same entity names. For queue names that crossed the threshold of 50 characters, it would be necessary to precalculate the MD5 hash and store that as the subscription name. Alternatively simply configure the subscription name already used in production as a hardcoded value. Below is the MD5 hash as a GUID for a queue name called QueueNameThatIsLongerThanFiftyCharactersAndStillValid:

topology.OverrideSubscriptionNameFor("QueueNameThatIsLongerThanFiftyCharactersAndStillValid", "7b7139c2-dd0e-2870-424a-891c84f89477")

the hash was calculated assuming the known ValidateAndHashIfNeeded strategy.

static string HashName(string input)
{
    var inputBytes = Encoding.Default.GetBytes(input);
    var hashBytes = MD5.HashData(inputBytes);
    return new Guid(hashBytes).ToString();
}

Migrating rule name customizations

Previously, rule names could be assigned using function delegates

transport.SubscriptionRuleNamingConvention(x => "MyPrefix-"+x);

Starting with v5, rule names can be mapped directly:

topology.EventToMigrate<MyEvent>("MyPrefix-MyEvent")

Or via configuration:

{
  "$type": "migration-topology-options",
  ...
  "EventsToMigrateMap": [
    "Namespace.Subnamespace.VeryLongEventName1"
  ],
  "SubscribedEventToRuleNameMap": {
    "Namespace.Subnamespace.VeryLongEventName1": "MyRuleName"
  }
}

The assumption is that any previous delegate invocation would needed to be idempotent to create reliable runtime behavior. Rules names must adhere to the limits outlined in the Microsoft documentation on subscription creation and are automatically validated during startup.

For example, if previously an MD5 hash was used as the sanitization function it might be required to preserve the same entity names. For rule names that crossed the threshold of 50 characters, it would then be necessary to precalculate the MD5 hash and store that as the rule name. Alternatively simply configure the rule name already used in production as a hardcoded value. Below is the MD5 hash as a GUID for a rule name called Namespace.Subnamespace.VeryLongEventName1:

topology.EventToMigrate<Namespace.Subnamespace.VeryLongEventName1>("76b98b1a-3a59-490a-a064-de65c0bc9aa6")

the hash was calculated assuming the previously known ValidateAndHashIfNeeded strategy.

static string HashName(string input)
{
    var inputBytes = Encoding.Default.GetBytes(input);
    var hashBytes = MD5.HashData(inputBytes);
    return new Guid(hashBytes).ToString();
}

Blending topic-per-event with a fallback topic

A full topic-per-event migration provisions one topic per migrated event. For a namespace near its entity cap (for example, a single messaging-unit Premium namespace limited to 1,000 entities with several hundred event types), that migration can spend the headroom it was meant to preserve.

The blended topology uses the fallback topic and routing modes, which require version 6.4 or later of the transport (NServiceBus 10). This guide covers the version 4 to 5 upgrade, and the recommended approach is to upgrade one major version at a time. For a namespace near its entity cap, though, the per-event headroom cost of a full migration may make the intermediate step unattractive. In that case, migrate directly from version 4 to version 6, skipping the intermediate major, and apply the blended topology from the start. If the endpoint has already been upgraded to version 5, the same approach is available once it continues to version 6.4 or later.

With the blended topology, you skip the migration topology and move straight to TopicTopology.Default, pointing the fallback topic at your existing single topic (for example bundle-1). Unmapped events keep flowing through the shared topic, and only the events you explicitly map get dedicated topics.

var topology = TopicTopology.Default;

// Route the long tail of low-volume events through the existing shared topic.
// SqlLikeFilter matches the EnclosedMessageTypes header every publisher sets,
// so it stays compatible with existing SQL-filter subscriptions on that topic.
topology.UseFallbackTopic("bundle-1", TopicRoutingMode.SqlLikeFilter);

// Carve out dedicated topics only where volume or fan-out warrants it.
topology.PublishTo<OrderAccepted>("Shipping.OrderAccepted");

This "inverse" approach has a few practical advantages during a migration:

  • You avoid writing EventToMigrate and MigratedX mappings for every event.
  • Topic count grows with the few events you carve out, not with your total event-type count.
  • The existing shared topic keeps carrying traffic, so there is no drain window before you switch.

When to choose this path

  • The namespace is near its entity cap and pure topic-per-event would exceed it.
  • Most event types are low-volume and a shared topic is acceptable for them.
  • Only a subset of events justify dedicated topics for isolation or scale.

In short, choose this path when the per-event mapping and sequencing effort of the migration topology would buy little because few events actually benefit from a dedicated topic.

Trade-offs

  • Rule count is the dominant cost, not filter type. Every message published to a shared topic is evaluated against every rule on every subscription. Event inheritance can quietly multiply rules: a concrete type plus its interfaces can mean several rules per subscription per event. Treat rule count as an operational metric: subscriptions per topic, rules per subscription, and rules compared per published message. Watch topic depth and CPU as event hierarchy depth grows.
  • Start with SqlLikeFilter; postpone CorrelationFilter unless you need it. SqlLikeFilter matches on the always-present EnclosedMessageTypes header, so new subscriptions coexist with existing single-topic endpoints without extra coordination, the least-effort choice during a migration. CorrelationFilter is cheaper per evaluation and has a higher quota (roughly 100,000 correlation-filter rules versus about 2,000 SQL filter rules per topic), but it requires every publisher on the shared topic to stamp the correlation properties, and a large number of correlation rules still pressures the topic. The quota is the largest supported configuration, not its runtime cost. Pick the cheapest filter that expresses the condition, keep rule count low, and reach for CorrelationFilter later only if rule count or per-evaluation cost becomes a real problem.
  • Keep the shared topic on the low-volume tail. Carve high-volume or high-fan-out events onto dedicated topics, where the hot path has no filter evaluation, and reserve the fallback topic for the low-volume long tail so the per-message routing cost is amortized over low volume.
  • Fallback mode is a global contract. All publishers and subscribers must agree on the fallback topic name and mode. Changing the mode retroactively changes the effective routing of every event riding the fallback.
  • Shared-topic limits still apply. The fallback topic shares a single quota (5 GB default, up to 80 GB on Premium) and a single failure domain, and monitoring granularity is coarser for the events that share it.

List of Samples

Related Articles