C# – MassTransit

By | 18/03/2026

In this post, we will see what MassTransit is and when and how we can use it in our projects.

When building modern distributed systems, one of the most critical challenges we face is reliable communication between services. While we could implement ‘message based’ communication manually, managing message serialization, retry logic, error handling, and distributed transactions quickly becomes overwhelming.
For these complex scenarios, MassTransit provides a fully featured abstraction layer.
MassTransit is an open-source .NET library that sits on top of message brokers (Azure Service Bus, RabbitMQ, ActiveMQ, Amazon SQS, etc.) and gives us a consistent programming model for messaging: publish/subscribe, send/receive, consumers, retries, delayed redelivery, fault messages, correlation, request/response, and long-running workflows (sagas).
Instead of wiring those patterns manually (and differently) for each broker, we get one abstraction and a set of proven defaults.
MassTransit is not “a broker.”
It doesn’t replace Azure Service Bus or RabbitMQ.
It’s the layer that helps us use a broker correctly and consistently in .NET, so we spend less time reinventing messaging infrastructure and more time on business logic.”


WHEN SHOULD WE USE MASSTRANSIT?
The decision to introduce MassTransit into our architecture should be driven by specific needs rather than trends.
Consider MassTransit when we’re building systems that require asynchronous processing, where immediate responses aren’t necessary and we can benefit from decoupling.
For example, when a user uploads a document that needs processing, or when an order is placed and needs to trigger inventory updates, email notifications, and payment processing.

MassTransit becomes particularly valuable in microservices architectures where services need to communicate without tight coupling.
If Service A needs to notify Service B about an event without knowing Service B’s implementation details or even whether Service B is currently available, message-based communication with MassTransit provides that flexibility.

Another strong use case is when we need reliable background job processing.
Rather than using simple background tasks that might fail silently, MassTransit gives us automatic retries, error queues, and the ability to monitor message flow through your system. When we need to handle traffic spikes by queuing requests for later processing, or when we want to implement the saga pattern for managing long-running business processes, MassTransit provides battle-tested implementations.

We should strongly consider MassTransit when we recognize one (or more) of these situations:

  • We have multiple producers and consumers and we want a clear contract first approach. MassTransit nudges us toward message contracts, consumers, and explicit boundaries, which scales well across teams and services.
  • We need resiliency patterns beyond “try/catch and abandon.” MassTransit gives us configurable retries, delayed redelivery, and fault events, so failure handling is systematic rather than ad-hoc.
  • We want observability and correlation by design. MassTransit propagates correlation IDs and includes conventions around headers and faults that make tracing flows much easier.
  • We anticipate broker portability, or we’re integrating different brokers across environments. With MassTransit, switching transports is often configuration work rather than a rewrite.
  • We need richer messaging features like request/response, scheduling, or sagas (state machines). We can implement these with raw broker SDKs, but MassTransit tends to reduce boilerplate and footguns.

When it’s probably overkill: if we have a tiny app with one queue and one consumer and no need for advanced behaviors, the broker SDK might be enough.
Adding MassTransit is still fine, but we’re paying some conceptual overhead for capabilities we may not use yet.


Let’s see a practical example with RabbitMQ but first, we’ll need RabbitMQ running locally.
Here’s a Docker Compose script that sets up RabbitMQ with the management interface:

version: '3.8'

services:
  rabbitmq:
    image: rabbitmq:3-management
    container_name: rabbitmq
    ports:
      - "5672:5672"   # AMQP port
      - "15672:15672" # Management UI
    environment:
      RABBITMQ_DEFAULT_USER: guest
      RABBITMQ_DEFAULT_PASS: guest
    volumes:
      - rabbitmq_data:/var/lib/rabbitmq

volumes:
  rabbitmq_data:

We save this as docker-compose.yml and run:

docker-compose up -d


Now, we can access the RabbitMQ management interface at http://localhost:15672 with username and password both set to “guest”:

Finally, before starting to develop, we must remember to install these two libraries:

dotnet add package MassTransit
dotnet add package MassTransit.RabbitMQ


In this example, we’ll use RabbitMQ as our message broker (running locally in Docker) and MassTransit as the .NET messaging abstraction. The API will publish an ‘OrderSubmitted’ event every time an order is submitted. A separate consumer (running in the same application for simplicity) listens on a queue and processes those events asynchronously.
The key idea is decoupling: the API can return immediately (fast and reliable), while the consumer performs the heavier work in the background. If the consumer fails, MassTransit can retry automatically, and RabbitMQ will buffer messages until the consumer is available again. This approach is a common building block for ‘event driven’ architectures and microservices.

[MESSAGE CONTRACT]

namespace Contracts;

