WolverineFX for .NET
When to Use This Skill
Use this skill when:
- Building message handlers or command handlers with Wolverine
- Creating HTTP endpoints with WolverineFx.HTTP (alternative to Minimal API/MVC)
- Implementing event sourcing with Marten and Wolverine
- Setting up transactional outbox pattern for reliable messaging
- Configuring message transports (RabbitMQ, Azure Service Bus, Amazon SQS, TCP)
- Implementing CQRS with event sourcing
- Processing messages in batches
- Using cascading messages for testable, pure function handlers
- Configuring error handling and retry policies
- Pre-generating code for optimized cold starts
Related Skills
efcore-patterns- Entity Framework Core patterns for data accesscsharp-coding-standards- Modern C# patterns (records, pattern matching)http-client-resilience- Polly resilience patterns (complementary)background-services- Hosted services and background job patternsaspire-configuration- .NET Aspire orchestration
Core Principles
- Low Ceremony Code - Pure functions, method injection, minimal boilerplate
- Cascading Messages - Return messages from handlers instead of injecting IMessageBus
- Transactional Outbox - Guaranteed message delivery with database transactions
- Code Generation - Runtime or pre-generated code for optimal performance
- Vertical Slice Architecture - Organize code by feature, not technical layers
- Pure Functions for Business Logic - Isolate infrastructure from business logic
Required NuGet Packages
Core Messaging
xml<PackageReference Include="Wolverine" /> <PackageReference Include="WolverineFx.Http" />
Persistence Integration
xml<PackageReference Include="WolverineFx.Marten" />
Transports
xml<PackageReference Include="WolverineFx.RabbitMQ" /> <PackageReference Include="WolverineFx.AzureServiceBus" /> <PackageReference Include="WolverineFx.Kafka" /> <PackageReference Include="WolverineFx.AmazonSQS" />
Basic Setup
Program.cs (ASP.NET Core)
csharpusing JasperFx; using Wolverine; using Wolverine.Http; var builder = WebApplication.CreateBuilder(args); builder.Host.UseWolverine(opts => { opts.Policies.AutoApplyTransactions(); opts.Policies.UseDurableLocalQueues(); }); builder.Services.AddWolverineHttp(); var app = builder.Build(); app.MapWolverineEndpoints(); return await app.RunJasperFxCommands(args);
Message Handlers
Simple Message Handler
csharppublic record DebitAccount(long AccountId, decimal Amount); public static class DebitAccountHandler { public static void Handle(DebitAccount command, IAccountRepository repository) { repository.Debit(command.AccountId, command.Amount); } }
Handler with Cascading Messages
csharppublic record CreateOrder(Guid OrderId, string[] Items); public record OrderCreated(Guid OrderId); public static class CreateOrderHandler { public static (OrderCreated, ShipOrder) Handle( CreateOrder command, IDocumentSession session) { var order = new Order { Id = command.OrderId, Items = command.Items }; session.Store(order); return ( new OrderCreated(command.OrderId), new ShipOrder(command.OrderId) ); } }
Using OutgoingMessages for Multiple Messages
csharppublic static OutgoingMessages Handle(ProcessOrder command) { var messages = new OutgoingMessages { new OrderProcessed(command.OrderId), new SendEmail(command.CustomerEmail, "Order processed"), new UpdateInventory(command.Items) }; messages.Delay(new CleanupOrder(command.OrderId), 5.Minutes()); return messages; }
HTTP Endpoints (WolverineFx.HTTP)
Basic GET Endpoint
csharp[WolverineGet("/users/{id}")] public static Task<User?> GetUser(int id, IQuerySession session) => session.LoadAsync<User>(id);
POST with Message Publishing
csharp[WolverinePost("/orders")] public static async Task<IResult> CreateOrder( CreateOrderRequest request, IDocumentSession session, IMessageBus bus) { var order = new Order { Id = Guid.NewGuid(), Items = request.Items }; session.Store(order); await bus.PublishAsync(new OrderCreated(order.Id)); return Results.Created($"/orders/{order.Id}", order); }
Compound Handler (Load/Validate/Handle)
csharppublic static class UpdateOrderEndpoint { public static async Task<(Order?, IResult)> LoadAsync( UpdateOrder command, IDocumentSession session) { var order = await session.LoadAsync<Order>(command.OrderId); return order != null ? (order, new WolverineContinue()) : (order, Results.NotFound()); } [WolverinePut("/orders")] public static void Handle(UpdateOrder command, Order order, IDocumentSession session) { order.Items = command.Items; session.Store(order); } }
Event Sourcing with Marten
Aggregate Handler Workflow
csharppublic class Order { public Guid Id { get; set; } public int Version { get; set; } public Dictionary<string, Item> Items { get; set; } = new(); public DateTimeOffset? Shipped { get; private set; } public void Apply(ItemReady ready) => Items[ready.Name].Ready = true; public void Apply(IEvent<OrderShipped> shipped) => Shipped = shipped.Timestamp; public bool IsReadyToShip() => Shipped == null && Items.Values.All(x => x.Ready); } public record MarkItemReady(Guid OrderId, string ItemName, int Version); [AggregateHandler] public static IEnumerable<object> Handle(MarkItemReady command, Order order) { if (order.Items.TryGetValue(command.ItemName, out var item)) { item.Ready = true; yield return new ItemReady(command.ItemName); } if (order.IsReadyToShip()) { yield return new OrderReady(); } }
Read Aggregate (Read-Only)
csharp[WolverineGet("/orders/{id}")] public static Order GetOrder([ReadAggregate] Order order) => order;
Write Aggregate with Validation
csharppublic static IEnumerable<object> Handle( MarkItemReady command, [WriteAggregate(Required = true, OnMissing = OnMissing.ProblemDetailsWith404)] Order order) { order.Items[command.ItemName].Ready = true; yield return new ItemReady(command.ItemName); }
Returning Updated Aggregate
csharp[AggregateHandler] public static (UpdatedAggregate, Events) Handle( MarkItemReady command, Order order) { var events = new Events(); events.Add(new ItemReady(command.ItemName)); return (new UpdatedAggregate(), events); }
Transactional Outbox
Marten Integration
csharpbuilder.Services.AddMarten(opts => { opts.Connection(connectionString); }).IntegrateWithWolverine(); builder.Host.UseWolverine(opts => { opts.Policies.AutoApplyTransactions(); });
Using Outbox in Controllers
csharp[HttpPost("/orders")] public async Task Post( [FromBody] CreateOrder command, [FromServices] IDocumentSession session, [FromServices] IMartenOutbox outbox) { outbox.Enroll(session); var order = new Order { Id = command.OrderId }; session.Store(order); await outbox.PublishAsync(new OrderCreated(command.OrderId)); await session.SaveChangesAsync(); }
Transport Configuration
RabbitMQ
csharpbuilder.Host.UseWolverine(opts => { opts.UseRabbitMq("host=localhost") .AutoProvision() .AutoPurgeOnStartup(); opts.PublishAllMessages() .ToRabbitExchange("wolverine.events", exchange => { exchange.ExchangeType = ExchangeType.Topic; }); });
Azure Service Bus
csharpbuilder.Host.UseWolverine(opts => { opts.UseAzureServiceBus(asbConnectionString) .AutoProvision() .ConfigureQueue(q => q.MaxDeliveryCount = 5); });
Amazon SQS
csharpbuilder.Host.UseWolverine(opts => { opts.UseAmazonSqs(sqsConfig) .AutoProvision(); });
Batch Message Processing
Configure Batching
csharpbuilder.Host.UseWolverine(opts => { opts.BatchMessagesOf<SubTaskCompleted>(batching => { batching.BatchSize = 500; batching.TriggerTime = 1.Seconds(); }); });
Batch Handler
csharppublic static class ItemBatchHandler { public static void Handle(Item[] items, IRepository repository) { foreach (var item in items) { repository.Process(item); } } }
Custom Batching Strategy
csharppublic record SubTaskCompleted(string TaskId, string SubTaskId); public record SubTaskBatch(string TaskId, string[] SubTaskIds); public class SubTaskBatcher : IMessageBatcher { public IEnumerable<Envelope> Group(IReadOnlyList<Envelope> envelopes) { var groups = envelopes .GroupBy(x => x.Message!.As<SubTaskCompleted>().TaskId); foreach (var group in groups) { var subTaskIds = group .Select(x => x.Message) .OfType<SubTaskCompleted>() .Select(x => x.SubTaskId) .ToArray(); yield return new Envelope( new SubTaskBatch(group.Key, subTaskIds), group); } } public Type BatchMessageType => typeof(SubTaskBatch); }
Error Handling
Retry Policies
csharpbuilder.Host.UseWolverine(opts => { opts.Policies.OnException<SqlException>() .RetryWithCooldown(50.Milliseconds(), 100.Milliseconds(), 250.Milliseconds()); opts.Policies.OnException<TimeoutException>() .RetryTimes(3); opts.Policies.OnException<InvalidOperationException>() .Requeue(); opts.Policies.OnAnyException() .MoveToErrorQueue(); });
Circuit Breaker
csharpopts.ListenToRabbitQueue("incoming") .CircuitBreaker(cb => { cb.PauseTime = 1.Minutes(); cb.FailurePercentageThreshold = 50; cb.MinimumThreshold = 10; });
Scheduled Messages
Delayed Messages
csharppublic static IEnumerable<object> Handle(OrderCreated command) { yield return new ProcessPayment(command.OrderId); yield return new ShipOrder(command.OrderId) .DelayedFor(30.Minutes()); }
Scheduled at Specific Time
csharpyield return new GenerateReport() .ScheduledAt(DateTime.Today.AddDays(1));
Using OutgoingMessages
csharpvar messages = new OutgoingMessages(); messages.Delay(new Reminder(orderId), TimeSpan.FromHours(24)); messages.Schedule(new MonthlyReport(), DateTime.Today.AddMonths(1));
Request/Reply Pattern
Sending with Response Request
csharppublic async Task<OrderStatus> GetOrderStatus(IMessageBus bus, Guid orderId) { return await bus.InvokeAsync<OrderStatus>(new GetOrder(orderId)); }
Handler with Response
csharppublic record GetOrder(Guid OrderId); public record OrderStatus(Guid OrderId, string Status); public static class GetOrderHandler { public static OrderStatus Handle(GetOrder query, IQuerySession session) { var order = session.Load<Order>(query.OrderId); return new OrderStatus(query.OrderId, order?.Status ?? "NotFound"); } }
Middleware
Custom Middleware
csharppublic class LoggingMiddleware { public void Before(Envelope envelope, ILogger logger) { logger.LogInformation("Processing {MessageType}", envelope.MessageType); } public void After(Envelope envelope, ILogger logger) { logger.LogInformation("Completed {MessageType}", envelope.MessageType); } } builder.Host.UseWolverine(opts => { opts.Handlers.AddMiddleware<LoggingMiddleware>(); });
Transaction Middleware
csharpbuilder.Host.UseWolverine(opts => { opts.Policies.AutoApplyTransactions(); });
Code Generation
Pre-Generate Types
bashdotnet run -- codegen write
Generated code appears in ./Internal/Generated/WolverineHandlers/
Configure for AOT/Trimming
csharpbuilder.Host.UseWolverine(opts => { opts.CodeGeneration.TypeLoadMode = TypeLoadMode.Auto; });
Multi-Tenancy
Conjoined Tenancy
csharpbuilder.Host.UseWolverine(opts => { opts.Policies.ConjoinedTenancy(x => { x.TenantIdStyle = TenantIdStyle.PerRequest; }); });
Publishing to Specific Tenant
csharpawait bus.PublishAsync(new OrderCreated(orderId), new DeliveryOptions { TenantId = "tenant-1" });
Marten Side Effects
csharppublic static IMartenOp Handle(CreateTodo command) { var todo = new Todo { Name = command.Name }; return MartenOps.Store(todo); } public static IMartenOp Handle(DeleteTodo command) { return MartenOps.Delete<Todo>(command.Id); }
Ancillary Stores (Modular Monolith)
csharppublic interface IPlayerStore : IDocumentStore; builder.Host.UseWolverine(opts => { opts.Services.AddMartenStore<IPlayerStore>(m => { m.Connection(connectionString); m.DatabaseSchemaName = "players"; }) .IntegrateWithWolverine(); }); [MartenStore(typeof(IPlayerStore))] public static class PlayerMessageHandler { public static IMartenOp Handle(PlayerMessage message) { return MartenOps.Store(new Player { Id = message.Id }); } }
Command Line Tools
Available Commands
bashdotnet run -- help # List all commands dotnet run -- describe # Application description dotnet run -- resources # Resource management dotnet run -- storage # Message storage admin dotnet run -- codegen write # Pre-generate code
Best Practices
- Prefer pure functions - Business logic should be testable without mocks
- Use cascading messages - Return messages instead of injecting IMessageBus
- Keep call stacks short - Avoid deep service hierarchies
- Pre-generate code - Optimize cold starts in production
- Use compound handlers - Separate load/validate/handle logic
- Configure error handling - Let Wolverine handle retries and errors
- Use transactional outbox - Guarantee message delivery
- Batch when appropriate - Improve throughput for high-volume messages
Anti-Patterns to Avoid
- Injecting IMessageBus deep in call stack - Makes workflow hard to reason about
- Over-using constructor injection - Prefer method injection
- Ignoring transactional outbox - Can lose messages on failure
- Not pre-generating code - Slow cold starts in production
- Mixing too many concerns in one handler - Keep handlers focused
- Not configuring error handling - Messages end up in error queue unexpectedly

