messaging
codewithmukesh/dotnet-claude-kit
Asynchronous messaging patterns for .NET: Wolverine and MassTransit, outbox, sagas, and broker configuration.
What is messaging?
Covers event-driven communication, message publishing, and consumption patterns for .NET applications. Use this skill when implementing background processing, module-to-module messaging, or working with message brokers like RabbitMQ and Azure Service Bus.
- Set up Wolverine or MassTransit for asynchronous messaging with convention-based handler discovery
- Implement the transactional outbox pattern to ensure reliable message publishing with database transactions
- Build event choreography for simple workflows or sagas for complex stateful orchestrations with compensating actions
- Configure RabbitMQ and Azure Service Bus brokers with auto-provisioning and auto-purge options
- Publish events via cascading messages (return values) or explicit IMessageBus calls
- Consume events with zero-ceremony convention-based handlers
How to install messaging
npx skills add https://github.com/codewithmukesh/dotnet-claude-kit --skill messaging- A .NET project with Entity Framework Core DbContext
- RabbitMQ or Azure Service Bus broker (or in-memory transport for testing)
- Shared contracts project for message type definitions
How to use messaging
- 1.Create a shared Contracts project and define message types as simple records with primitive types
- 2.Register Wolverine (or MassTransit) in Program.cs with transport and DbContext integration
- 3.Add Wolverine inbox/outbox tables to your DbContext via AddIncomingWolverineMessageTable() and AddOutgoingWolverineMessageTable()
- 4.Write handler methods following the convention: public static Task HandleAsync(MessageType message, ...) or return cascading messages as tuples
- 5.Publish events either by returning them from handlers or via explicit IMessageBus.PublishAsync() calls
- 6.For complex workflows, implement a Saga<T> with Start and Handle methods that return cascading messages to drive state transitions
Use cases
- Publishing domain events when an order is created and consuming them in notifications or inventory modules
- Implementing a multi-step order fulfillment workflow with payment processing, inventory reservation, and shipment coordination
- Setting up a saga that compensates (cancels) previous steps if a payment fails partway through a workflow
- Decoupling microservices by using a message bus instead of direct HTTP calls between modules
- Processing background jobs asynchronously without blocking the main request handler
- Backend .NET developers building event-driven or microservice architectures
- Teams implementing domain-driven design with domain events and bounded contexts
- Developers working with RabbitMQ, Azure Service Bus, or other message brokers
- Architects designing reliable, scalable asynchronous communication patterns
messaging FAQ
Wolverine is the recommended default: MIT licensed, combines mediator + messaging in one library with built-in outbox and saga support. MassTransit is a mature alternative but requires a commercial license from v9+.
The outbox pattern stores messages in the same database transaction as your domain changes, then publishes them asynchronously. This eliminates the dual-write problem and ensures messages are only sent if the database transaction succeeds.
Use event choreography for simple workflows with 2-3 steps where each event triggers the next handler independently. Use a saga for complex workflows with compensating actions or state that needs to be tracked across multiple steps.
Wolverine uses convention-based discovery: any public method named Handle, HandleAsync, Consume, or ConsumeAsync with the message type as the first parameter is automatically registered as a handler.
Wolverine supports both RabbitMQ and Azure Service Bus. Configure the transport in UseWolverine() with either UseRabbitMq() or UseAzureServiceBus() depending on your broker.
Full instructions (SKILL.md)
Source of truth, from codewithmukesh/dotnet-claude-kit.
name: messaging description: > Asynchronous messaging patterns for .NET applications. Covers Wolverine and MassTransit, outbox pattern, saga and choreography, and broker configuration for RabbitMQ and Azure Service Bus. Load this skill when implementing event-driven communication, background processing, module-to-module messaging, or when the user mentions "Wolverine", "MassTransit", "message bus", "RabbitMQ", "Azure Service Bus", "event", "publish", "consumer", "outbox", "saga", "integration event", "queue", or "pub/sub".
Messaging
Core Principles
- Wolverine is the recommended default — MIT licensed, combines mediator + messaging in one library with built-in outbox, saga support, and convention-based handlers. MassTransit is an alternative but requires a commercial license from v9.
- Outbox pattern for reliability — Always use the transactional outbox to ensure messages are published only when the database transaction succeeds.
- Choreography for simple flows, saga for complex — If a workflow has 2-3 steps, use event choreography. If it has compensating actions or complex state, use a saga.
- Messages are contracts — Put message types in a shared contracts project. Keep them as simple records with primitive types.
Patterns
Wolverine Setup
// Program.cs
builder.Host.UseWolverine(opts =>
{
// Auto-discover handlers from this assembly
opts.Discovery.IncludeAssembly(typeof(Program).Assembly);
// RabbitMQ transport
opts.UseRabbitMq(rabbit =>
{
rabbit.HostName = "localhost";
// Or from configuration:
// rabbit.HostName = builder.Configuration["RabbitMq:Host"]!;
})
.AutoProvision() // Create queues/exchanges automatically
.AutoPurgeOnStartup(); // Dev only — clear queues on startup
// Enable transactional outbox with EF Core
opts.Services.AddDbContextWithWolverineIntegration<AppDbContext>(x =>
x.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
opts.Policies.AutoApplyTransactions(); // Wrap handlers in DB transactions
});
Why: UseWolverine() registers handler discovery, transport, and outbox in one place. AutoProvision() eliminates manual broker setup during development.
Publishing Events
Wolverine supports two publishing styles: cascading messages (return values) and explicit publishing.
// Message contract (in shared Contracts project)
public record OrderCreated(Guid OrderId, string CustomerId, decimal Total, DateTimeOffset CreatedAt);
// Style 1: Cascading messages — return the event from the handler
// Wolverine automatically publishes returned messages after the handler completes.
public static class CreateOrder
{
public record Command(string CustomerId, List<OrderItem> Items);
public record Response(Guid OrderId, decimal Total);
public static async Task<(Response, OrderCreated)> HandleAsync(
Command command, AppDbContext db, TimeProvider clock, CancellationToken ct)
{
var order = Order.Create(command.CustomerId, command.Items, clock.GetUtcNow());
db.Orders.Add(order);
await db.SaveChangesAsync(ct);
var response = new Response(order.Id, order.Total);
var @event = new OrderCreated(order.Id, order.CustomerId, order.Total, order.CreatedAt);
return (response, @event); // Both are published automatically
}
}
// Style 2: Explicit publishing via IMessageBus
public static class CreateOrder
{
public record Command(string CustomerId, List<OrderItem> Items);
public record Response(Guid OrderId, decimal Total);
public static async Task<Response> HandleAsync(
Command command, AppDbContext db, IMessageBus bus, TimeProvider clock, CancellationToken ct)
{
var order = Order.Create(command.CustomerId, command.Items, clock.GetUtcNow());
db.Orders.Add(order);
await db.SaveChangesAsync(ct);
await bus.PublishAsync(new OrderCreated(
order.Id, order.CustomerId, order.Total, order.CreatedAt));
return new Response(order.Id, order.Total);
}
}
Why: Cascading messages (tuple return) are simpler and testable — the handler is a pure function. Use explicit IMessageBus when publishing is conditional or requires multiple events.
Consuming Events
Wolverine uses convention-based handlers — no interface, no base class. Just a Handle method with the message type as the first parameter.
// Notifications module — handles OrderCreated from Orders module
public static class OrderCreatedHandler
{
public static async Task HandleAsync(
OrderCreated message, NotificationsDbContext db, ILogger logger, CancellationToken ct)
{
logger.LogInformation("Processing OrderCreated: {OrderId}", message.OrderId);
var notification = new OrderNotification(message.OrderId, message.CustomerId);
db.Notifications.Add(notification);
await db.SaveChangesAsync(ct);
}
}
Why: Convention-based handlers have zero ceremony. Wolverine discovers them by signature: any public method named Handle/HandleAsync/Consume/ConsumeAsync with the message type as the first parameter.
Transactional Outbox
Ensures messages are only published if the database transaction succeeds.
// 1. Register DbContext with Wolverine integration
builder.Host.UseWolverine(opts =>
{
opts.Services.AddDbContextWithWolverineIntegration<AppDbContext>(x =>
x.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
opts.Policies.AutoApplyTransactions();
});
// 2. DbContext — add Wolverine outbox tables
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
public DbSet<Order> Orders => Set<Order>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Wolverine inbox/outbox tables — required for transactional messaging
modelBuilder.AddIncomingWolverineMessageTable();
modelBuilder.AddOutgoingWolverineMessageTable();
}
}
Why: AddDbContextWithWolverineIntegration + AutoApplyTransactions wraps every handler in a transaction that includes outbox writes. Messages are only sent after the transaction commits — no dual-write problem.
Saga (Stateful Orchestration)
Wolverine sagas use a Saga<T> base class with Start and Handle methods. Cascading messages drive the saga forward.
public record OrderSagaState(Guid Id)
{
public string? CustomerId { get; set; }
public bool PaymentReceived { get; set; }
}
public class OrderSaga : Saga<OrderSagaState>
{
public Guid Id { get; set; }
// Start the saga when an OrderCreated event arrives
public static (OrderSagaState, ProcessPayment) Start(OrderCreated message)
{
var state = new OrderSagaState(message.OrderId)
{
CustomerId = message.CustomerId
};
var command = new ProcessPayment(message.OrderId, message.Total);
return (state, command); // State is persisted, command is sent
}
// Handle payment result
public CompleteOrder Handle(PaymentCompleted message)
{
PaymentReceived = true;
MarkCompleted(); // Ends the saga
return new CompleteOrder(Id);
}
// Compensating action on failure
public CancelOrder Handle(PaymentFailed message)
{
MarkCompleted();
return new CancelOrder(Id);
}
}
Why: Wolverine sagas use simple C# methods instead of a state machine DSL. Each handler returns cascading messages to drive the workflow. MarkCompleted() cleans up the saga state.
Alternative: MassTransit
MassTransit is a mature alternative with a commercial license requirement from v9+. Key API surface:
// Setup
builder.Services.AddMassTransit(x =>
{
x.SetKebabCaseEndpointNameFormatter();
x.AddConsumers(typeof(Program).Assembly);
x.UsingRabbitMq((context, cfg) =>
{
cfg.Host(builder.Configuration.GetConnectionString("RabbitMq"));
cfg.ConfigureEndpoints(context);
});
});
// Publishing
await publishEndpoint.Publish(new OrderCreated(...), ct);
// Consuming — requires IConsumer<T> interface
public class OrderCreatedConsumer(AppDbContext db) : IConsumer<OrderCreated>
{
public async Task Consume(ConsumeContext<OrderCreated> context)
{
var message = context.Message;
// Handle event...
}
}
// Outbox
x.AddEntityFrameworkOutbox<AppDbContext>(o =>
{
o.UsePostgres();
o.UseBusOutbox();
});
// Saga — uses MassTransitStateMachine<TState>
public class OrderSaga : MassTransitStateMachine<OrderSagaState> { /* ... */ }
License note: MassTransit v9+ requires a commercial license for production use. Wolverine (MIT) is the recommended default for new projects.
Anti-patterns
Don't Publish Events Without Outbox
// BAD — if SaveChanges succeeds but Publish fails, data is inconsistent
await db.SaveChangesAsync(ct);
await bus.PublishAsync(new OrderCreated(...));
// GOOD — use transactional outbox (messages are in the same transaction)
// Configure AddDbContextWithWolverineIntegration() + AutoApplyTransactions()
// Wolverine handles this automatically
Don't Put Complex Logic in Message Contracts
// BAD — behavior in a message
public record OrderCreated(Guid OrderId)
{
public decimal CalculateShipping() => /* logic */; // DON'T
}
// GOOD — messages are pure data
public record OrderCreated(Guid OrderId, string CustomerId, decimal Total, DateTimeOffset CreatedAt);
Don't Use Fire-and-Forget for Important Events
// BAD — no guarantee of delivery
_ = Task.Run(() => bus.PublishAsync(new OrderCreated(...)));
// GOOD — await the publish (with outbox, this is transactional)
await bus.PublishAsync(new OrderCreated(...));
Decision Guide
| Scenario | Recommendation |
|---|---|
| Module-to-module communication (new project) | Wolverine with events (MIT, free) |
| Module-to-module communication (existing MassTransit) | MassTransit (commercial license required from v9) |
| Reliable event publishing | Transactional outbox (both Wolverine and MassTransit support this) |
| Simple 2-3 step workflow | Event choreography |
| Complex workflow with compensation | Wolverine saga or MassTransit saga |
| Local development broker | RabbitMQ (via Docker or Aspire) |
| Production cloud broker | Azure Service Bus or RabbitMQ |
| Want single lib for mediator + messaging | Wolverine (replaces both Mediator and MassTransit) |
Related skills
More from codewithmukesh/dotnet-claude-kit and the wider catalog.

migrate
Guided, safe migration workflow for EF Core schema changes, .NET upgrades, and NuGet updates with rollback strategies.

minimal-api
Build type-safe HTTP endpoints with .NET 10 minimal APIs, auto-discovery, and OpenAPI documentation.

modern-csharp
Modern C# 14 and .NET 10 language features: primary constructors, records, pattern matching, spans, and the field keyword.

openapi
Built-in OpenAPI support for .NET 10 with document generation, transformers, and security schemes—no Swashbuckle needed.

opentelemetry
Distributed tracing, metrics, and logs for .NET 10 with OpenTelemetry and OTLP export.

outdated
Audit .NET NuGet packages for vulnerabilities, staleness, and commercial-license traps.