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 with the retry loop, circuit-breaker state, timeout token, idempotency check, and telemetry — all inline.

Terminal window
dotnet add package Pragmatic.Messaging # core + EF Core 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.

PackageRole
Pragmatic.MessagingCore runtime + EF Core transactional outbox
Pragmatic.Messaging.CoreInterfaces, attributes, in-memory bus
Pragmatic.Messaging.Channels / .RabbitMQ / .KafkaTransports (in-process / AMQP / streaming)
Pragmatic.Messaging.JobsScheduled (future) message delivery, via Pragmatic.Jobs
Pragmatic.Messaging.Auditing / .EFCoreAudit trail (in-memory / EF Core)
Pragmatic.Messaging.TestingMessageBusTestHarness for handler tests

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.

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 |

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