Wolverine
WolverineFx is a .NET runtime for command execution and message handling. One mental model covers many use cases: a method handles a message; the rest is just where the message comes from and what happens after it succeeds.
- In-process mediator — invoke a command, run a handler. Lower ceremony than MediatR.
- Local message bus — fire-and-forget across in-memory queues with optional durability.
- Distributed messaging — same handler model, fronted by RabbitMQ, Azure Service Bus, SQS/SNS, Kafka, NATS, Pulsar, MQTT, Redis, GCP Pub/Sub, PostgreSQL, SQL Server, etc.
- HTTP endpoints —
WolverineFx.Httpexposes methods as ASP.NET Core endpoints with the same conventions. - Durable messaging — inbox/outbox on top of Marten, EF Core, or RavenDb for transactional consistency.
- Sagas / process managers — long-running stateful workflows.
CQRS and event sourcing are one thing Wolverine pairs well with (via Marten), but most features apply equally to plain command/event-driven services, modular monoliths, vertical-slice web apps, background workers, and ETL pipelines. Do not assume a Wolverine question implies event sourcing.
When agent context is tight
If you only need the API shape for a specific topic, read the matching reference file below and skip the rest. The list below is the index — load only what you need for the current task.
| Reference | Load when... |
|-----------|-------------|
| references/handlers.md | Writing or modifying a handler: discovery rules, signatures, cascading messages, side effects, return types, error policies, FluentValidation, [Entity] loading. |
| references/messaging.md | Sending/publishing/scheduling messages, routing rules, listener config, local queues, conventional routing, topics. |
| references/transports.md | Configuring a specific broker (RabbitMQ, Azure Service Bus, SQS, Kafka, NATS, Pulsar, MQTT, Redis, PostgreSQL/SQL Server transport). |
| references/http.md | Building or migrating WolverineFx.Http endpoints, mediator-style routes, response/request conventions. |
| references/durability.md | Enabling inbox/outbox, idempotency, dead letter storage, claim checks, transactional middleware. |
| references/persistence.md | Picking/wiring Marten, EF Core, or RavenDb integration; sagas; multi-tenancy; event sourcing aggregate handlers (only when actually using event sourcing). |
| references/middleware-and-policies.md | Writing conventional middleware, policies, attributes; modifying handler chains. |
| references/testing-and-ops.md | Tracked-session integration tests, command-line diagnostics, codegen, logging, health checks, AOT/cold-start tuning. |
| references/patterns.md | Architectural shape: vertical-slice, modular monolith, mediator-only, railway, ping-pong, leader election, MediatR/MVC/MinAPI migration. |
Core mental model (always-loaded)
A Wolverine application is WolverineOptions configured on the .NET Generic Host:
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseWolverine(opts =>
{
// routing, transports, durability, policies go here
});
var app = builder.Build();
// Opt into JasperFx CLI commands for `dotnet run -- describe|codegen|...`
return await app.RunJasperFxCommands(args);
IMessageBus is scoped, injected via DI, and is the single entry point at
runtime:
await bus.InvokeAsync(new DebitAccount(1111, 250)); // run handler now, await result
var status = await bus.InvokeAsync<AccountStatus>(cmd); // request/response
await bus.SendAsync(new DebitAccount(1111, 250)); // requires at least one subscriber
await bus.PublishAsync(new AccountOverdrawn(1111)); // fire-and-forget, OK with 0 subscribers
await bus.ScheduleAsync(new ReminderDue(id), 1.Days()); // delayed
A handler is any public method on a public class with a name like
Handle / HandleAsync / Consume / ConsumeAsync on a class suffixed
Handler or Consumer (or marked [WolverineHandler] / IWolverineHandler).
The first parameter is the message; the rest are method-injected from DI:
public static class DebitAccountHandler
{
public static IssueDebited Handle(DebitAccount cmd, IDocumentSession session)
{
// ...mutate state, return event
return new IssueDebited(cmd.AccountId);
}
}
Return values are not just data — they're cascading messages, side effects, or storage actions (see handlers.md). This is what lets handlers stay pure functions.
Critical conventions and gotchas
These are the things agents trip over. Read this section before touching unfamiliar handler code.
- No runtime reflection in the hot path. Wolverine generates an adapter class per handler at startup (or ahead of time).
dotnet run -- codegen writewrites the generated code toInternal/Generated/so you can read what Wolverine is actually doing — use this when something seems "magic". - IoC must be transparent. Prefer
AddSingleton<T>()/AddScoped<TInterface, TImpl>()/ constructor injection. Opaque lambda factories (AddScoped<T>(sp => ...)) that handlers consume now throw at startup (Wolverine 6); useopts.CodeGeneration.AlwaysUseServiceLocationFor<T>()only as a deliberate escape hatch. - Public-everything rule. Handler types, handler methods, and message types must all be public. So must FluentValidation validators (or set
IncludeInternalTypes). - Don't abstract
IMessageBus. Inject it as a method parameter when you need it; otherwise prefer returning cascading messages so handlers stay testable as pure functions. - Discovery is allow-list based. Only the application assembly is scanned by default. Other assemblies need
[assembly: WolverineModule]oropts.Discovery.IncludeAssembly(...). - Generated code is not auto-regenerated. When you change a handler signature or middleware while using
Static/Autocodegen mode, delete the stale file underInternal/Generated/(or rundotnet run -- codegen write). InvokeAsynconly auto-applies Retry policies from your error rules.Requeue,Discard,MoveToErrorQueue, andPauseThenRequeueare silently ignored when invoking inline — exhausted retries propagate the exception back to the caller. If you need dead-letter semantics on synchronous invocation, catch it yourself or usebus.SendAsyncso a listener processes it.SendAsyncthrows when no subscriber exists —IndicatesNoHandlersException. If a handler "doesn't fire" afterSendAsync, check the exception first (see testing-and-ops.md → diagnostics recipes); usePublishAsyncfor events with 0+ subscribers.- Fire-and-forget is only durable with the outbox.
PublishAsync/SendAsyncare in-memory by default. A crash between the handler returning and the broker accepting the message loses the event. Enable the outbox in durability.md when you need guaranteed delivery — coming from MediatR's in-processINotificationmodel, this is the trap most teams hit first. - Don't double-commit. With
opts.Policies.AutoApplyTransactions()or[Transactional], the middleware callsSaveChangesAsync()/ MartenSaveChangesAsync()for you and flushes the outbox in the same transaction. Calling it yourself produces a second commit and breaks atomicity. - Don't read CQRS/event-sourcing language into a Wolverine question. Wolverine handles plain commands and events just as well; only the Marten integration adds event-sourcing-specific helpers (
[Aggregate],IEvent<T>, event forwarding). Skip persistence.md's event-sourcing section unless the user is actually usingIDocumentSession.Eventsor Marten projections. - Local queues are real. A message with a known handler is, by default, routed to a per-message-type in-process queue — not invoked synchronously. Use
InvokeAsyncif you need synchronous semantics; configureopts.LocalQueue(...)to tune parallelism and durability.
Diagnostics first
Before debugging routing, handler discovery, or codegen issues, run:
dotnet run -- describe # full configuration dump: handlers, routes, endpoints, options
dotnet run -- codegen preview # see the generated adapter code Wolverine produced
dotnet run -- codegen write # persist generated code into Internal/Generated/
dotnet run -- check-env # validate environment & connectivity (transports, message store)
dotnet run -- resources setup # provision broker/db resources Wolverine knows about
opts.DescribeHandlerMatch(typeof(SomeHandler)) prints a textual report
explaining why Wolverine did or did not pick a type up as a handler. Reach
for it when handlers "go missing".
Picking the right next reference
- "I need to write/edit a handler" →
handlers.md - "How do I send/publish/route messages" →
messaging.md, plustransports.mdfor the specific broker - "I'm wiring up RabbitMQ / Kafka / SQS / ASB / NATS / ..." →
transports.md - "HTTP endpoint with Wolverine" →
http.md - "Transactional outbox / dead-letter / idempotency" →
durability.md, pluspersistence.mdfor the store - "Saga / long-running workflow" →
persistence.md(sagas section) - "Custom middleware / cross-cutting policy" →
middleware-and-policies.md - "Integration test / cold-start / CLI / logging" →
testing-and-ops.md - "Migrate from MediatR/MVC/MinAPI" or "should this be a modular monolith" →
patterns.md - "I just want a pure CQRS mediator, no broker, no ES" →
patterns.md#mediator-only(start here for MediatR-replacement use cases)