Pragmatic.Messaging
Event-driven messaging for .NET 10 — zero reflection, AOT-safe handler pipelines, transactional outbox, saga orchestration, batch processing, and multi-transport support, all source-generated at compile time.
The Problem
Section titled “The Problem”Messaging in .NET usually means MassTransit or NServiceBus. Both are powerful, but both rely on runtime assembly scanning for handler discovery, dynamic dispatch, and runtime-evaluated middleware pipelines. Retry lives in a separate Polly config; sagas are discovered by reflection; the link between a handler and its resilience policy is implicit and scattered.
// Without Pragmatic: config scattered across files; the handler has no idea about retry/CB/timeoutservices.AddMassTransit(x =>{ x.AddConsumer<OrderCreatedConsumer>(); // runtime discovery x.UsingRabbitMq((ctx, cfg) => cfg.ReceiveEndpoint("order-created", e => { e.UseMessageRetry(r => r.Intervals(200, 500, 1000)); // separate from the handler e.UseCircuitBreaker(cb => cb.TrackingPeriod = TimeSpan.FromMinutes(1)); e.ConfigureConsumer<OrderCreatedConsumer>(ctx); }));});The Solution
Section titled “The Solution”Resilience is declared on the handler. The generator produces the complete pipeline — retry loop, circuit breaker, timeout, idempotency, telemetry — as inline code at compile time. Zero Polly, zero reflection.
[MessageHandler][Retry(MaxAttempts = 3, Strategy = BackoffStrategy.ExponentialWithJitter, BaseDelayMs = 200)][CircuitBreaker(FailureThreshold = 5, BreakDurationSeconds = 30)][Timeout(TimeoutSeconds = 60)]public sealed partial class OrderCreatedHandler(IOrderService service) : IMessageHandler<OrderCreated>{ public async Task HandleAsync(OrderCreated message, MessageContext context, CancellationToken ct) => await service.ProcessAsync(message.OrderId, ct);}The generator emits OrderCreatedHandler_Pipeline.g.cs with the retry loop, circuit-breaker state,
timeout token, idempotency check, and telemetry — all inline.
Installation
Section titled “Installation”dotnet add package Pragmatic.Messaging # core + EF Core outboxdotnet add package Pragmatic.Messaging.Core # interfaces, attributes, in-memory busdotnet add package Pragmatic.SourceGenerator # the unified analyzerAdd transports and bridges as needed:
dotnet add package Pragmatic.Messaging.Channels # in-process asyncdotnet add package Pragmatic.Messaging.RabbitMQ # distributed (AMQP)dotnet add package Pragmatic.Messaging.Kafka # event streamingdotnet add package Pragmatic.Messaging.Jobs # scheduled (future) deliverydotnet add package Pragmatic.Messaging.Auditing # message audit trail(Building inside this monorepo? See Monorepo Structure.)
Quick Start
Section titled “Quick Start”1. Define a message and handler (the generator auto-discovers handlers by [MessageHandler] — no
manual registration):
public record OrderCreated(Guid OrderId, decimal Total, string CustomerId);
[MessageHandler][Retry(MaxAttempts = 3)]public sealed partial class OrderCreatedHandler(INotificationService notifications) : IMessageHandler<OrderCreated>{ public async Task HandleAsync(OrderCreated message, MessageContext context, CancellationToken ct) => await notifications.SendOrderConfirmationAsync(message.OrderId, ct);}2. Configure the host:
await PragmaticApp.RunAsync(args, app =>{ app.UseMessaging(msg => { msg.UseChannels(ch => { ch.Capacity = 1000; ch.ConsumerCount = 2; }); msg.EnableIdempotency(); });});3. Publish:
public class CheckoutService(IMessageBus bus){ public Task CompleteCheckoutAsync(Order order, CancellationToken ct) => bus.PublishAsync(new OrderCreated(order.Id, order.Total, order.CustomerId), ct);}Full walkthrough: Getting Started.
Packages
Section titled “Packages”| Package | Role |
|---|---|
Pragmatic.Messaging | Core runtime + EF Core transactional outbox |
Pragmatic.Messaging.Core | Interfaces, attributes, in-memory bus |
Pragmatic.Messaging.Channels / .RabbitMQ / .Kafka | Transports (in-process / AMQP / streaming) |
Pragmatic.Messaging.Jobs | Scheduled (future) message delivery, via Pragmatic.Jobs |
Pragmatic.Messaging.Auditing / .EFCore | Audit trail (in-memory / EF Core) |
Pragmatic.Messaging.Testing | MessageBusTestHarness for handler tests |
Operational note
Section titled “Operational note”The outbox is at-least-once: a message can be delivered more than once (e.g. after a retry or a
crash between commit and dispatch). Make handlers idempotent — enable EnableIdempotency() and/or
guard side effects by a business key. In cross-boundary scenarios, publish through the outbox
([EnableOutbox]) so the message commits in the same transaction as your data. See
Common Mistakes.
Status
Section titled “Status”Core handler pipeline, outbox, sagas, transports, and batch are functional within the 0.8 preview; see the roadmap for what is settling before 1.0.
| Concepts | Message lifecycle, handler pipeline, transport architecture, registration, Events vs Messaging |
| Getting Started | Define a message, handler, publish, configure a transport |
| Sagas | ISaga<T>, orchestration, compensation, timeouts |
| Advanced Patterns | Request/Reply, multi-bus, scheduled messages, auditing |
| Common Mistakes | The most frequent messaging pitfalls (idempotency, outbox, retry, batch) |
| Troubleshooting | Handler/outbox/retry/saga checklists, diagnostics reference, FAQ |
Cross-module integration
Section titled “Cross-module integration”Publishes Events across boundaries, commits the outbox in the same
Persistence transaction, bridges to Jobs
for scheduled delivery, and is wired by Composition’s UseMessaging().
Requirements
Section titled “Requirements”- .NET 10.0+
Pragmatic.SourceGeneratoranalyzer
License
Section titled “License”Part of the Pragmatic.Design ecosystem — see Licensing. Pragmatic.Messaging is licensed under the PolyForm Small Business 1.0.0 license (free for small businesses; commercial license above the threshold).