DEV Community

Cover image for MassTransit in ASP.NET Core: A Practical Guide to Event-Driven .NET
Spyros Ponaris
Spyros Ponaris

Posted on

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

**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
Enter fullscreen mode Exit fullscreen mode

(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; }
    }
Enter fullscreen mode Exit fullscreen mode

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;
        }
    }

Enter fullscreen mode Exit fullscreen mode

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; // explicit is safer for versioning
                e.ConfigureConsumer<SubmitOrderConsumer>(context);

                // Robustness: retry with jitter + immediate faults to _error queue if exhausted
                e.UseMessageRetry(r => r.Interval(3, TimeSpan.FromSeconds(5)));
                e.PrefetchCount = 16;
                e.ConcurrentMessageLimit = 8;
            });
        });
    });
Enter fullscreen mode Exit fullscreen mode
builder.Services.AddHealthChecks();

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

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
Enter fullscreen mode Exit fullscreen mode

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
            });
    }
Enter fullscreen mode Exit fullscreen mode

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 });
        }
    }
Enter fullscreen mode Exit fullscreen mode

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;
            });
        });
    });
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)