Skip to content

Observer / Event

The Observer pattern establishes a one-to-many relationship between objects: when an object (subject) changes state, all its observers are notified automatically. In Granit, Wolverine acts as the subscription mechanism — handlers are discovered by convention and implicitly subscribe to the message types they process.

sequenceDiagram
    participant P as Publisher
    participant W as Wolverine Bus
    participant H1 as FeatureCacheInvalidationHandler
    participant H2 as AuditLogHandler
    participant H3 as WebhookFanoutHandler

    P->>W: Publish FeatureValueChangedEvent
    W->>H1: HandleAsync() -- purge cache
    W->>H2: HandleAsync() -- audit log

    P->>W: Publish WebhookTrigger
    W->>H3: HandleAsync() -- fan-out to subscriptions

Wolverine automatically discovers handlers by naming convention. A handler that accepts a parameter of type T implicitly subscribes to all messages of type T.

Handler (observer)Event (subject)File
FeatureCacheInvalidationHandlerFeatureValueChangedEventsrc/Granit.Features/Cache/FeatureCacheInvalidationHandler.cs
WebhookFanoutHandlerWebhookTriggersrc/Granit.Webhooks/Handlers/WebhookFanoutHandler.cs

Wolverine handlers can return events via yield return or IEnumerable<T>. Wolverine automatically dispatches these events to registered observers.

Implicit observation via Wolverine eliminates coupling between the publisher and observers. The publisher does not know how many observers exist or what they do. Adding a new observer requires no modification to the publisher.

// Publisher -- knows no observers
public static class UpdateFeatureValueHandler
{
public static IEnumerable<object> Handle(
UpdateFeatureValueCommand command,
IFeatureStore store)
{
store.SetAsync(command.TenantId, command.FeatureName, command.Value);
// Wolverine dispatches this event to all registered handlers
yield return new FeatureValueChangedEvent
{
TenantId = command.TenantId,
FeatureName = command.FeatureName
};
}
}
// Observer 1 -- discovered automatically
public static class FeatureCacheInvalidationHandler
{
public static async Task Handle(
FeatureValueChangedEvent evt,
HybridCache cache)
{
string cacheKey = FeatureCacheKey.Build(evt.TenantId, evt.FeatureName);
await cache.RemoveAsync(cacheKey);
}
}
// Observer 2 -- added later, no modification to publisher
public static class FeatureAuditLogHandler
{
public static void Handle(FeatureValueChangedEvent evt, ILogger logger)
{
logger.LogInformation("[AUDIT] Feature {Feature} changed for tenant {Tenant}",
evt.FeatureName, evt.TenantId);
}
}