# Messages
In MassTransit, a message contract is defined code first by creating a .NET type. A message can be defined using a class or an interface, resulting in a strongly-typed contract. Messages should be limited to read-only properties and not include methods or behavior.
Important
MassTransit uses the full type name, including the namespace, for message contracts. When creating the same message type in two separate projects, the namespaces must match or the message will not be consumed.
An example message to update a customer address is shown below.
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; }
}
}
TIP
It is strongly suggested to use interfaces for message contracts, based on experience over several years with varying levels of developer experience. MassTransit will create dynamic interface implementations for the messages, ensuring a clean separation of the message contract from the consumer.
A common mistake when engineers are new to messaging is to create a base class for messages, and try to dispatch that base class in the consumer – including the behavior of the subclass. Ouch. This always leads to pain and suffering, so just say no to base classes.
# 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. Commands are sent (using Send
) to an endpoint, as it is expected that a single service instance performs the command action. A command should never be published.
Commands should be expressed in a verb-noun sequence, following the tell style.
Example Commands:
- UpdateCustomerAddress
- UpgradeCustomerAccount
- SubmitOrder
# Events
An event signifies that something has happened. Events are published (using Publish
) via either IBus
(standalone) or ConsumeContext
(within a message consumer). An event should not be sent directly to an endpoint.
Events should be expressed in a noun-verb (past tense) sequence, indicating that something happened.
Example Events:
- CustomerAddressUpdated
- CustomerAccountUpgraded
- OrderSubmitted, OrderAccepted, OrderRejected, OrderShipped
# Message Headers
MassTransit encapsulates every sent or published message in a message envelope (described by the Envelope Wrapper (opens new window) 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.
# 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.
namespace UsageMessageCorrelation
{
using System;
using System.Threading;
using System.Threading.Tasks;
using UsageContracts;
using MassTransit;
public class Program
{
public static async Task Main()
{
var busControl = Bus.Factory.CreateUsingRabbitMq();
await busControl.StartAsync(new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token);
try
{
var endpoint = await busControl.GetSendEndpoint(new Uri("queue:order-service"));
// Set CorrelationId using SendContext<T>
await endpoint.Send<SubmitOrder>(new { OrderId = InVar.Id }, context =>
context.CorrelationId = context.Message.OrderId);
// Set CorrelationId using initializer header
await endpoint.Send<SubmitOrder>(new
{
OrderId = InVar.Id,
__CorrelationId = InVar.Id
// InVar.Id returns the same value within the message initializer
});
}
finally
{
await busControl.StopAsync();
}
}
}
}
# 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.
namespace UsageMessageSetCorrelation
{
using System;
using UsageContracts;
using MassTransit;
using MassTransit.Context;
using MassTransit.Topology.Topologies;
public class Program
{
public static void Main()
{
// 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.
namespace UsageMessageSendCorrelation
{
using System;
using System.Threading;
using System.Threading.Tasks;
using UsageContracts;
using MassTransit;
public class Program
{
public static async Task Main()
{
var busControl = Bus.Factory.CreateUsingRabbitMq(cfg =>
{
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.
New in Version 7
Previous versions of MassTransit only supported automatic correlation when the message implemented the CorrelatedBy<Guid>
interface. Starting with Version 7, all of the above conventions are used.
# 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 (opens new window) 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 start enjoying the benefits of fast, distributed 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 interfaces and message initializers. Once you adjust it starts to make more sense. Use the Roslyn Analyzer to identify missing or incompatible property initializers.
- Inheritance is okay, but keep it sensible as the type hierarchy will be applied to the broker. 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.
- Consuming a base class type, and expecting polymorphic method behavior almost always leads to problems.
- Message design is not object-oriented design. Messages should contain state, not behavior. Behavior should be in a separate class or service.
- A big base class may cause pain down the road as changes are made, particularly when supporting multiple message versions.