Skip to content

Direct & Topic Messages and Auto-Binding Guide

This page explains how to use direct messages (RabbitMQ direct exchanges), topic messages (RabbitMQ topic exchanges), and the auto-binding behavior added to consumer definitions.

What problem this solves

Direct/topic exchanges require routing keys. Manually configuring:

  • PublishDirect<T>() / PublishTopic<T>() in every service that publishes/consumes, and
  • endpoint bindings (Bind<T>(routingKey) / BindTopic<T>(pattern))

is easy to forget and easy to misconfigure.


Message attributes

[Direct]

Marks a message as being published to a direct exchange.

C#
1
2
3
4
5
[Direct]
public sealed record ExecuteContribution
{
    public Guid CorrelationId { get; init; }
}

[Topic]

Marks a message as being published to a topic exchange.

C#
1
2
3
4
5
[Topic]
public sealed record AnimalTopicMessage
{
    public Guid CorrelationId { get; init; }
}

[RoutingKey] (optional)

Marks a single property/field used to compute the routing key automatically when publishing.

  • 0 members marked: OK (routing key may be provided manually at publish-time).
  • 1 member marked: OK.
  • 2+ members marked: ❌ configuration should fail (ambiguous).
C#
1
2
3
4
5
6
7
8
[Direct]
public sealed record StartDataLoadingBatch
{
    public Guid CorrelationId { get; init; }

    [RoutingKey]
    public required string EntityWellKnownName { get; init; }
}

Publishing messages

Direct publishing

C#
config.PublishDirect<MyMessage>();                  // direct exchange, no routing key formatter
config.PublishDirect<MyMessage>(ctx => "rk-value"); // direct exchange + routing key formatter

Case A: message has [RoutingKey]
If the message has exactly one [RoutingKey] member, the module auto-configures the formatter and you can just publish:

C#
1
2
3
4
5
await publishEndpoint.Publish(new StartDataLoadingBatch
{
    CorrelationId = NewId.NextGuid(),
    EntityWellKnownName = "landing-request"
});

Case B: message has NO [RoutingKey] (manual routing key)
If the message is [Direct] but has no [RoutingKey], the module configures only PublishDirect<T>(). Provide a key manually when needed:

C#
1
2
3
4
5
6
7
8
9
using MassTransit.RabbitMqTransport;

await publishEndpoint.Publish(new ExecuteContribution
{
    CorrelationId = NewId.NextGuid()
}, ctx =>
{
    ctx.SetRoutingKey("landing-request");
});

Important

If you publish to a direct exchange without a routing key (or with the wrong key), consumers bound to different routing keys will never receive the message.

Topic publishing

C#
config.PublishTopic<MyMessage>();                   // topic exchange, no routing key formatter
config.PublishTopic<MyMessage>(ctx => "rk-value");  // topic exchange + routing key formatter

Topic routing keys are dot-separated words that can be matched by consumer patterns (see below).

If the message has no [RoutingKey], you typically publish with a manual routing key:

C#
1
2
3
4
5
6
7
await publishEndpoint.Publish(new AnimalTopicMessage
{
    CorrelationId = NewId.NextGuid()
}, ctx =>
{
    ctx.SetRoutingKey("quick.orange.rabbit");
});

Consuming messages (auto-binding)

The framework exposes a simple binding API for any consumer whose definition implements AutoBindConsumerDefinition (or any of its derivatives, including ResilientUnitOfWorkConsumerDefinition).

Direct bindings

Override:

C#
protected override void ConfigureDirectRoutingKeys(DirectRoutingKeyMap map)

Bindings use:

  • endpointConfigurator.Bind<T>()
  • endpointConfigurator.Bind<T>(routingKey)

Example: many messages, same routing key

C#
public sealed class Definition(IServiceProvider sp)
    : ResilientUnitOfWorkConsumerDefinition<LandingRequestProjectionConsumer>(sp)
{
    public Definition(IServiceProvider serviceProvider) : base(serviceProvider)
    {
        this.UseUniqueEndpointName();
        var endpointNameFormatter = serviceProvider.GetRequiredService<IEndpointNameFormatter>();
        this.EndpointName = $"{endpointNameFormatter.Consumer<LandingRequestProjectionConsumer>()}-v2";
    }

    protected override void ConfigureDirectRoutingKeys(DirectRoutingKeyMap map)
    {
        const string key = LandingsEntityDescriptionConstants.LandingRequestShipWellKnownName;

        map.Bind<WorkflowGuardStateUpdated>(key);
        map.Bind<ApprovalAdded>(key);
        map.Bind<ApprovalApproved>(key);
        map.Bind<ApprovalRejected>(key);
        map.Bind<ApprovalReset>(key);
    }
}

Topic bindings

Override:

C#
protected override void ConfigureTopicRoutingPatterns(TopicRoutingPatternMap map)

Bindings use:

  • endpointConfigurator.BindTopic<T>(pattern)
  • endpointConfigurator.BindTopic(exchangeName, pattern)

Topic patterns support RabbitMQ wildcards:

  • * matches exactly one word
  • # matches zero or more words

Example: bind a topic message with patterns

C#
public sealed class Definition(IServiceProvider sp)
    : ResilientUnitOfWorkConsumerDefinition<TopicMessagesConsumer>(sp)
{
    protected override void ConfigureTopicRoutingPatterns(TopicRoutingPatternMap map)
    {
        map.Bind<AnimalTopicMessage>(_ => new[]
        {
            "*.orange.*",
            "*.*.rabbit",
            "lazy.#",
        });
    }

    protected override bool StrictTopicBindings => true;
}

Important

Many routing maps store a single resolver per message type. If you call map.Bind(...) multiple times for the same T, the last one may overwrite the others. Prefer providing all keys/patterns in a single call (e.g., returning an array).


How auto-binding behaves

When a consumer consumes at least one routed message ([Direct] or [Topic]):

  • Consume topology is disabled (ConfigureConsumeTopology = false) only if the auto-binding layer can bind something.
  • Non-routed consumed messages are bound using Bind<T>().
  • Direct messages are bound using:
  • Bind<T>(routingKey) when routing keys are configured in ConfigureDirectRoutingKeys.
  • Topic messages are bound using:
  • BindTopic<T>(pattern) when patterns are configured in ConfigureTopicRoutingPatterns.

Backward compatibility

Existing consumers that already manually bind exchanges keep working:

  • If you don’t override ConfigureDirectRoutingKeys / ConfigureTopicRoutingPatterns, auto-binding does nothing.
  • Manual endpointConfigurator.Bind<...>() / BindTopic<...>() calls remain valid.

This allows incremental migration: update consumers only when it adds value.


Direct

  • Prefer [RoutingKey] on direct messages that always route by a well-known key.
  • Use manual routing keys only for “advanced” scenarios.
  • Keep routing keys stable. Treat them like part of the contract.

Topic

  • Use dot-separated routing keys like: domain.entity.event
  • Example: landings.request.submitted
  • Prefer a stable prefix per bounded context (landings.*, rbac.*, etc.).
  • Avoid “random strings” as routing keys: debugging becomes painful fast.

Troubleshooting

Message published but never consumed

Common causes:

  • consumer did not bind the routing key/pattern (Bind<T>(key) / Bind<T>(pattern) missing)
  • routing key does not match the consumer binding (direct: exact match, topic: wildcard rules)
  • message has no [RoutingKey] and publish code forgot to set the routing key manually

Startup fails due to [RoutingKey]

Check:

  • message has exactly one [RoutingKey] member
  • you didn’t annotate both a property and a field