Skip to content

Direct Messages and Auto-Binding Guide

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

What problem this solves

Direct exchanges require routing keys. Manually configuring:

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

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; }
}

[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 direct messages

Direct message publishing is configured using the existing extensions:

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:

  • PublishDirect<T>() and
  • the routing key formatter using that member

So publishing is straightforward:

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 auto-configures only:

  • PublishDirect<T>()

You must provide a routing 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.


Consuming direct messages

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

C#
protected override void ConfigureDirectRoutingKeys(DirectRoutingKeyMap map)

The binding still uses the existing endpoint extensions:

  • 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);
    }

    protected override void ConfigureConsumer(
        IReceiveEndpointConfigurator endpointConfigurator,
        IConsumerConfigurator<LandingRequestProjectionConsumer> consumerConfigurator,
        IRegistrationContext context)
    {
        base.ConfigureConsumer(endpointConfigurator, consumerConfigurator, context);

        if (endpointConfigurator is IRabbitMqReceiveEndpointConfigurator rabbit)
            rabbit.SingleActiveConsumer = true;

        endpointConfigurator.PrefetchCount = 1;
        endpointConfigurator.ConcurrentMessageLimit = 1;
    }
}

Example: routing key computed dynamically (generic consumer)

C#
internal sealed class Definition(IServiceProvider sp)
    : ResilientUnitOfWorkConsumerDefinition<
        StartDataLoadingBatchConsumer<TEntity, TDataEntity, TRequest, TResponse>>(sp)
{
    private readonly IEntityDescriptionProvider entityDescriptionProvider =
        sp.GetRequiredService<IEntityDescriptionProvider>();

    protected override void ConfigureDirectRoutingKeys(DirectRoutingKeyMap map)
    {
        map.Bind<StartDataLoadingBatch>(_ =>
            entityDescriptionProvider.GetEntityDescriptionOrThrow(typeof(TEntity)).WellKnownName);
    }
}

How auto-binding behaves

When a consumer consumes at least one [Direct]:

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

Backward compatibility

Existing consumers that already manually bind exchanges keep working:

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

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


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

Troubleshooting

Message published but never consumed

Common causes:

  • consumer did not bind the routing key (Bind<T>(routingKey) missing)
  • routing key is different from what consumer bound to
  • 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