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)