Messaging for .NET, within and between services.
Mocha sends commands and events between your services, for example telling the shipping service that an order was placed. It also runs commands and queries inside a single service, with no broker involved. Define messages and handlers in C#, and Mocha generates the handler registration at build time.
Answer the caller now. Finish the rest in the background.
When a customer checks out, the order service can confirm the order right away. Billing, inventory, shipping, and search keep working in the background, each at its own pace. Mocha gives you two ways to do this: send a message you don't need to wait for, or send a request and wait for the reply.
Start with a message and a handler.
Messages in Mocha are represented as C# records and processed by handler classes. Define the message, implement its handler interface, and Mocha takes care of registering the handler at build time.
Its analyzers also detect invalid or duplicate handlers before the application starts.
You write the code. Mocha wires up the broker.
Mocha can automatically configure the messaging infrastructure your services need. Based on the messages they send and receive, it creates the appropriate routes and transport resources. This configuration is called the topology.
Mocha sets up the topology at startup, so naming and configuration conflicts surface early. You can also define the transport configuration yourself when you need more control.
builder.Services
.AddMessageBus()
.AddOrderService()
.AddRabbitMQ();
// exchanges, queues, and bindings are derived
// from your handlers, validated at startupMocha is also a mediator.
Mocha does more than move messages between services. Its mediator dispatches commands and queries inside your own process, with no broker involved. It builds the dispatch pipeline at build time and runs your middleware around each handler, so the hot path avoids reflection.
public record PlaceOrderCommand(
Guid ProductId, int Quantity)
: ICommand<PlaceOrderResult>;
var result = await sender.SendAsync(
new PlaceOrderCommand(productId, 2), ct);Publish an event. Each subscriber handles it at its own pace.
Each subscriber gets its own durable queue, set up before you start publishing. From there, it processes events at its own pace and picks up where it left off after a restart. How reliably messages are delivered, and how long they're kept, depends on the transport (in-memory, broker-backed, or database-backed) and the topology you configure.
await bus.PublishAsync(orderPlaced, ct);Hand off a command without waiting for the handler.
Use a command when one service needs to do work without making the caller wait for it to finish. Mocha routes the command to its handler. You choose the transport and reliability settings that fit the job.
await bus.SendAsync(
new ReserveInventoryCommand(orderId), ct);Wait for a typed response from another service.
Use request/reply when a caller needs an answer from one service and can wait for it. Mocha matches the reply to the original request and returns a typed response, or times out if none arrives. If the handler fails, it surfaces the error through the same reply channel.
var product = await bus.RequestAsync(
new GetProductRequest(id), ct);Process messages in batches.
When several messages can be processed together, batching can reduce the amount of work your application has to do. Mocha collects messages until the batch reaches a configured size or a timeout expires, then passes them to the handler in a single call.
This is especially useful for bulk operations, such as writing many records to a database at once instead of issuing a separate request for each message.
.AddBatchHandler<OrderPlacedBatchHandler>(
o => o.MaxBatchSize = 100);Publish a message at a time you choose.
Sometimes you don't want to process a message immediately. Mocha lets you schedule a message for delivery at a specific time or after a delay, making it useful for reminders, delayed retries, and other deferred work.
Whether a scheduled message survives restarts or can be cancelled depends on the scheduling store that holds it until it is due.
var result = await bus.SchedulePublishAsync(
new SendWelcomeEmail(userId),
DateTimeOffset.UtcNow.AddMinutes(30), ct);Avoid duplicate writes when a broker redelivers a message.
Message brokers may deliver the same message more than once, such as after a crash or a lost acknowledgment. Mocha prevents those redeliveries from repeating the same work by recording which messages have already been processed.
The inbox and the handler's changes are committed together, while any outgoing messages are held until that transaction succeeds. For database operations, this provides effectively exactly-once processing for as long as the inbox record is retained.
Keep long-running workflows in one state machine.
Some workflows cannot be completed by a single message. They unfold over several steps and may need to react to events, failures, or timeouts along the way.
A saga keeps track of that progress and determines what should happen next. Its state can be stored in memory or persisted when the workflow needs to survive restarts.
If part of the workflow fails, the saga can trigger compensating actions to undo earlier steps. Timeout handling requires a scheduling store that supports delayed messages.
See where a message went and what handled it.
When a message passes through several services, it can be difficult to see where time was spent or where something went wrong. Mocha integrates with OpenTelemetry to connect dispatch, receive, and handler activity into a single trace that follows the message across the system.
Those traces can be sent to Nitro, where you can inspect slow handlers, failed messages, and delays between services.
One message, end to end
TRACE · 7f3a·9b2eCorrelated across the gap
OTELpublish -> consume, end to end
delivery latency
Separate application logic from infrastructure.
Your messaging infrastructure will often change as your application grows. You might start with an in-memory transport during development, move to a database-backed option for simpler deployments, or use a message broker in a distributed system.
Mocha keeps those infrastructure choices out of your handlers. The same message-handling code works across transports, while each transport provides its own delivery guarantees, durability, scheduling support, and routing model.
builder.Services
.AddMessageBus()
.AddOrderService() // source-generated
.AddRabbitMQ(t => t.IsDefaultTransport())
.AddEventHub(t =>
t.Handler<DeviceTelemetryHandler>());Publish your first message with Mocha.
You can start with a single process using the in-memory transport, a message, and a handler. As your application grows, switching to a broker-backed or database-backed transport doesn't require changing your handlers. You simply gain the delivery, durability, and reliability features your application needs.