Messages
In MassTransit, a message contract is defined code first by creating a .NET type. A message can be defined using a record, class, or interface. Messages should only consist of properties, methods and other behavior should not be included.
The message examples below show the same command to update a customer address using each of the supported contract types.
Message Types
Messages must be reference types, and can be defined using records, interfaces, or classes.
Records
namespace Company.Application.Contracts
{
using System;
public record UpdateCustomerAddress
{
public Guid CommandId { get; init; }
public DateTime Timestamp { get; init; }
public string CustomerId { get; init; }
public string HouseNumber { get; init; }
public string Street { get; init; }
public string City { get; init; }
public string State { get; init; }
public string PostalCode { get; init; }
}
}
Interfaces
namespace Company.Application.Contracts
{
using System;
public interface UpdateCustomerAddress
{
Guid CommandId { get; }
DateTime Timestamp { get; }
string CustomerId { get; }
string HouseNumber { get; }
string Street { get; }
string City { get; }
string State { get; }
string PostalCode { get; }
}
}
When defining a message type using an interface, MassTransit will create a dynamic class implementing the interface for serialization, allowing the interface with get-only properties to be presented to the consumer. To create an interface message, use a message initializer.
Classes
namespace Company.Application.Contracts
{
using System;
public class UpdateCustomerAddress
{
public Guid CommandId { get; set; }
public DateTime Timestamp { get; set; }
public string CustomerId { get; set; }
public string HouseNumber { get; set; }
public string Street { get; set; }
public string City { get; set; }
public string State { get; set; }
public string PostalCode { get; set; }
}
}
Properties with
private set;
are not recommended as they are not serialized by default when usingSystem.Text.Json
.
Message Attributes
Attribute | Description |
---|---|
EntityName | The exchange or topic name |
ExcludeFromTopology | Don't create an exchange or topic unless it is directly consumed or published |
ExcludeFromImplementedTypes | Don't create a middleware filter for the message type |
MessageUrn | The message urn |
Message Names
There are two main message types, events and commands. When choosing a name for a message, the type of message should dictate the tense of the message.
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
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
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
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
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 CorrelationId
property, 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 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 CorrelationId
GlobalTopology.Send.UseCorrelationId<SubmitOrder>(x => x.OrderId);
// Previous approach, which now calls the new way above
MessageCorrelation.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
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 obtain 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
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 (or, their term, uniqueidentifier) 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 uniqueidentifier 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
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.
- Use records, define properties as
public
and specify{ get; init; }
accessors. Create messages using the constructor/object initializer or a message initializer. - Use interfaces, specify only
{ get; }
accessors. Create messages using message initializers and use the Roslyn Analyzer to identify missing or incompatible properties. - Limit the use of inheritance, pay attention to polymorphic message routing. A message type containing a dozen interfaces is a bit annoying to untangle if you need to delve deep into message routing to troubleshoot an issue.
- Class inheritance has the same guidance as interfaces, but with more caution.
- Message design is not object-oriented design. Messages should contain state, not behavior. Behavior should be in a separate class or service.
- Consuming a base class type, and expecting polymorphic method behavior almost always leads to problems.
- A big base class may cause pain down the road as changes are made, particularly when supporting multiple message versions.
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".
An example, let's say that you have a message that is defined by the dotnet 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 Netwonsoft.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 class, then you will need to subscribe to all implementations of a class.