MassTransit in ASP.NET Core: A Practical Guide to Event-Driven .NET

Leader posted 2 min read

MassTransit in ASP.NET Core: A Practical Guide to Event-Driven .NET
Intro

Event-driven architectures help teams decouple services, scale independently, and handle failures gracefully. MassTransit is a mature, open-source library that makes message-based workflows in .NET straightforward. This guide shows a minimal but production-ready setup in ASP.NET Core with RabbitMQ.

1) Install packages

From your Web API project:

dotnet add package MassTransit
dotnet add package MassTransit.RabbitMQ

(For Azure Service Bus use MassTransit.Azure.ServiceBus.Core instead.)

2) Define a message contract

Contracts should be versionable and live in a shared project.

public interface SubmitOrder
{
    Guid OrderId { get; }
    string CustomerId { get; }
    DateTime Timestamp { get; }
}

3) Create a consumer

using MassTransit;

public sealed class SubmitOrderConsumer : IConsumer<SubmitOrder>
{
    public async Task Consume(ConsumeContext<SubmitOrder> context)
    {
        var msg = context.Message;
        // Your domain logic here (idempotent!)
        Console.WriteLine($"Received SubmitOrder {msg.OrderId} for {msg.CustomerId}");
        await Task.CompletedTask;
    }
}

4) Configure MassTransit in Program.cs (.NET 9 minimal hosting)

RabbitMQ example with retries and health checks.

using MassTransit;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddMassTransit(cfg =>
{
    cfg.SetKebabCaseEndpointNameFormatter();

    cfg.AddConsumer<SubmitOrderConsumer>(c =>
    {
        // optional: configure consumer-level retry, etc.
    });

    cfg.UsingRabbitMq((context, bus) =>
    {
        bus.Host("rabbitmq", h =>
        {
            h.Username("guest");
            h.Password("guest");
        });

        bus.ReceiveEndpoint("submit-order-queue", e =>
        {
            e.ConfigureConsumeTopology = false; 
            e.ConfigureConsumer<SubmitOrderConsumer>(context);

            e.UseMessageRetry(r => r.Interval(3, TimeSpan.FromSeconds(5)));
            e.PrefetchCount = 16;
            e.ConcurrentMessageLimit = 8;
        });
    });
});

builder.Services.AddHealthChecks();

var app = builder.Build();
app.MapHealthChecks("/health");
app.MapGet("/", () => "OK");
app.Run();

Docker tip

If you run RabbitMQ locally via Docker:

docker run -d --hostname rabbit \
  -p 5672:5672 -p 15672:15672 \
  --name rabbitmq rabbitmq:3-management

UI is at http://localhost:15672 (guest/guest).

5) Publish a message (from a Controller or Service)

Inject IPublishEndpoint for pub/sub or ISendEndpointProvider for point-to-point.

using MassTransit;

public sealed class OrderAppService
{
    private readonly IPublishEndpoint _publish;
    public OrderAppService(IPublishEndpoint publish) => _publish = publish;

    public Task SubmitAsync(Guid orderId, string customerId)
        => _publish.Publish<SubmitOrder>(new
        {
            OrderId = orderId,
            CustomerId = customerId,
            Timestamp = DateTime.UtcNow
        });
}

Or via an endpoint (send to a specific queue):

public sealed class OrderSender
{
    private readonly ISendEndpointProvider _send;
    public OrderSender(ISendEndpointProvider send) => _send = send;

    public async Task SendAsync(Guid orderId, string customerId)
    {
        var endpoint = await _send.GetSendEndpoint(new Uri("queue:submit-order-queue"));
        await endpoint.Send<SubmitOrder>(new { OrderId = orderId, CustomerId = customerId, Timestamp = DateTime.UtcNow });
    }
}

6) Error handling, retries, and observability

Retries: use UseMessageRetry on endpoints or the bus. Prefer bounded retries with intervals or exponential backoff.

Poison messages: failed messages after retries land in _error queues automatically.

Health checks: expose /health and rely on container orchestration to restart unhealthy pods.

Idempotency: make consumers safe to reprocess (e.g., check a processed table or use dedup keys).

7) (Optional) Outbox & transactions

If you publish events within a DB transaction, consider an outbox pattern (MassTransit integrates with EFCore Outbox) to avoid dual-write issues and ensure at-least-once delivery without duplicates.

8) Azure Service Bus variant (quick sketch)

Swap the transport:

builder.Services.AddMassTransit(cfg =>
{
    cfg.AddConsumer<SubmitOrderConsumer>();

    cfg.UsingAzureServiceBus((context, bus) =>
    {
        bus.Host(builder.Configuration["ASB_CONNECTION"]!);

        bus.SubscriptionEndpoint<SubmitOrder>("submit-order-sub", e =>
        {
            e.ConfigureConsumer<SubmitOrderConsumer>(context);
            e.MaxConcurrentCalls = 8;
        });
    });
});

Conclusion

MassTransit keeps the happy path simple while giving you the tools for serious systems: retries, sagas, scheduling, observability, and transport flexibility. Start minimal, add policies as you learn your failure modes, and keep consumers idempotent.

If you read this far, tweet to the author to show them you care. Tweet a Thanks

Very informative breakdown of setting up MassTransit with ASP.NET Core and RabbitMQ. How do you think this approach compares to using built-in .NET background services for handling event-driven workflows in terms of scalability and maintenance?

Great question.

MassTransit with RabbitMQ is built for distributed, event-driven systems scales horizontally, gives you retries, sagas, and observability out of the box.

BackgroundService is lightweight and fine for simple, app-local jobs, but you end up reinventing queues, retries, and state handling as complexity grows.
If you expect workflows, cross-service integration, or high traffic, MassTransit is the maintainable, future-proof choice.

More Posts

Building a Pub/Sub System in .NET: MassTransit, Reactive Extensions, and BlockingCollection

Spyros - Aug 26

Building an Order Processing Saga with MassTransit

Spyros - Aug 12

Supercharging EF Core Specifications with EF.CompileQuery

Spyros - Aug 5

Rate limiting middleware in ASP.NET Core using .NET 8.0

Hussein Mahdi - May 11, 2024

Understanding MediatR Assembly Registration in .NET

Moses Korir - Jul 4
chevron_left