public record OrderSubmitted
{
    public Guid OrderId { get; init; }
    public string CustomerId { get; init; } = string.Empty;
    public decimal Total { get; init; }
    public DateTime SubmittedAtUtc { get; init; }
}

[CONSUMER]

using MassTransit;

namespace TestMassTransit.Consumers;

// Consumer = message handler.
// MassTransit will create a queue endpoint for this consumer (configured in Program.cs).
public class OrderSubmittedConsumer(ILogger<OrderSubmittedConsumer> logger) : IConsumer<OrderSubmitted>
{
    public async Task Consume(ConsumeContext<OrderSubmitted> context)
    {
        // ConsumeContext contains headers, correlation ids, and metadata besides the message itself.
        var msg = context.Message;

        logger.LogInformation(
            "Processing OrderSubmitted. OrderId={OrderId}, CustomerId={CustomerId}, Total={Total}, MessageId={MessageId}",
            msg.OrderId, msg.CustomerId, msg.Total, context.MessageId);

        // Simulate some work (examples: calling a payment provider, writing to DB, etc.)
        await Task.Delay(300);
    }
}

[API ENDPOINTS THAT PUBLISHES]

using MassTransit;
using Microsoft.AspNetCore.Mvc;

namespace TestMassTransit.Controllers;

[ApiController]
[Route("api/orders")]
public class OrdersController(IPublishEndpoint publishEndpoint) : ControllerBase
{
    [HttpPost("{orderId:guid}/submit")]
    public async Task<IActionResult> Submit(Guid orderId)
    {
        // Publish = event/fan-out semantics.
        // Any number of consumers can subscribe to this message type.
        await publishEndpoint.Publish(new OrderSubmitted
        {
            OrderId = orderId,
            CustomerId = "CUST-123",
            Total = 199.99m,
            SubmittedAtUtc = DateTime.UtcNow
        });

        // API returns immediately; processing happens asynchronously in the consumer.
        return Accepted(new { orderId });
    }
}

[MASSTRANSIT + RABBITMQ CONFIGURATION IN PROGRAM.CS]

using MassTransit;
using TestMassTransit.Consumers;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();

// Register MassTransit in DI
builder.Services.AddMassTransit(x =>
{
    // Register consumer in MassTransit container
    x.AddConsumer<OrderSubmittedConsumer>();

    // Configure RabbitMQ as transport
    x.UsingRabbitMq((context, cfg) =>
    {
        // RabbitMQ host configuration (local Docker)
        cfg.Host("localhost", 5672, "/", h =>
        {
            h.Username("guest");
            h.Password("guest");
        });

        // Retry policy: if consumer throws, retry 3 times with 2s intervals
        // This prevents transient issues from immediately dead-lettering / failing the message.
        cfg.UseMessageRetry(r => r.Interval(3, TimeSpan.FromSeconds(2)));

        // Optional but very useful: configure a dedicated receive endpoint name.
        // This maps to a RabbitMQ queue with the same name.
        cfg.ReceiveEndpoint("order-submitted-queue", e =>
        {
            // Concurrency can be tuned (default is reasonable).
            // e.PrefetchCount = 16;

            // Link the consumer to this endpoint.
            e.ConfigureConsumer<OrderSubmittedConsumer>(context);
        });

        // If we had multiple consumers and didn't want manual endpoints:
        // cfg.ConfigureEndpoints(context);
    });
});

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();


Now, for testing it, we open an API client and we send a request (Post) at the endpoint ‘api/orders/{orderId}/submit’:

We should see logs from ‘OrderSubmittedConsumer’ confirming the message was consumed:

Additionally, in RabbitMQ management UI, we should see the queue ‘order submitted queue’, messages rates and consumers:


This example demonstrates the core MassTransit workflow: define a contract, publish an event, and handle it in a consumer. From here, we can move the consumer to a separate service, add retries, dead-letter handling, or introduce more advanced patterns as our system grows.

One important advantage is that switching from RabbitMQ to Azure Service Bus requires only configuration changes our contracts, consumers, and business logic remain exactly the same.

Install the Azure Service Bus transport:

dotnet add package MassTransit.Azure.ServiceBus.Core

Replace the transport configuration in Program.cs:

x.UsingAzureServiceBus((context, cfg) =>
{
    cfg.Host(builder.Configuration.GetConnectionString("ServiceBus"));

    cfg.UseMessageRetry(r => r.Interval(3, TimeSpan.FromSeconds(2)));

    cfg.ReceiveEndpoint("order-submitted-queue", e =>
    {
        e.ConfigureConsumer<OrderSubmittedConsumer>(context);
    });
});


That’s it.
No changes to contracts.
No changes to consumers.
No changes to publishing logic.




Leave a Reply

Your email address will not be published. Required fields are marked *