Skip to content

Granit.Wolverine

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.Security
Granit.Wolverine.PostgresqlPostgreSQL transactional outbox, EF Core integrationGranit.Wolverine, Granit.Persistence
graph TD
    W[Granit.Wolverine] --> S[Granit.Security]
    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()