Use Wolverine when you need:

  • High-performance in-process command handling (Mediator replacement)
  • Durable messaging with Inbox / Outbox pattern
  • Seamless transactional consistency between database & messaging
  • A single framework for:
    • Commands
    • Events
    • Background processing
    • External brokers
  • Minimal ceremony compared to MassTransit / NServiceBus

Avoid Wolverine when:

  • You only need a simple mediator with no async messaging
  • Your team already standardized on MassTransit or NServiceBus
  • You need advanced Saga orchestration out of the box

Installation & Setup

Core Package

dotnet add package WolverineFx

Entity Framework Core Integration

dotnet add package WolverineFx.EntityFrameworkCore

 

Register Wolverine in Startup

builder.Host.UseWolverine(opts =>
{
    // configuration here
});

 


Define Messages & Handlers

Message (Command / Event)

public class ItemCommand
{
    public long Id { get; set; }
}

Handler

public class ItemHandler
{
    public async Task Handle(ItemCommand command, AppDbContext dbContext)
    {
        // handle command
    }
}

Handler Discovery Conventions

A handler is detected if:

  • Class name ends with Handler or Consumer
  • OR decorated with [WolverineHandler]
  • OR implements IWolverineHandler

 

Static vs Instance Handlers

public static class StaticHandler
{
    public static Task Handle(ItemCommand command) { }
}
  • Static handlers are preferred for performance
  • Instance handlers allow constructor injection
  • Both are supported

 

Handler Return Values (Chained Messaging)

public class CreateOrderHandler
{
    public OrderCreated Handle(CreateOrder command)
    {
        return new OrderCreated(command.Id);
    }
}
  • Returned messages are automatically published or sent
  • Wolverine will look for handlers for returned message types
  • Enables pipeline-style workflows

 

Transaction & EF Core Behavior

If a handler has DbContext as a parameter:

  • Wolverine automatically wraps handler execution in a transaction
  • Outgoing messages are stored in Outbox within the same transaction
  • If the handler fails:
    • DB changes are rolled back
    • Outgoing messages are NOT sent

This guarantees atomicity between state changes and messaging.

 


In-Process Command Handling (Mediator Pattern)

app.MapGet("/test", async ([FromServices] IMessageBus bus) =>
{
    await bus.InvokeAsync(new ItemCommand { Id = 444 });
});

Notes

  • Inject IMessageBus
  • Use InvokeAsync
  • Executes in-process
  • If handler returns an output, Wolverine will try to find a matching handler for it

Asynchronous Messaging & Routing (SendAsync)

Command with Exactly One Handler

app.MapGet("/test", async ([FromServices] IMessageBus bus) =>
{
    await bus.SendAsync(new ItemCommand { Id = 444 });
});

Characteristics

  • Uses SendAsync
  • Exactly one handler required
  • If no handler found, will throw exception
  • Runs in background
  • Durable (Inbox / Outbox supported)
  • To internal service or external brokers

 


Event with Multiple or Optional Handlers(PublishAsync)

app.MapGet("/test", async ([FromServices] IMessageBus bus) =>
{
    await bus.PublishAsync(new ItemEvent { Id = 444 });
});

Characteristics

  • Handler is not mandatory
  • Multiple handlers allowed
  • Runs in background
  • Durable(Inbox / Outbox supported)
  • To internal service or external brokers

 

 

Scheduled / Delayed Messages

// One-off Delay
await bus.ScheduleAsync( new ItemCommand { Id = 444 },
                         TimeSpan.FromMinutes(10) );


// with absolute time:
await bus.ScheduleAsync(new ExpireOrder { OrderId = id },
                        DateTimeOffset.UtcNow.AddHours(1));

Use cases:

  • Delayed processing
  • Time-based workflows
  • Retry with backoff
  • Not Repeating
  • Survives restarts
  • Durable ( Outbox / SqlServer)

 


Wolverine Scheduled Jobs (Cron-style)

Wolverine has a separate concept called Scheduled Jobs. These are not messages, but background jobs that can send messages.

Sample Scheduled Job

public class CleanupExpiredOrders
{
    public async Task ExecuteAsync(IMessageBus bus)
    {
        await bus.SendAsync(new CleanupExpiredOrdersCommand());
    }
}

