Skip to content

Generator: Create Observer Pattern #45

@JerrettDavis

Description

@JerrettDavis

Summary

Add a source generator that produces a complete implementation of the Observer pattern for event publication and subscription, with safe-by-default lifetimes and deterministic behavior.

The generator lives in PatternKit.Generators and emits code that is:

  • reflection-free
  • allocation-aware
  • explicit about threading policy
  • self-contained (no runtime PatternKit dependency)

Motivation / Problem

Observer is easy to misuse:

  • leaking subscriptions
  • nondeterministic invocation order
  • unclear exception behavior (one subscriber breaks others?)
  • ad-hoc concurrency policies

We want a generated implementation that makes the “rules of engagement” explicit and testable.


Supported Targets (must-have)

The generator must support:

  • partial class
  • partial struct
  • partial record class
  • partial record struct

Two consumption modes must be supported:

  1. Event type (a type represents one observable event stream).
  2. Event hub (a type groups multiple generated events).

Proposed User Experience

A) Single event, payload-based

[Observer]
public partial class TemperatureChanged { }

Generated (representative shape):

public partial class TemperatureChanged
{
    public IDisposable Subscribe(Action<Temperature> handler);
    public IDisposable Subscribe(Func<Temperature, ValueTask> handler);

    public void Publish(Temperature value);
    public ValueTask PublishAsync(Temperature value, CancellationToken ct = default);
}

B) Event hub (multi-event grouping)

[ObserverHub]
public static partial class SystemEvents
{
    [ObservedEvent]
    public static partial TemperatureChanged TemperatureChanged { get; }

    [ObservedEvent]
    public static partial ShutdownRequested ShutdownRequested { get; }
}

Generated semantics:

  • Each [ObservedEvent] property returns a singleton instance of that event stream.
  • Hub generation is optional, but if present must be deterministic and self-contained.

Attributes / Surface Area

Namespace: PatternKit.Generators.Observer

Core

  • [Observer] on an event stream type
  • [ObserverHub] on a hub type
  • [ObservedEvent] on hub properties

Configuration

ObserverAttribute suggested properties:

  • ObserverThreadingPolicy Threading (default: Locking)
  • ObserverExceptionPolicy Exceptions (default: Continue)
  • ObserverOrderPolicy Order (default: RegistrationOrder)
  • bool GenerateAsync (default: inferred)
  • bool ForceAsync (default: false)

Enums:

  • ObserverThreadingPolicy: SingleThreadedFast, Locking, Concurrent
  • ObserverExceptionPolicy: Stop, Continue, Aggregate
  • ObserverOrderPolicy: RegistrationOrder, Undefined

Semantics (must-have)

Subscriptions

  • Subscribe(Action<T>) returns an IDisposable token.
  • Dispose() unsubscribes deterministically.
  • Duplicate subscriptions are allowed in v1 (invoked multiple times).

Publishing

  • Default order: RegistrationOrder.
  • Publishing uses snapshot semantics (publish iterates a stable snapshot so modifications during publish do not affect the current cycle).

Exception policies

  • Continue (default): invoke all handlers; exceptions do not stop others.

    • v1: either swallow exceptions or route them to an optional user hook (see below). Must be explicit.
  • Stop: first exception aborts.

  • Aggregate: run all and throw an AggregateException (or return a result) at the end.

Recommended v1 behavior:

  • For sync Publish: Continue swallows by default but provides an optional hook: OnSubscriberError(Exception ex) if present.
  • For async PublishAsync: same semantics.

Async

  • Subscribe(Func<T, ValueTask>) must be supported.
  • PublishAsync invokes async handlers in deterministic order.
  • Cancellation token behavior: best-effort. If canceled before next invocation, stop and return canceled.

Threading policies

  • SingleThreadedFast: no locks; documented as not thread-safe.
  • Locking: lock around subscribe/unsubscribe; publish takes snapshot under lock.
  • Concurrent: thread-safe with concurrent primitives; ordering may degrade to Undefined unless extra work is done. Must be documented.

Optional advanced features (explicitly v2 unless trivial)

  • Weak subscriptions
  • Backpressure / queueing
  • Filters / predicate subscriptions
  • “Once” subscriptions

Diagnostics (must-have)

Stable IDs, actionable:

  • PKOBS001 Type marked [Observer] must be partial.
  • PKOBS002 Hub type marked [ObserverHub] must be partial and static.
  • PKOBS003 Hub property marked [ObservedEvent] has invalid shape (must be static partial and return the event stream type).
  • PKOBS004 Async publish requested but async handler shape unsupported.
  • PKOBS005 Invalid configuration combination (e.g., Concurrent + RegistrationOrder requires additional ordering guarantees).

Generated Code Layout

  • TypeName.Observer.g.cs
  • TypeName.ObserverHub.g.cs (if hub feature enabled)

Determinism:

  • internal storage deterministic where promised (especially for RegistrationOrder).

Testing Expectations

  • Subscribe/unsubscribe works and is idempotent.

  • Publish invokes in registration order.

  • Dispose during publish does not break iteration (snapshot semantics).

  • Exception policy behavior matches docs:

    • Continue
    • Stop
    • Aggregate
  • Threading policy tests:

    • Locking prevents basic races in a concurrent subscribe/publish stress test.
  • Async publish respects cancellation (best-effort).


Acceptance Criteria

  • [Observer] works for class/struct/record class/record struct.
  • Supports sync handlers and async handlers (ValueTask).
  • Deterministic ordering for RegistrationOrder modes.
  • Snapshot publishing semantics.
  • Clear exception policy with tests.
  • Explicit, documented threading policy.
  • Hub mode supported (optional but if implemented, fully tested).
  • Diagnostics cover invalid usage and config.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions