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.
Package structure
Section titled “Package structure”DirectoryGranit.Wolverine/ Core: routing, context propagation, FluentValidation middleware
- Granit.Wolverine.Postgresql PostgreSQL outbox, EF Core transactions
| Package | Role | Depends on |
|---|---|---|
Granit.Wolverine | Domain event routing, context propagation, validation | Granit.Security |
Granit.Wolverine.Postgresql | PostgreSQL transactional outbox, EF Core integration | Granit.Wolverine, Granit.Persistence |
Dependency graph
Section titled “Dependency graph”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=..." }}[DependsOn(typeof(GranitWolverineModule))]public class AppModule : GranitModule { }No outbox — messages are in-memory only. Lost on crash.
[DependsOn(typeof(GranitWolverinePostgresqlModule))]public class AppModule : GranitModule{ public override void ConfigureServices(ServiceConfigurationContext context) { context.Builder.AddGranitWolverineWithPostgresqlPerTenant<AppDbContext>(); }}Each tenant’s messages persist in their own database (strictest ISO 27001 isolation).
Domain events vs integration events
Section titled “Domain events vs integration events”| Domain event | Integration event | |
|---|---|---|
| Interface | IDomainEvent | IIntegrationEvent |
| Naming | PatientDischargedOccurred | BedReleasedEvent |
| Scope | In-process, same transaction | Cross-module, durable |
| Transport | Local queue ("domain-events") | Outbox → PostgreSQL transport |
| Retry | Yes (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;Handler conventions
Section titled “Handler conventions”Discovery
Section titled “Discovery”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).
FluentValidation
Section titled “FluentValidation”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.
Context propagation
Section titled “Context propagation”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
| Header | Source | Behavior |
|---|---|---|
X-Tenant-Id | ICurrentTenant.Id | TenantContextBehavior restores AsyncLocal |
X-User-Id | ICurrentUserService.UserId | UserContextBehavior restores via WolverineCurrentUserService |
X-User-FirstName | ICurrentUserService.FirstName | Propagated for audit trail |
X-User-LastName | ICurrentUserService.LastName | Propagated for audit trail |
X-Actor-Kind | ICurrentUserService.ActorKind | User, ExternalSystem, or System |
X-Api-Key-Id | ICurrentUserService.ApiKeyId | Service account identification |
traceparent | Activity.Current?.Id | W3C Trace Context for distributed tracing |
This means AuditedEntityInterceptor populates CreatedBy/ModifiedBy correctly
even in background handlers — the user context travels with the message.
Transactional outbox
Section titled “Transactional outbox”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:
| Mode | Behavior | Use case |
|---|---|---|
Eager (default) | Explicit BeginTransactionAsync() | ISO 27001 compliance |
Lightweight | SaveChangesAsync()-level isolation | Non-critical operations |
Claim Check pattern
Section titled “Claim Check pattern”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 callvar report = await claimCheck.ConsumePayloadAsync<byte[]>( reference, cancellationToken) .ConfigureAwait(false);Register with services.AddInMemoryClaimCheckStore() for development.
Production implementations use blob storage.
Retry policy
Section titled “Retry policy”{ "Wolverine": { "RetryDelays": ["00:00:05", "00:00:30", "00:05:00"], "MaxRetryAttempts": 3 }}| Property | Default | Description |
|---|---|---|
RetryDelays | [5s, 30s, 5min] | Delay between retries |
MaxRetryAttempts | 3 | Max attempts before dead letter |
ValidationException bypasses retries entirely — sent directly to the error queue.
Wolverine is optional
Section titled “Wolverine is optional”Wolverine is not required to use Granit. These modules have built-in Channel<T>
fallbacks when Wolverine is not installed:
| Module | Without Wolverine | With Wolverine |
|---|---|---|
Granit.BackgroundJobs | In-memory Channel | Durable outbox |
Granit.Notifications | In-memory Channel | Durable outbox |
Granit.Webhooks | In-memory Channel | Durable outbox |
Granit.DataExchange | In-memory Channel | Durable outbox |
Granit.Persistence.Migrations | In-memory Channel | Durable outbox |
Public API summary
Section titled “Public API summary”| Category | Key types | Package |
|---|---|---|
| Module | GranitWolverineModule, GranitWolverinePostgresqlModule | — |
| Options | WolverineMessagingOptions, WolverinePostgresqlOptions | — |
| Context | OutgoingContextMiddleware, TenantContextBehavior, UserContextBehavior, TraceContextBehavior | Granit.Wolverine |
| User service | WolverineCurrentUserService (internal, implements ICurrentUserService) | Granit.Wolverine |
| Claim Check | IClaimCheckStore, ClaimCheckReference | Granit.Wolverine |
| Attributes | [WolverineHandlerModule] | Granit.Wolverine |
| Extensions | AddGranitWolverine(), AddGranitWolverineWithPostgresql(), AddGranitWolverineWithPostgresqlPerTenant<T>(), AddInMemoryClaimCheckStore() | — |
See also
Section titled “See also”- Core module —
IDomainEvent,IIntegrationEvent,IDomainEventDispatcher - Persistence module —
DomainEventDispatcherInterceptor, transactional outbox - Multi-tenancy module — Tenant context propagation
- Security module —
ICurrentUserServicepropagation - API Reference (auto-generated from XML docs)