Skip to content

pscschn/event-hooks

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Introduction

This project extends the traditional Observer pattern with contextual, scoped, and lifetime-bound event hooks. At its core is an event hub for publishing and subscribing to events. The system supports both persistent observers and dynamic hooks:

  • Observers are long-lived subscribers that continuously react to events.
  • Hooks are contextual subscriptions that may be constrained by scope, lifetime, groups, or predicates and are automatically removed when no longer needed.

This model enables dynamic and declarative workflows without tightly coupling application layers together.

Instead of propagating state changes and control flow through deep call stacks, components can coordinate through domain events and temporary subscriptions. Infrastructure, UI, and orchestration layers may react to domain activity while remaining strongly separated from the underlying business logic.

The result is a lightweight event orchestration model built on top of familiar observer semantics.

Observers

Observers are long-lived subscribers that react to published events. Subscribing an observer returns a subscription handle that may be used to unsubscribe later. For temporary or workflow-specific subscriptions, hooks are generally a better fit.

public class OrderPlacedObserver : IObserver<OrderPlacedEvent> {
    public async Task HandleAsync(OrderPlacedEvent e) {
        // ...
    }
}
var hub = new EventHub<TestEvent>();
var unsubscriber = await hub.SubscribeAsync(new OrderPlacedObserver());

await hub.PublishAsync(new OrderPlacedEvent());

Hooks

The Basics

Hooks differ from observers in that they react to lifecycle states of an event, not just the event publication itself. By default, hooks are single-use and unsubscribe automatically after their first invocation. The event hub exposes three core hook states:

  • onPublished: invoked when an event of a given type is published
  • onProcessed: invoked after all observers have finished handling the event
  • onError: invoked when an observer throws during event handling
var hub = new EventHub<TestEvent>();

await hub.OnProcessedAsync<OrderApprovedEvent>(async e => { /* ... */ });
await hub.OnPublishedAsync<OrderApprovedEvent>(async e => { /* ... */ });
await hub.OnErrorAsync<OrderApprovedEvent>(async (Exception? e) => { /* ... */ });

Inteception

Hooks can also be composed into a more powerful mechanism: interception. Interception allows you to capture events that are published while another event is being processed. This makes it possible to aggregate related events and react once the parent event has completed. For example, processing an OrderPlacedEvent may emit several OrderUpdatedEvents. Interception can collect those updates and execute a callback once order processing is complete.

var e = new OrderPlacedEvent();;

await hub.InterceptAsync(e, async (IEnumerable<OrderUpdatedEvent> events) => { /* ... */ });

await hub.OnPublishedAsync<OrderPlacedEvent>(async e =>
{
    await hub.PublishAsync(new OrderUpdatedEvent());
    await hub.PublishAsync(new OrderUpdatedEvent());
    await hub.PublishAsync(new OrderUpdatedEvent());
});

await hub.PublishAsync(e);

Advanced Hook Features

Next to the basics and interception, hooks provide a number of other useful features such as:

  • Lifetimes: Each hook can define how many times it may be invoked before it is automatically unsubscribed.
  • Scopes: Scopes tie hooks to a disposable owner and automatically unsubscribe them when the owner is disposed.
  • Groups: Associate hooks with an id that acts filter to all events published with the same id.
  • Predicates: Filter incoming events dynamically before they are routed to a hook.

Lifetimes

Hooks have a maximum invocation lifetime which may be configured individually. The default lifetime is one invocation, though an unlimited lifetime can be configured as well. If not unlimited, the lifetime is decreased on each invocation until it reaches zero and is unsubscribed automatically. Invocations rejected by a predicate do not count toward the lifetime limit.

await hub.OnProcessedAsync<OrderPlacedEvent>(
    async e => {/* ... */}, lifetime: IEventHook.UnlimitedLifetime);

await hub.OnProcessedAsync<OrderPlacedEvent>(
    async e => {/* ... */}, lifetime: 2);

Scopes

Scopes provide a disposable subscription context for hooks. Any hook registered through a scope is automatically unsubscribed when the scope is disposed. This is especially useful for workflows or request-style processing where a temporary set of hooks is needed while a use case is executing.

var hub = new EventHub<TestEvent>();

await using (var scope = hub.CreateHookScope())
{
    await scope.OnProcessedAsync<OrderPlacedEvent>(
        async e => { /* ... */ });

    await hub.PublishAsync(new OrderPlacedEvent());
}

Groups

Groups partition hooks and events by identifier. When a hook is registered with a group id, it will only receive events published with the same group id. This allows multiple independent event flows to coexist safely within the same event hub. Groups are especially useful in concurrent or workflow-driven systems where events of the same type may belong to unrelated execution contexts.

Guid gid1 = Guid.NewGuid();
Guid gid2 = Guid.NewGuid();

await hub.OnPublishedAsync<OrderApprovedEvent>(async e => { /* ... */ }, groupId: gid1);

await hub.OnPublishedAsync<OrderApprovedEvent>(async e => { /* ... */ }, groupId: gid2);

await hub.PublishAsync(new OrderApprovedEvent(), gid1);

Predicates

Predicates provide dynamic event filtering in addition to groups. A predicate is evaluated for each published event to determine whether the event should be routed to a hook. Invocations rejected by a predicate do not count toward the hook lifetime limit.

var hub = new EventHub<TestEvent>();
int id = 4;

await hub.OnPublishedAsync<OrderApprovedEvent>(
    async e => { /* ... */ }, 
    /*predicate: */ e => e.Id != id);

await hub.PublishAsync(new OrderApprovedEvent()
{
    Id = id
});

License

Licensed under the GNU General Public License v3.0 or later (GPL-3.0-or-later). See the LICENSE file for details.

About

Traditional observer pattern extended with contextual, scoped, and lifetime-bound event hooks

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages