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
<PackageReference Include="Wolverine" />
<PackageReference Include="WolverineFx.Http" />
Persistence Integration
<PackageReference Include="WolverineFx.Marten" />
Transports
<PackageReference Include="WolverineFx.RabbitMQ" />
<PackageReference Include="WolverineFx.AzureServiceBus" />
<PackageReference Include="WolverineFx.Kafka" />
<PackageReference Include="WolverineFx.AmazonSQS" />
Basic Setup
Program.cs (ASP.NET Core)
using 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
public 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
public 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
public 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
[WolverineGet("/users/{id}")]
public static Task<User?> GetUser(int id, IQuerySession session)
=> session.LoadAsync<User>(id);
POST with Message Publishing
[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)
public 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
public 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)
[WolverineGet("/orders/{id}")]
public static Order GetOrder([ReadAggregate] Order order) => order;
Write Aggregate with Validation
public 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
[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
builder.Services.AddMarten(opts =>
{
opts.Connection(connectionString);
}).IntegrateWithWolverine();
builder.Host.UseWolverine(opts =>
{
opts.Policies.AutoApplyTransactions();
});
Using Outbox in Controllers
[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
builder.Host.UseWolverine(opts =>
{
opts.UseRabbitMq("host=localhost")
.AutoProvision()
.AutoPurgeOnStartup();
opts.PublishAllMessages()
.ToRabbitExchange("wolverine.events", exchange =>
{
exchange.ExchangeType = ExchangeType.Topic;
});
});
Azure Service Bus
builder.Host.UseWolverine(opts =>
{
opts.UseAzureServiceBus(asbConnectionString)
.AutoProvision()
.ConfigureQueue(q => q.MaxDeliveryCount = 5);
});
Amazon SQS
builder.Host.UseWolverine(opts =>
{
opts.UseAmazonSqs(sqsConfig)
.AutoProvision();
});
Batch Message Processing
Configure Batching
builder.Host.UseWolverine(opts =>
{
opts.BatchMessagesOf<SubTaskCompleted>(batching =>
{
batching.BatchSize = 500;
batching.TriggerTime = 1.Seconds();
});
});
Batch Handler
public static class ItemBatchHandler
{
public static void Handle(Item[] items, IRepository repository)
{
foreach (var item in items)
{
repository.Process(item);
}
}
}
Custom Batching Strategy
public 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
builder.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
opts.ListenToRabbitQueue("incoming")
.CircuitBreaker(cb =>
{
cb.PauseTime = 1.Minutes();
cb.FailurePercentageThreshold = 50;
cb.MinimumThreshold = 10;
});
Scheduled Messages
Delayed Messages
public 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
yield return new GenerateReport()
.ScheduledAt(DateTime.Today.AddDays(1));
Using OutgoingMessages
var 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
public async Task<OrderStatus> GetOrderStatus(IMessageBus bus, Guid orderId)
{
return await bus.InvokeAsync<OrderStatus>(new GetOrder(orderId));
}
Handler with Response
public 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
public 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
builder.Host.UseWolverine(opts =>
{
opts.Policies.AutoApplyTransactions();
});
Code Generation
Pre-Generate Types
dotnet run -- codegen write
Generated code appears in ./Internal/Generated/WolverineHandlers/
Configure for AOT/Trimming
builder.Host.UseWolverine(opts =>
{
opts.CodeGeneration.TypeLoadMode = TypeLoadMode.Auto;
});
Multi-Tenancy
Conjoined Tenancy
builder.Host.UseWolverine(opts =>
{
opts.Policies.ConjoinedTenancy(x =>
{
x.TenantIdStyle = TenantIdStyle.PerRequest;
});
});
Publishing to Specific Tenant
await bus.PublishAsync(new OrderCreated(orderId),
new DeliveryOptions { TenantId = "tenant-1" });
Marten Side Effects
public 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)
public 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
dotnet 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