Messages
A message in the world of MassTransit is simply a piece of data that represents something that needs to happen (a command) or something that has
happened (an event). It’s the fundamental unit of work in a message-driven architecture: a shape of data, serialized (typically as JSON), sent from one service
to another, consumed and acted upon. Behind the scenes, MassTransit wraps that data in a rich envelope that carries metadata (MessageId, CorrelationId,
source, destination, message type, etc.) so you get the robustness of distributed systems without reinventing the plumbing.
While most developers will recognize messages as JSON blobs moving through queues or topics, MassTransit gives them structure, meaning, and version-safe contracts. Under the hood you’ll define your message types as .NET classes, records or interfaces – public, reference types with easily serializable properties. That contract becomes the agreement across producers and consumers: it says, “this is the shape of the data I expect, this is how the message will travel, and this is the behavior I commit to.” In short: you shape the message, you define the contract, and MassTransit handles the delivery mechanics.
Message Names
Section titled “Message Names”There are four main message types: commands, events, requests, and responses. When choosing a name for a message, the type of message should dictate the tense of the message name.
Commands
Section titled “Commands”A command tells a service to do something, and typically a command should only be consumed by a single consumer. If you have a command, such as SubmitOrder,
then you should have only one consumer that implements IConsumer<SubmitOrder> or one saga state machine with the Event<SubmitOrder> configured. By
maintaining the one-to-one relationship of a command to a consumer, commands may be published, and they will be automatically routed to the consumer.
When using RabbitMQ, there is no additional overhead using this approach. However, both Azure Service Bus and Amazon SQS have a more complicated routing
structure, and because of that structure, additional charges may be incurred since messages need to be forwarded from topics to queues. For low- to
medium-volume message loads this isn’t a major concern, but for larger high-volume loads it may be preferable to send (using
Send) commands directly to the queue to reduce latency and cost.
Commands should be expressed in a verb-noun sequence, following the tell style. For example:
- UpdateCustomerAddress
- UpgradeCustomerAccount
- SubmitOrder
Events
Section titled “Events”An event signifies that something has happened. Events are published (using Publish) via either
ConsumeContext (within a message consumer), IPublishEndpoint (within a container scope), or IBus (standalone).
Events should be expressed in a noun-verb (past tense) sequence, indicating that something happened. Some example event names may include:
- CustomerAddressUpdated
- CustomerAccountUpgraded
- OrderSubmitted, OrderAccepted, OrderRejected, OrderShipped
Requests
Section titled “Requests”A request is a message sent from a client to a service, and the service responds with a response message. Requests are typically initiated by a client, and the response is consumed by the client. Requests are typically initiated by a client, and the response is consumed by the client. Requests are typically initiated by a client, and the response is consumed by the client.
- GetCustomerAddress
- GetJobStatus
Responses
Section titled “Responses”Responses are messages sent from a service to a client and typically contain the result of a request. Therefore, responses are typically named after the request they respond to so that developers can easily identify the response message for a given request.
- CustomerAddress
- JobStatus
Of course, this may confuse message and entity types, since the names are likely the same between a persistent date model and the message contract. Some
developers may choose to use a different naming convention, such as GetCustomerAddressResponse or CustomerAddressResponse. They also might name their
request GetCustomerAddressRequest, but I find that super annoying.
Message Headers
Section titled “Message Headers”MassTransit encapsulates every sent or published message in a message envelope (described by the Envelope Wrapper pattern). The envelope adds a series of message headers, including:
| Property | Type | Description |
|---|---|---|
| MessageId | Auto | Generated for each message using NewId.NextGuid. |
| CorrelationId | User | Assigned by the application, or automatically by convention, and should uniquely identify the operation, event, etc. |
| RequestId | Request | Assigned by the request client, and automatically copied by the Respond methods to correlate responses to the original request. |
| InitiatorId | Auto | Assigned when publishing or sending from a consumer, saga, or activity to the value of the CorrelationId on the consumed message. |
| ConversationId | Auto | Assigned when the first message is sent or published and no consumed message is available, ensuring that a set of messages within the same conversation have the same identifier. |
| SourceAddress | Auto | Where the message originated (may be a temporary address for messages published or sent from IBus). |
| DestinationAddress | Auto | Where the message was sent |
| ResponseAddress | Request | Where responses to the request should be sent. If not present, responses are published. |
| FaultAddress | User | Where consumer faults should be sent. If not present, faults are published. |
| ExpirationTime | User | When the message should expire, which may be used by the transport to remove the message if it isn’t consumed by the expiration time. |
| SentTime | Auto | When the message was sent, in UTC. |
| MessageType | Auto | An array of message types, in a MessageUrn format, which can be deserialized. |
| Host | Auto | The host information of the machine that sent or published the message. |
| Headers | User | Additional headers, which can be added by the user, middleware, or diagnostic trace filters. |
Message headers can be read using the ConsumeContext interface and specified using the SendContext interface.
Message Correlation
Section titled “Message Correlation”Messages are usually part of a conversation, and identifiers are used to connect messages to that conversation. In the previous section, the headers supported by MassTransit, including ConversationId, CorrelationId, and InitiatorId, are used to combine separate messages into a conversation. Outbound messages that are published or sent by a consumer will have the same ConversationId as the consumed message. If the consumed message has a CorrelationId, that value will be copied to the InitiatorId. These headers capture the flow of messages involved in the conversation.
CorrelationId may be set, when appropriate, by the developer publishing or sending a message. CorrelationId can be set explicitly on the PublishContext or SendContext or when using a message initializer via the __CorrelationId property. The example below shows how either of these methods can be used.
To set the CorrelationId using the SendContext:
await endpoint.Send<SubmitOrder>(new { OrderId = InVar.Id }, sendContext => sendContext.CorrelationId = context.Message.OrderId);To set the CorrelationId using a message initializer:
await endpoint.Send<SubmitOrder>(new{ OrderId = context.Message.OrderId, __CorrelationId = context.Message.OrderId});Correlation Conventions
Section titled “Correlation Conventions”CorrelationId can also be set by convention. MassTransit includes several conventions by default, which may be used as the source to initialize the CorrelationId header.
- If the message implements the
CorrelatedBy<Guid>interface, which has aGuid CorrelationIdproperty, its value will be used. - If the message has a property named CorrelationId, CommandId, or EventId that is a Guid or Guid?, its value will be used.
- If the developer registered a CorrelationId provider for the message type, it will be used to get the value.
The final convention requires the developer to register a CorrelationId provider prior to bus creation. The convention can be registered two ways, one of which is the new way, and the other which is the original approach that simply calls the new way. An example of the new approach, as well as the previous method, is shown below.
// Use the OrderId as the message CorrelationIdGlobalTopology.Send.UseCorrelationId<SubmitOrder>(x => x.OrderId);
// Previous approach, which now calls the new way aboveMessageCorrelation.UseCorrelationId<SubmitOrder>(x => x.OrderId);The convention can also be specified during bus configuration, as shown. In this case, the convention applies to the configured bus instance. The previous approach was a global configuration shared by all bus instances.
cfg.SendTopology.UseCorrelationId<SubmitOrder>(x => x.OrderId);Registering CorrelationId providers should be done early in the application, prior to bus configuration. An easy approach is putting the registration methods into a class method and calling it during application startup.
Saga Correlation
Section titled “Saga Correlation”Sagas must have a CorrelationId, it is the primary key used by the saga repository and the way messages are correlated to a specific saga instance.
MassTransit follows the conventions above to get the CorrelationId used to create a new or load an existing saga instance. Newly created saga instances
will be assigned the CorrelationId from the initiating message.
Identifiers
Section titled “Identifiers”MassTransit uses and highly encourages the use of Guid identifiers. Distributed systems would crumble using monotonically incrementing identifiers (such as
int or long) due to the bottleneck of locking and incrementing a shared counter. Historically, certain types (okay, we’ll call them out - SQL DBAs) have
argued against using Guid (uniqueidentifier, uuid, etc.) as a key – a clustered primary key in particular. However, with MassTransit, we solved that
problem.
MassTransit uses NewId to generate identifiers that are unique, sequential, and represented as a Guid. The generated identifiers are clustered-index friendly, and are ordered so that SQL Server can efficiently insert them into a database with the unique identifier as the primary key.
To create a Guid, call NewId.NextGuid() where you would otherwise call Guid.NewGuid() and enjoy the benefits of fast, distributed unique identifiers.
Guidance
Section titled “Guidance”When defining message contracts, what follows is general guidance based upon years of using MassTransit combined with continued questions raised by developers new to MassTransit.
Message Inheritance
Section titled “Message Inheritance”This concept comes up often enough that it warrants its own special section. By design, MassTransit treats your classes, records, and interfaces as a “contract.”
For example, say you have a message defined by the .NET class below.
public record SubmitOrder{ public string Sku { get; init; } public int Quantity { get; init; }}You want all of your messages to have a common set of properties, so you try and do this.
public record CoreEvent{ public string User { get; init; }}
public record SubmitOrder : CoreEvent{ public string Sku { get; init; } public int Quantity { get; init; }}If you try and consume a Batch<CoreEvent> and expect to get a variety of types, one of which would be SubmitOrder. In OOP land, that makes all the sense in
the world, but in MassTransit contract design it does not. The application has said that it cares about batches of CoreEvent so it will only get back the
single property User. This is not a symptom of using System.Text.Json, this has been the standard behavior of MassTransit since day one, even when using
NewtonSoft.JSON. MassTransit will always respect the contract that has been designed.
If you want to have a standard set of properties available, by all means use a base class or bundle them up into a single property, our preference. If you want to subscribe to all implementations of a class, then you will need to subscribe to all implementations of a class.