Register with Cron Expression

builder.Host.UseWolverine(opts =>
{
    opts.Schedule.CronJob<CleanupExpiredOrders>("0 */5 * * * ?");
});

👉 This cron expression uses Quartz-style cron.

 

Cron Expression Support (Important)

  • Format: Quartz cron, , not Linux cron
  • Supports seconds field
  • Example:
    • Every 5 minutes
      0 */5 * * * ? 
    • Every day at 02:00
      0 0 2 * * ? 

Competing Consumers & Scaling

  • Wolverine supports multiple consumers on the same queue
  • Messages are load-balanced automatically
  • Enables horizontal scaling without extra configuration

 


Middleware Pipeline (Russian Doll Model)

Wolverine middleware is similar in concept to ASP.NET Core but implemented differently.

Example Middleware

public static class StopwatchMiddleware
{
    public static Stopwatch Before()
    {
        var sw = Stopwatch.StartNew();
        return sw;
    }


    public static void Finally( Stopwatch stopwatch,  ILogger logger,  Envelope envelope)
    {
        stopwatch.Stop();
        logger.LogDebug( "Envelope {Id} / {MessageType} ran in {Duration} ms",
                                                 envelope.Id,  envelope.MessageType,  stopwatch.ElapsedMilliseconds);
    }
}

 

Supported Middleware Method Conventions

Lifecycle Phase

Method Names

Before handler

Before , BeforeAsync , Load , LoadAsync , Validate , ValidateAsync 

After handler

After , AfterAsync , PostProcess , PostProcessAsync 

Finally

Finally , FinallyAsync 

 

Generated Execution Flow

middleware.Before();
try
{
    // handler execution
    middleware.After();
}
finally
{
    middleware.Finally();
}

 

Register Middleware by Message Type

builder.Host.UseWolverine(opts =>
{
    opts.Policies
        .ForMessagesOfType<IAccountCommand>()
        .AddMiddleware(typeof(AccountLookupMiddleware));
});

 

Apply Middleware Explicitly via Attribute

public static class SomeHandler
{
    [Middleware(typeof(StopwatchMiddleware))]
    public static void Handle(PotentiallySlowMessage message)
    {
        // handler logic
    }
}

 


Enforce Durable Outbox Globally

Activate Inbox / Outbox (Durable Messaging)

dotnet add package WolverineFx.SqlServer
builder.Host.UseWolverine(options =>
{
    options.Policies.UseDurableOutboxOnAllSendingEndpoints();
});

This forces all outgoing messages to use Inbox / Outbox pattern.

 


Automatic Retries

  • Failed message handling is retried automatically
  • Retry count and delay can be configured per message type or endpoint
    opts.Policies
        .ForMessagesOfType<ItemCommand>()
        .Retry(3);

 

Dead Letter Queue

  • Messages that exceed retry limits are moved to dead-letter storage
  • Can be inspected and replayed

 


Idempotency & Inbox Behavior

  • Inbox ensures a message is processed only once
  • Duplicate messages are ignored safely
  • Especially important for:
    • External brokers
    • At-least-once delivery systems

Inbox works automatically when durable messaging is enabled.

 

 


Message Broker

dotnet add package WolverineFx.RabbitMQ
dotnet add package WolverineFx.AzureServiceBus
dotnet add package WolverineFx.Kafka

 

Use RabbitMQ 

builder.Host.UseWolverine(options =>
{
    options
        .UseRabbitMq(new Uri("amqp://localhost:5672"))
        .AutoProvision();
});

 

Use Kafka 

builder.Host.UseWolverine(opts =>
{
    opts.UseKafka(kafka =>
    {
        kafka.BootstrapServers = "localhost:9092";
        kafka.Configure(config =>
        {
            config.SecurityProtocol = SecurityProtocol.SaslPlaintext;
            // or: SecurityProtocol.SaslSsl

            config.SaslMechanism = SaslMechanism.Plain;
            config.SaslUsername ="my-user";
            config.SaslPassword ="my-password";

            // Recommended production defaults
            config.Acks = Acks.All;
            config.EnableIdempotence = true;
        });
    });
});

 

 


