Skip to main content

Message Mapping

When using multiple transports, messages may need transformation between transport-specific formats. Message mapping handles transport-level property translation (headers, routing keys, partition keys) as messages move between different messaging systems.

Before You Start

Core Concepts

ComponentPurpose
IMessageMapperTransforms a message context from one transport format to another
IMessageMapper<TSource, TTarget>Strongly-typed mapper between specific message types
IMessageMapperRegistryRegistry for looking up mappers by source/target transport
IMessageMappingBuilderFluent builder for configuring per-message-type mappings
IOutboxMessageMapperBridges outbox messages to transport-specific contexts

Quick Start

builder.Services.AddDispatch(dispatch =>
{
dispatch.AddHandlersFromAssembly(typeof(Program).Assembly);

// Configure message mapping for multi-transport
dispatch.WithMessageMapping(mapping =>
{
mapping.UseDefaultMappers();
});

dispatch.UseRouting(routing =>
{
routing.Transport
.Route<OrderCreatedEvent>().To("kafka")
.Default("rabbitmq");
});
});

IMessageMapper

The base mapper interface transforms transport message contexts:

public interface IMessageMapper
{
string Name { get; }
string SourceTransport { get; }
string TargetTransport { get; }
bool CanMap(string sourceTransport, string targetTransport);
ITransportMessageContext Map(ITransportMessageContext source, string targetTransportName);
}

IMessageMapper<TSource, TTarget>

The typed mapper transforms message payloads between types:

public interface IMessageMapper<in TSource, out TTarget>
{
TTarget Map(TSource source, ITransportMessageContext context);
}

Use this for converting between internal and external message formats, version upgrades/downgrades, or enriching messages with additional data.

IMessageMapperRegistry

The registry provides lookup by transport combination:

public interface IMessageMapperRegistry
{
void Register(IMessageMapper mapper);
IMessageMapper? GetMapper(string sourceTransport, string targetTransport);
IEnumerable<IMessageMapper> GetAllMappers();
bool HasMapper(string sourceTransport, string targetTransport);
}

Built-In Mappers

Dispatch provides built-in mappers for common transport pairs:

MapperSourceTargetWhat It Maps
RabbitMqToKafkaMapperRabbitMQKafkaRouting keys -> partition keys, AMQP headers -> Kafka headers
KafkaToRabbitMqMapperKafkaRabbitMQPartition keys -> routing keys, Kafka headers -> AMQP headers
DefaultMessageMapperAnyAnyPass-through with basic header mapping

Enable Built-In Mappers

dispatch.WithMessageMapping(mapping =>
{
mapping.UseDefaultMappers(); // Registers RabbitMQ<->Kafka mappers
});

Fluent Mapping Builder

The IMessageMappingBuilder provides per-message-type transport configuration:

dispatch.WithMessageMapping(mapping =>
{
mapping.MapMessage<OrderCreatedEvent>()
.ToRabbitMq(ctx =>
{
ctx.Exchange = "orders";
ctx.RoutingKey = "orders.created";
ctx.DeliveryMode = 2; // persistent
})
.ToKafka(ctx =>
{
ctx.Topic = "orders";
ctx.Key = "order-created";
});
mapping.MapMessage<PaymentProcessedEvent>()
.ToAzureServiceBus(ctx =>
{
ctx.TopicOrQueueName = "payments";
ctx.SessionId = "payment-session";
});
mapping.ConfigureDefaults(defaults => defaults
.ForRabbitMq(ctx => ctx.DeliveryMode = 2)
.ForKafka(ctx => ctx.Topic = "default-topic"));
});

Builder Methods

MethodDescription
MapMessage<T>()Begin mapping configuration for a message type
RegisterMapper(mapper)Register a custom mapper instance
RegisterMapper<T>()Register a custom mapper type via DI
UseDefaultMappers()Register built-in mappers for common transport pairs
ConfigureDefaults(...)Set global default mappings for all message types

Transport-Specific Mapping Contexts

Each transport exposes a context interface for fine-grained control over message properties.

RabbitMQ — IRabbitMqMappingContext

PropertyTypeDescription
Exchangestring?Exchange name
RoutingKeystring?Routing key
Prioritybyte?Message priority (0-255)
ReplyTostring?Reply-to queue name
Expirationstring?Expiration in milliseconds
DeliveryModebyte?1 = non-persistent, 2 = persistent
SetHeader(key, value)methodSet a custom AMQP header

Kafka — IKafkaMappingContext

PropertyTypeDescription
Topicstring?Topic name
Keystring?Message key (partitioning)
Partitionint?Target partition (null = auto)
SchemaIdint?Schema registry ID
SetHeader(key, value)methodSet a custom Kafka header

Azure Service Bus — IAzureServiceBusMappingContext

