Skip to content

Wolverine — Messaging & Command Bus for .NET

Granit.Wolverine integrates the Wolverine message bus into the module system. Domain events route to local queues, integration events persist in a transactional outbox, and tenant/user/trace context propagates automatically across async message processing. FluentValidation runs as bus middleware — invalid commands go straight to the error queue, no retry.

  • DirectoryGranit.Wolverine/ Core: routing, context propagation, FluentValidation middleware
    • Granit.Wolverine.Postgresql PostgreSQL outbox, EF Core transactions
PackageRoleDepends on
Granit.WolverineDomain event routing, context propagation, validationGranit.Users
Granit.Wolverine.PostgresqlPostgreSQL transactional outbox, EF Core integrationGranit.Wolverine, Granit.Persistence
graph TD
    W[Granit.Wolverine] --> S[Granit.Users]
    WP[Granit.Wolverine.Postgresql] --> W
    WP --> P[Granit.Persistence]
[DependsOn(typeof(GranitWolverinePostgresqlModule))]
public class AppModule : GranitModule { }
{
"Wolverine": {
"MaxRetryAttempts": 3,
"RetryDelays": ["00:00:05", "00:00:30", "00:05:00"]
},
"WolverinePostgresql": {
"TransportConnectionString": "Host=db;Database=myapp;Username=app;Password=..."
}
}
Domain eventIntegration event
InterfaceIDomainEventIIntegrationEvent
NamingPatientDischargedOccurredBedReleasedEvent
ScopeIn-process, same transactionCross-module, durable
TransportLocal queue ("domain-events")Outbox → PostgreSQL transport
RetryYes (configurable delays)Yes (at-least-once delivery)
public sealed record PatientDischargedOccurred(
Guid PatientId, Guid BedId) : IDomainEvent;
public sealed record BedReleasedEvent(
Guid BedId, Guid WardId, DateTimeOffset ReleasedAt) : IIntegrationEvent;

Mark handler assemblies with [WolverineHandlerModule] — Granit auto-discovers handlers and validators:

[assembly: WolverineHandlerModule]
namespace MyApp.Handlers;
public static class DischargePatientHandler
{
public static IEnumerable<object> Handle(
DischargePatientCommand command,
PatientDbContext db)
{
var patient = db.Patients.Find(command.PatientId)
?? throw new EntityNotFoundException(typeof(Patient), command.PatientId);
patient.Discharge();
// Domain event — same transaction
yield return new PatientDischargedOccurred(patient.Id, patient.BedId);
// Integration event — persisted in outbox
yield return new BedReleasedEvent(
patient.BedId, patient.WardId, DateTimeOffset.UtcNow);
}
}

Handlers returning IEnumerable<object> produce multiple outbox messages atomically (fan-out pattern).

Validators run as bus middleware before handler execution:

public class DischargePatientCommandValidator
: AbstractValidator<DischargePatientCommand>
{
public DischargePatientCommandValidator()
{
RuleFor(x => x.PatientId).NotEmpty();
}
}

ValidationException goes directly to the error queue — no retry. Other exceptions follow the retry policy.

Three contexts automatically propagate through message envelopes:

sequenceDiagram
    participant HTTP as HTTP Request
    participant Out as OutgoingMiddleware
    participant Env as Message Envelope
    participant In as Incoming Behaviors
    participant Handler as Handler

    HTTP->>Out: TenantId, UserId, TraceId
    Out->>Env: X-Tenant-Id, X-User-Id, traceparent
    Env->>In: Read headers
    In->>Handler: ICurrentTenant, ICurrentUserService, Activity
HeaderSourceBehavior
X-Tenant-IdICurrentTenant.IdTenantContextBehavior restores AsyncLocal
X-User-IdICurrentUserService.UserIdUserContextBehavior restores via WolverineCurrentUserService
X-User-FirstNameICurrentUserService.FirstNamePropagated for audit trail
X-User-LastNameICurrentUserService.LastNamePropagated for audit trail
X-Actor-KindICurrentUserService.ActorKindUser, ExternalSystem, or System
X-Api-Key-IdICurrentUserService.ApiKeyIdService account identification
traceparentActivity.Current?.IdW3C Trace Context for distributed tracing

