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
0 votes
0 votes

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

Reliable Messaging in .NET: Domain Events and the Outbox Pattern with EF Core Interceptors

Spyros - Oct 13
chevron_left