Skip to content

Getting Started

Build a message handler with resilience from zero in 5 minutes.

  • .NET 10 SDK
  • A Pragmatic.Design project with [Module] and [Boundary] configured
  • Pragmatic.SourceGenerator referenced as Analyzer

Before we start, an important distinction:

Pragmatic.EventsPragmatic.Messaging
ScopeIn-process, same boundaryCross-boundary, cross-service
DispatchSynchronous, same transactionAsync, decoupled, outbox-backed
HandlerIDomainEventHandler<T>IMessageHandler<T>
Use whenSide effects within same boundaryCommunication between boundaries/services

The bridge: The SG generates a MessageHandlerEventAdapter<T> that lets domain events (from Pragmatic.Events) also reach message handlers. This is optional — you can use messaging standalone.

This tutorial covers Pragmatic.Messaging — the cross-boundary, async messaging system.

A message is any record/class. No base class required.

Showcase.Billing/Messages/InvoiceApprovalRequested.cs
namespace Showcase.Billing.Messages;
public sealed record InvoiceApprovalRequested(
Guid InvoiceId,
Guid ApproverId,
decimal Amount,
string Currency);
Showcase.Billing/Handlers/InvoiceApprovalHandler.cs
using Pragmatic.Messaging;
using Pragmatic.Messaging.Attributes;
namespace Showcase.Billing.Handlers;
[MessageHandler]
[Retry(MaxAttempts = 3, Strategy = BackoffStrategy.ExponentialWithJitter, BaseDelayMs = 200)]
public sealed partial class InvoiceApprovalHandler(
IInvoiceService invoiceService,
ILogger<InvoiceApprovalHandler> logger) : IMessageHandler<InvoiceApprovalRequested>
{
public async Task HandleAsync(
InvoiceApprovalRequested message,
MessageContext context,
CancellationToken ct = default)
{
await invoiceService.ApproveAsync(message.InvoiceId, message.ApproverId, ct);
logger.LogInformation("Invoice {Id} approved by {Approver}", message.InvoiceId, message.ApproverId);
}
}

Key points:

  • [MessageHandler] — SG discovers and generates a pipeline wrapper
  • [Retry] — generates a retry loop with exponential backoff + jitter (inline, no Polly)
  • partial — required for SG to add [LoggerMessage] stubs
  • IMessageHandler<T> — the contract. T is your message type.

From anywhere in your code, inject IMessageBus and publish:

public class InvoiceWorkflow(IMessageBus bus)
{
public async Task RequestApproval(Guid invoiceId, Guid approverId, decimal amount)
{
var message = new InvoiceApprovalRequested(invoiceId, approverId, amount, "EUR");
await bus.PublishAsync(message);
}
}

With context (correlation, tenant):

var context = MessageContext.New(
correlationId: orderId.ToString(),
tenantId: currentTenant);
await bus.PublishAsync(message, context);
Program.cs
await PragmaticApp.RunAsync(args, app =>
{
app.UseMessaging(msg =>
{
msg.UseChannels(ch =>
{
ch.Capacity = 1000;
ch.ConsumerCount = 2;
});
});
});

The SG auto-registers handlers — no manual DI needed.

Terminal window
dotnet build

Check generated files in obj/Debug/net10.0/generated/:

FileContent
InvoiceApprovalHandler.Pipeline.g.csRetry loop (3 attempts, exponential+jitter) + telemetry
_Infra.Messaging.Registration.g.csAddPragmaticMessageHandlers() — auto-registers all handlers
_Infra.Messaging.TypeRegistry.g.csAOT-safe switch for message deserialization
  1. bus.PublishAsync(message) dispatches to all registered IMessageHandler<T>
  2. InvoiceApprovalHandler_Pipeline.ExecuteAsync() runs:
    • Idempotency check (if EnableIdempotency() configured)
    • Retry loop: attempt 1 → on failure, wait ~200ms → attempt 2 → wait ~600ms → attempt 3
    • Telemetry: Activity span, duration metric, handler counter
    • Logging: started/completed/failed via [LoggerMessage]
  3. On success: metrics recorded, activity closed
  4. On final failure: exception propagates, dead letter store captures message

If you also use Pragmatic.Events, the SG generates an adapter so domain events reach your message handlers:

// Entity raises a domain event (Pragmatic.Events)
reservation.RaiseDomainEvent(new ReservationConfirmed(...));
// The SG generates MessageHandlerEventAdapter<ReservationConfirmed>
// which bridges IDomainEventHandler<T> → IMessageHandler<T>
// So your [MessageHandler] for ReservationConfirmed also runs.

This bridge is automatic when both Events and Messaging are referenced. No config needed.

  • Concepts — architecture, pipeline diagram, transport model
  • Common Mistakes — top 10 pitfalls
  • Troubleshooting — checklists, diagnostics, FAQ
  • Showcase: examples/showcase/src/Showcase.Billing/Events/Handlers/ for real-world patterns