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.

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

Decorate services in ASP.NET Core, step by step

Spyros - Nov 14

Global Exception Handler vs Custom Middleware in ASP.NET Core

Spyros - Nov 4

Supercharging EF Core Specifications with EF.CompileQuery

Spyros - Aug 5
chevron_left