Observability & Diagnostics

  • Built-in structured logging per Envelope
  • Envelope ID enables end-to-end tracing
  • Middleware can be used for:
    • Metrics
    • Timing
    • Correlation IDs

Goal

  • End-to-end message tracing
  • Clear visibility into latency, failures, and retries
  • Correlation across HTTP → Message → Handler → Database

1) Correlation ID (Non-Negotiable)

Rule

Every message must have a CorrelationId.

  • Either propagated from HTTP headers
  • Or generated at the message boundary

Correlation & Logging Scope Middleware

public static class CorrelationMiddleware
{
    public static void Before(Envelope envelope, ILogger logger)
    {
        var correlationId = envelope.CorrelationId ?? Guid.NewGuid().ToString("N");
        envelope.CorrelationId = correlationId;

        logger.BeginScope(new Dictionary<string, object>
        {
            ["CorrelationId"] = correlationId,
            ["MessageType"]   = envelope.MessageType,
            ["EnvelopeId"]    = envelope.Id
        });
    }
}

Global Registration

builder.Host.UseWolverine(opts =>
{
    opts.Policies .ForAllMessages()
                    .AddMiddleware(typeof(CorrelationMiddleware));
});

Result

  • All logs share the same correlation context
  • Distributed debugging becomes feasible

 

2) Latency & Performance Measurement

Allocation-Friendly Timing Middleware

public static class TimingMiddleware
{
    public static ValueStopwatch Before()  => ValueStopwatch.StartNew();

    public static void Finally( ValueStopwatch stopwatch,   Envelope envelope,   ILogger logger)
    {
        logger.LogInformation( "Message {MessageType} ({EnvelopeId}) executed in {Elapsed} ms",
            envelope.MessageType,
            envelope.Id,
            stopwatch.GetElapsedTime().TotalMilliseconds);
    }
}

Why this matters:

  • Handler latency is often the first early-warning signal
  • p95 / p99 tells you more than averages

 

3) Failure Visibility (No Silent Errors)

Failure Logging Middleware

public static class ErrorLoggingMiddleware
{
    public static void Finally( Envelope envelope,  ILogger logger,   Exception? exception)
    {
        if (exception == null) return;

        logger.LogError( exception,
            "Message failed: {MessageType} / Envelope {EnvelopeId}",
            envelope.MessageType,
            envelope.Id);
    }
}

Outcome

  • Exceptions are never swallowed
  • Retries and dead-letter events are diagnosable

 

4) Retry Telemetry

Retry Policy with Observability Hooks

builder.Host.UseWolverine(opts =>
{
    opts.Policies  .ForAllMessages()
        .Retry(3)
        .OnRetry((ex, attempt, env) =>
        {
            var logger = opts.Services.GetRequiredService<ILoggerFactory>() .CreateLogger("Wolverine.Retry");

            logger.LogWarning(  ex,
                "Retry {Attempt} for {MessageType} / {EnvelopeId}",
                attempt,
                env.MessageType,
                env.Id);
        });
});

Why

  • Retry storms are invisible without telemetry
  • Retry count is a strong indicator of downstream instability

 

5) OpenTelemetry Tracing

Message-Level Tracing Middleware

public static class OpenTelemetryMiddleware
{
    private static readonly ActivitySource Source =  new("Wolverine.Messages");

    public static Activity Before(Envelope envelope)
    {
        var activity = Source.StartActivity(  envelope.MessageType, ActivityKind.Consumer);
        activity?.SetTag("messaging.message_id", envelope.Id);
        activity?.SetTag("messaging.correlation_id", envelope.CorrelationId);

        return activity!;
    }

    public static void Finally(Activity activity)
    {
        activity?.Dispose();
    }
}

Result

  • Full visibility in Jaeger / Tempo / Application Insights
  • Message handling appears as first-class trace spans

 

6) Metrics & Health Signals

Key signals you should expose:

  • Queue depth
  • Handler execution latency
  • Retry rate
  • Dead-letter count

Metrics Middleware Example

public static class MetricsMiddleware
{
    public static void Finally(  Envelope envelope,   IMetrics metrics)
    {
        metrics.Measure.Counter.Increment( MetricsRegistry.ProcessedMessages,
            tags: new[] { $"type:{envelope.MessageType}" });
    }
}

 