PropertyTypeDescription
TopicOrQueueNamestring?Topic or queue name
SessionIdstring?Session ID for session-enabled entities
PartitionKeystring?Partition key
ReplyToSessionIdstring?Reply-to session ID
TimeToLiveTimeSpan?Message TTL
ScheduledEnqueueTimeDateTimeOffset?Scheduled delivery time
SetProperty(key, value)methodSet a custom application property

AWS SQS — IAwsSqsMappingContext

PropertyTypeDescription
QueueUrlstring?Queue URL
MessageGroupIdstring?Group ID (FIFO queues)
MessageDeduplicationIdstring?Dedup ID (FIFO queues)
DelaySecondsint?Visibility delay
SetAttribute(name, value, dataType)methodSet a message attribute

AWS SNS — IAwsSnsMappingContext

PropertyTypeDescription
TopicArnstring?Topic ARN
MessageGroupIdstring?Group ID (FIFO topics)
MessageDeduplicationIdstring?Dedup ID (FIFO topics)
Subjectstring?Subject for email endpoints
SetAttribute(name, value, dataType)methodSet a message attribute

Google Pub/Sub — IGooglePubSubMappingContext

PropertyTypeDescription
TopicNamestring?Topic name
OrderingKeystring?Ordering key for ordered delivery
SetAttribute(key, value)methodSet a custom attribute

gRPC — IGrpcMappingContext

PropertyTypeDescription
MethodNamestring?Service method name
DeadlineTimeSpan?Call deadline
SetHeader(key, value)methodSet a custom call header

Custom Transport

For transports not covered by built-in contexts:

.ToTransport("my-custom-transport", ctx =>
{
ctx.SetHeader("custom-key", "custom-value");
})

Custom Mappers

Implement IMessageMapper for custom transport mapping. The easiest approach is to extend DefaultMessageMapper:

public class RabbitMqToServiceBusMapper : DefaultMessageMapper
{
public RabbitMqToServiceBusMapper()
: base("RabbitMqToServiceBus", "rabbitmq", "azureservicebus")
{
}

protected override void CopyTransportProperties(
ITransportMessageContext source,
TransportMessageContext target)
{
base.CopyTransportProperties(source, target);

// Transform RabbitMQ routing key to Service Bus subject header
if (source.Headers.TryGetValue("routing-key", out var routingKey))
{
target.SetHeader("ServiceBus-Subject", routingKey);
}
}
}

Register custom mappers:

dispatch.WithMessageMapping(mapping =>
{
mapping.UseDefaultMappers();
mapping.RegisterMapper(new RabbitMqToServiceBusMapper());
mapping.RegisterMapper<MyOtherMapper>(); // resolved via DI
});

IOutboxMessageMapper

The outbox mapper bridges the outbox pattern with the message mapping system, transforming outbound messages for their target transport:

public interface IOutboxMessageMapper
{
ITransportMessageContext CreateContext(OutboundMessage message, string targetTransport);

ITransportMessageContext MapToTransport(
OutboundMessage message,
ITransportMessageContext sourceContext,
string targetTransport);

IReadOnlyCollection<string> GetTargetTransports(string messageType);
}

How It Works

  1. The outbox processor reads an OutboundMessage from the store
  2. GetTargetTransports() determines which transports should receive the message
  3. CreateContext() builds an initial transport context from the outbound message
  4. MapToTransport() applies configured mappings to produce the final context
  5. The message is published to each target transport with its mapped context

End-to-End Cross-Transport Example

// Configure: OrderCreated goes to both RabbitMQ and Kafka
builder.Services.AddDispatch(dispatch =>
{
dispatch.AddHandlersFromAssembly(typeof(Program).Assembly);

dispatch.WithMessageMapping(mapping =>
{
mapping.MapMessage<OrderCreatedEvent>()
.ToRabbitMq(ctx =>
{
ctx.Exchange = "orders";
ctx.RoutingKey = "orders.created";
ctx.DeliveryMode = 2;
})
.ToKafka(ctx =>
{
ctx.Topic = "orders-events";
ctx.Key = "order-created";
});
});

dispatch.UseRouting(routing =>
{
routing.Transport
.Route<OrderCreatedEvent>().To("rabbitmq");
});
});

When OrderCreatedEvent is dispatched:

  1. Transport routing sends it to both RabbitMQ and Kafka
  2. The RabbitMQ copy gets Exchange = "orders", RoutingKey = "orders.created", persistent delivery
  3. The Kafka copy gets Topic = "orders-events", Key = "order-created"

Message Mapping vs Transport Routing

These are complementary systems:

SystemPurposeAPI
Transport RoutingDetermines which transports receive a messageUseRouting() via routing.Transport
Message MappingTransforms how the message is formatted per transportWithMessageMapping()

Transport routing decides "send this to Kafka"; message mapping ensures the message has the correct Kafka-specific headers and properties.

Next Steps

See Also