This means AuditedEntityInterceptor populates CreatedBy/ModifiedBy correctly even in background handlers — the user context travels with the message.

The PostgreSQL outbox guarantees at-least-once delivery by persisting messages in the same transaction as business data:

sequenceDiagram
    participant H as Handler
    participant DB as PostgreSQL
    participant O as Outbox
    participant T as Transport

    H->>DB: UPDATE patients SET ...
    H->>O: INSERT outbox message
    H->>DB: COMMIT (atomic)
    Note over DB,O: Both succeed or both rollback
    O->>T: Dispatch post-commit
    T->>O: ACK → DELETE from outbox

Transaction modes:

ModeBehaviorUse case
Eager (default)Explicit BeginTransactionAsync()ISO 27001 compliance
LightweightSaveChangesAsync()-level isolationNon-critical operations

For large payloads that shouldn’t travel through the message bus:

public class LargeReportHandler(IClaimCheckStore claimCheck)
{
public async Task<ClaimCheckReference> Handle(
GenerateReportCommand command, CancellationToken cancellationToken)
{
byte[] reportData = await GenerateReportAsync(command, cancellationToken)
.ConfigureAwait(false);
return await claimCheck.StorePayloadAsync(
reportData, expiry: TimeSpan.FromHours(1), cancellationToken)
.ConfigureAwait(false);
}
}
// Consumer retrieves and deletes in one call
var report = await claimCheck.ConsumePayloadAsync<byte[]>(
reference, cancellationToken)
.ConfigureAwait(false);

Register with services.AddInMemoryClaimCheckStore() for development. Production implementations use blob storage.

{
"Wolverine": {
"RetryDelays": ["00:00:05", "00:00:30", "00:05:00"],
"MaxRetryAttempts": 3
}
}
PropertyDefaultDescription
RetryDelays[5s, 30s, 5min]Delay between retries
MaxRetryAttempts3Max attempts before dead letter

ValidationException bypasses retries entirely — sent directly to the error queue.

Wolverine is not required to use Granit. These modules have built-in Channel<T> fallbacks when Wolverine is not installed:

ModuleWithout WolverineWith Wolverine
Granit.BackgroundJobsIn-memory ChannelDurable outbox
Granit.NotificationsIn-memory ChannelDurable outbox
Granit.WebhooksIn-memory ChannelDurable outbox
Granit.DataExchangeIn-memory ChannelDurable outbox
Granit.Persistence.MigrationsIn-memory ChannelDurable outbox
CategoryKey typesPackage
ModuleGranitWolverineModule, GranitWolverinePostgresqlModule
OptionsWolverineMessagingOptions, WolverinePostgresqlOptions
ContextOutgoingContextMiddleware, TenantContextBehavior, UserContextBehavior, TraceContextBehaviorGranit.Wolverine
User serviceWolverineCurrentUserService (internal, implements ICurrentUserService)Granit.Wolverine
Claim CheckIClaimCheckStore, ClaimCheckReferenceGranit.Wolverine
Attributes[WolverineHandlerModule]Granit.Wolverine
ExtensionsAddGranitWolverine(), AddGranitWolverineWithPostgresql(), AddGranitWolverineWithPostgresqlPerTenant<T>(), AddInMemoryClaimCheckStore()
SymptomLikely causeFix
Message published but handler never runsHandler not discoveredAdd [assembly: WolverineHandlerModule] to the assembly
TenantId is null in handlerContext middleware not configuredVerify AddGranitWolverine() is called
Messages retry indefinitelyNo dead letter policy configuredCheck WithFailureRules() in Wolverine options
PostgreSQL transport errors on startupMissing PostgreSQL transport registrationCall AddGranitWolverineWithPostgresql() instead of AddGranitWolverine()
Slow message throughputToo many handlers in same queueSplit into dedicated queues with [Queue("name")]