npx skills add ...
npx skills add codewithmukesh/dotnet-claude-kit --skill messaging
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".
npx skills add codewithmukesh/dotnet-claude-kit --skill messaging
Why: UseWolverine() registers handler discovery, transport, and outbox in one place. AutoProvision() eliminates manual broker setup during development.
Wolverine supports two publishing styles: cascading messages (return values) and explicit publishing.
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.
Wolverine uses convention-based handlers — no interface, no base class. Just a Handle method with the message type as the first parameter.
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.
Ensures messages are only published if the database transaction succeeds.
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.
Wolverine sagas use a Saga<T> base class with Start and Handle methods. Cascading messages drive the saga forward.
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.
MassTransit is a mature alternative with a commercial license requirement from v9+. Key API surface:
License note: MassTransit v9+ requires a commercial license for production use. Wolverine (MIT) is the recommended default for new projects.
| 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) |
// 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);
}
}// 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);
}
}// 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();
}
}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);
}
}// 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> { /* ... */ }// 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// 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);// 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(...));