Messages
Understand the message envelope, naming conventions, correlation, and message type identity in Mocha. Learn how to define POCO message types, attach custom headers, and access envelope metadata in handlers.
Messages
A message is any C# class, record, or struct. No base class, no marker interface, no framework attributes. You define a type that holds your business data, and Mocha handles everything else - routing, serialization, correlation, and delivery. Records are used throughout the examples because they naturally fit message semantics, but they are not required. Mocha serializes message bodies as JSON by default.
Why envelopes exist
When a message crosses a process boundary, it carries more than your business payload. The receiving service needs to know what type the message is, where it came from, when it was sent, and how it relates to other messages in a workflow. None of this belongs in your domain model.
Mocha solves this with the envelope pattern: every message is wrapped in an envelope containing headers and the serialized body. Your POCO contains business data; the envelope contains infrastructure metadata. Keep them separate.
This separation means your OrderPlaced record stays a clean contract with business properties. The envelope wraps it with everything the infrastructure needs: correlation identifiers, addressing, timestamps, and custom headers. The bus builds the envelope automatically when you publish or send - most of the time you never interact with it directly.
Naming conventions
Convention: Name commands in imperative verb-noun form (
PlaceOrder,ProcessPayment). Name events in past-tense noun-verb form (OrderPlaced,PaymentProcessed). The name communicates intent - commands request action, events announce what happened.
Following this convention makes the intent of each message type clear at a glance and keeps your codebase consistent with the broader .NET messaging ecosystem.
Define and use messages
This section walks through defining a message, attaching custom headers when publishing, and reading envelope metadata inside a handler.
Define a message
Messages can be any class, record, or struct. Records with { get; init; } properties are a natural fit:
// OrderPlaced.csnamespace MyApp;
public sealed record OrderPlaced{ public required Guid OrderId { get; init; } public required string CustomerId { get; init; } public required decimal TotalAmount { get; init; }}
The record holds business data only. No framework types, no base classes, no marker interfaces.
Publish with custom headers
To attach custom metadata, pass PublishOptions with a Headers dictionary:
await bus.PublishAsync( new OrderPlaced { OrderId = Guid.NewGuid(), CustomerId = "CUST-001", TotalAmount = 99.95m }, new PublishOptions { Headers = new() { ["x-tenant"] = "acme", ["x-trace-id"] = "abc-123" } }, CancellationToken.None);
For commands, use SendOptions with the same Headers property:
await bus.SendAsync( new ProcessPayment { OrderId = "ORD-1", Amount = 99.95m }, new SendOptions { Headers = new() { ["x-tenant"] = "acme" } }, CancellationToken.None);
The bus merges your headers into the envelope's header collection before dispatching.
Access envelope metadata in a handler
IEventHandler<T> receives the deserialized message and a cancellation token - that is all. To read message IDs, correlation IDs, timestamps, or custom headers, implement IConsumer<T> instead. The IConsumeContext<T> parameter gives you both the deserialized message and all envelope fields:
// OrderAuditConsumer.csusing Mocha;
namespace MyApp;
public class OrderAuditConsumer(ILogger<OrderAuditConsumer> logger) : IConsumer<OrderPlaced>{ public ValueTask ConsumeAsync( IConsumeContext<OrderPlaced> context, CancellationToken cancellationToken) { // The deserialized message var order = context.Message;
// Envelope metadata - generated by the bus automatically logger.LogInformation( "MessageId={MessageId} CorrelationId={CorrelationId} " + "ConversationId={ConversationId} SentAt={SentAt}", context.MessageId, context.CorrelationId, context.ConversationId, context.SentAt);
// Custom headers you attached when publishing if (context.Headers.TryGetValue("x-tenant", out var tenant)) { logger.LogInformation("Tenant: {Tenant}", tenant); }
return default; }}
Register a consumer with .AddConsumer<T>():
builder.Services .AddMessageBus() .AddConsumer<OrderAuditConsumer>() .AddInMemory();
When the handler processes a published OrderPlaced event, you see output like:
MessageId=3f2a... CorrelationId=7b1c... ConversationId=9d4e... SentAt=2026-02-25T10:30:00ZTenant: acme
The bus generates MessageId, CorrelationId, and ConversationId automatically. Your custom headers appear alongside them.
Tip: Use
IEventHandler<T>when you only need the message payload. Switch toIConsumer<T>when you need envelope metadata or custom headers. Both can coexist for the same message type - Mocha routes to all registered handlers and consumers.
How correlation works
Mocha uses three identifiers to track relationships between messages:
ConversationId ─── groups all messages in a logical conversation │ ├── CorrelationId ─── chains messages in a specific workflow │ │ │ ├── CausationId ─── links parent → child │ │ │ │ │ └── CausationId ─── links parent → child │ │ │ └── CausationId ─── links parent → child │ └── CorrelationId ─── a different workflow in the same conversation
ConversationId is the broadest scope. When you publish the first message in a flow, the bus generates a ConversationId. Every subsequent message in that conversation - across services, across handler chains - inherits the same ConversationId. Use it to find all messages that belong to a single business transaction.
CorrelationId is narrower. It groups messages within a specific workflow or saga instance. A single conversation may contain multiple correlation scopes - for example, an order saga and a payment saga both triggered by the same initial event.
CausationId traces direct causality. When a handler publishes or sends a new message in response to a received message, the bus sets the new message's CausationId to the MessageId of the received message. This creates a parent-child chain you can follow to reconstruct the exact sequence of events.
Together, these three identifiers give you full traceability without adding any fields to your message records.
How message type resolution works
When you register a message type - explicitly with AddMessage<T>() or implicitly by adding a handler - Mocha assigns it a URN-based identity:
urn:message:<namespace>:<type-name>
For example, MyApp.Contracts.OrderPlaced becomes:
urn:message:my-app.contracts:order-placed
The bus stores this identity in the MessageType field of the envelope. On the receiving side, the message type selection middleware matches the incoming URN against the registered types to find the correct CLR type for deserialization.
Use AddMessage<T>() to configure a message type explicitly - for example, to pin its URN when refactoring CLR namespaces, or to configure a send route:
builder.Services .AddMessageBus() .AddMessage<OrderPlaced>(d => { d.Send(route => route.ToQueue("orders-queue")); }) .AddInMemory();
Polymorphic messages. If a message class implements interfaces or extends base classes that are also registered as message types, the envelope carries all of those identities in the EnclosedMessageTypes array. This allows a handler registered for an interface to receive messages that implement it.
Message versioning
Evolving message contracts requires care because producers and consumers may deploy independently. Adding a new init property with a default value is backward-compatible - existing consumers that do not know about the property ignore it during deserialization. Renaming or removing a required property is breaking and requires a coordinated deployment or a versioning strategy.
When you need to refactor a message type's CLR namespace without changing its wire identity, use AddMessage<T>() to pin the URN explicitly. This decouples the type's wire identity from its CLR location, so consumers continue to receive the message under the old URN even after you move or rename the class.
Next steps
Now that you understand message structure, learn the three messaging patterns.
- Messaging Patterns - Pub/sub events, point-to-point commands, and request/reply.
Full demo: Demo.Contracts contains a complete set of message contracts for an e-commerce system - events (
OrderPlacedEvent,PaymentCompletedEvent), send messages (ProcessRefundCommand,ReserveInventoryCommand), and request/reply pairs used by sagas.