Skip to content

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.

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/timeout
services.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);
}));
});

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 (a nested Pipeline class) with the retry loop, circuit-breaker state, timeout token, idempotency check, and telemetry, all inline.

Terminal window
dotnet add package Pragmatic.Messaging # bridge to Events + EF Core: the outbox
dotnet add package Pragmatic.Messaging.Core # interfaces, attributes, in-memory bus
dotnet add package Pragmatic.SourceGenerator # the unified analyzer

Add transports and bridges as needed:

Terminal window
dotnet add package Pragmatic.Messaging.Channels # in-process async
dotnet add package Pragmatic.Messaging.RabbitMQ # distributed (AMQP)
dotnet add package Pragmatic.Messaging.Kafka # event streaming
dotnet add package Pragmatic.Messaging.Jobs # scheduled (future) delivery
dotnet add package Pragmatic.Messaging.Auditing # message audit trail

(Building inside this monorepo? See Monorepo Structure.)

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.

Package Role
Pragmatic.Messaging Bridge to Events + EF Core: outbox interceptor, delivery service, event adapter
Pragmatic.Messaging.Core Interfaces, attributes, in-memory bus, middleware, routing, distributed request/reply; no dependency on Events or EF Core
Pragmatic.Messaging.Channels / .RabbitMQ / .Kafka / .AzureServiceBus / .Sql Transports (in-process / AMQP / streaming / cloud+emulator / PostgreSQL or SQL Server tables, no broker)
Pragmatic.Messaging.EFCore EF Core stores: outbox source, idempotency, audit persistence
Pragmatic.Messaging.Saga Saga orchestration over domain actions: compensation, timeouts, generated orchestrator
Pragmatic.Messaging.Batch Scatter/gather with progress tracking and rate limiting
Pragmatic.Messaging.ClaimCheck Large payloads moved to Pragmatic.Storage; the message carries the reference
Pragmatic.Messaging.Jobs Scheduled (future) message delivery, via Pragmatic.Jobs
Pragmatic.Messaging.Auditing Every handled message on the Pragmatic.Audit trail, by reference, never by payload
Pragmatic.Messaging.Dashboard Ops API and embedded panel: status, outbox, dead-letter replay, sagas, audit
Pragmatic.Messaging.Testing MessageBusTestHarness (record-only + dispatching modes)

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.

Functional within 1.0.0-alpha: the handler pipeline, the outbox, sagas, the transports, and batch. See the roadmap.

| Concepts | Message lifecycle, handler pipeline, transport architecture, registration, Events vs Messaging | | Getting Started | Define a message, handler, publish, configure a transport | | Transports | RabbitMQ / Kafka / Azure Service Bus / SQL (Postgres/SqlServer) / Channels: topology, dead-lettering, ordering, local testing (incl. ASB emulator) | | Reliability | The failure ladder: [Retry] → [Redelivery] → transport dead-letter; kill switch, rate/concurrency limits, idempotency purge, claim check | | Sagas | ISaga<T>, orchestration vs choreography, compensation, timeouts, generated Mermaid diagrams | | Advanced Patterns | Distributed request/reply, multi-bus with isolated transports, scheduled messages (Jobs / ASB / SQL native), auditing, claim check | | Operations | Ops dashboard (status, outbox, dead-letter replay, sagas, audit), counters, health | | Testing | Harness modes (record-only / dispatching), Testcontainers broker fixtures | | Common Mistakes | The most frequent messaging pitfalls (idempotency, outbox, retry, batch) | | Troubleshooting | Handler/outbox/retry/saga checklists, diagnostics reference, FAQ |

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().

  • .NET 10.0+
  • Pragmatic.SourceGenerator analyzer

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).