7) Non-Negotiable Observability Checklist

If any of these are missing, you do not have observability:

  • ❌ Correlation IDs
  • ❌ Structured logging
  • ❌ Latency tracking
  • ❌ Retry visibility
  • ❌ Distributed tracing

Wolverine enables these.
It does not configure them for you.

 

 


Message Versioning

Best practices:

  • Avoid breaking changes in message contracts 
  • Use versioned message types: ItemCommandV2 
  • Or additive changes only

Strategy 1⃣ — Explicit Versioned Message Types (Recommended)

V1 Message (Already in Production)

public record CreateOrder
{
    public Guid OrderId { get; init; }
    public decimal Amount { get; init; }
}

V1 Handler

public class CreateOrderHandler
{
    public Task Handle(CreateOrder command)
    {
        // V1 logic
        return Task.CompletedTask;
    }
}

 

V2 Message (Additive Change)

public record CreateOrderV2
{
    public Guid OrderId { get; init; }
    public decimal Amount { get; init; }
    public string Currency { get; init; } = "USD";
}

V2 Handler

public class CreateOrderV2Handler
{
    public Task Handle(CreateOrderV2 command)
    {
        // V2 logic
        return Task.CompletedTask;
    }
}

Coexistence (Critical)

  • V1 producers keep working
  • V2 producers opt-in
  • Zero downtime
  • No conditional logic in handlers

This is the safest pattern

 

Strategy 2 — Event Versioning (Immutable History)

Events are forever.
Never change old ones.

V1 Event

public record OrderPlaced
{
    public Guid OrderId { get; init; }
}

V2 Event

public record OrderPlacedV2
{
    public Guid OrderId { get; init; }
    public DateTime PlacedAt { get; init; }
}

Consumer Supporting Both

public class AccountingConsumer
{
    public Task Handle(OrderPlaced e)
    {
        return HandleInternal(e.OrderId, DateTime.MinValue);
    }

    public Task Handle(OrderPlacedV2 e)
    {
        return HandleInternal(e.OrderId, e.PlacedAt);
    }

    private Task HandleInternal(Guid orderId, DateTime placedAt)
    {
        return Task.CompletedTask;
    }
}

 

💡If using JSON serialization:

  • ✅ Adding fields is safe
  • ❌ Renaming fields is breaking
  • ❌ Changing types is breaking

Safe Evolution

public record PaymentProcessed
{
    public Guid PaymentId { get; init; }
    public decimal Amount { get; init; }
    public string? ProviderReference { get; init; }
}

Old consumers ignore unknown fields.
New consumers get extra data.

 

What NOT to Do ❌ (Real Anti-Patterns)

❌ Conditional Logic in Handler

if (command.Version == 1) { ... }

else { ... }

This rots fast and becomes untestable.

 

❌ Reusing Message Type Name

public record CreateOrder // modified fields

Old messages in queues will break.

 

❌ Removing Fields

Inbox + retries + delayed messages = 💥

 

Deployment Rule (Non-Negotiable)

Safe rollout order

  1. Deploy consumers that support both versions
  2. Deploy new producers
  3. Monitor
  4. Remove old version later (optional)

Never reverse this order.

 

💡Versioning Checklist

  • ✅ Messages treated as public contracts
  • ✅ Additive changes only
  • ✅ Versioned types for breaking changes
  • ❌ No mutation of historical events

 


Bottom Line

If your system:

  • Is distributed
  • Uses retries
  • Uses durable messaging

Then message versioning is mandatory.

Wolverine doesn’t save you from bad contracts.
But it gives you clean tools to evolve them safely.

 

 


Testing Wolverine Handlers (Missing)

In-Memory Message Bus

using var host = await Host.CreateDefaultBuilder()
                                         .UseWolverine()
                                         .StartAsync();

await host .Services
                 .GetRequiredService<IMessageBus>()
                 .InvokeAsync(new ItemCommand());
  • No external broker required
  • Suitable for unit and integration tests
  • Handlers execute synchronously for predictability

 

 


Summary

  • InvokeAsync → synchronous, in-process (mediator style)
  • SendAsync → asynchronous, durable, background execution
  • Middleware supports rich lifecycle hooks
  • Built-in support for Inbox / Outbox and external brokers

 

/