Skip to content

Add Observer Pattern Source Generator#119

Draft
Copilot wants to merge 13 commits intomainfrom
copilot/create-observer-pattern-generator
Draft

Add Observer Pattern Source Generator#119
Copilot wants to merge 13 commits intomainfrom
copilot/create-observer-pattern-generator

Conversation

Copy link
Contributor

Copilot AI commented Feb 19, 2026

Observer Pattern Source Generator - Third Review Round

Comment 2828802620 Addressed ✅

Removed Unused Unsubscribe Method

  • Removed GenerateUnsubscribeMethod call and implementation
  • The generated Unsubscribe method was never called - Subscription.Dispose handles removal directly
  • This reduces generated code size and improves maintainability
  • No behavior change - disposal logic remains in Subscription.Dispose exactly as before

Test Results: 11/13 Passing (85%)

Same as before - all changes backward compatible, no regressions.

Summary

Eliminated dead code from the generator. The Subscription.Dispose method already contains all the unsubscription logic inline based on the threading policy, making the separate Unsubscribe method unnecessary. This simplifies the generated code and reduces the size of generated types.

Original prompt

This section details on the original issue you should resolve

<issue_title>Generator: Create Observer Pattern</issue_title>
<issue_description>## 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 ...

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 8 commits February 19, 2026 04:20
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
…ribute

Complete rewrite of ObserverGenerator.cs to extract the payload type from the
[Observer(typeof(TPayload))] attribute constructor argument and generate type-safe
Subscribe/Publish methods.

Key features:
- Extract TPayload from attribute constructor (typeof(TPayload))
- Generate Subscribe(Action<TPayload>) and Subscribe(Func<TPayload, ValueTask>)
- Generate Publish(TPayload) and PublishAsync(TPayload, CancellationToken)
- Implement proper snapshot semantics for thread-safe iteration
- Support three threading policies:
  * SingleThreadedFast: No locking, just a List
  * Locking: Use lock() for thread safety (default)
  * Concurrent: Use Immutable collections for lock-free operation
- Support three exception policies:
  * Continue: Invoke all handlers, call optional OnSubscriberError hook
  * Stop: Rethrow first exception
  * Aggregate: Collect all exceptions and throw AggregateException
- Support RegistrationOrder (FIFO) and Undefined order policies
- Support all target type kinds (class, struct, record class, record struct)
- Handle structs without field initializers (C# 11+ compatibility)
- Nested private Subscription class implementing IDisposable
- Idempotent, thread-safe disposal
- Clean, deterministic code generation with proper nullability annotations

The generator now follows the same pattern as StrategyGenerator for extracting
constructor arguments and generating clean, focused implementation code.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ty issues

- Add ObserverAttribute(Type payloadType) constructor to accept payload type
- Add PayloadType property to ObserverAttribute
- Update documentation example to show [Observer(typeof(Temperature))]
- Fix critical thread safety bugs in lock usage:
  * Use lock field assignment to ensure same lock object is used
  * Change 'lock (_lock ?? new object())' to proper initialization
- Add defensive null check for Attributes array access
- Ensure lock object is initialized before use in Publish and Unsubscribe

These fixes address code review feedback and ensure proper thread-safe operation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
- Created comprehensive documentation at docs/generators/observer.md
  - Overview and motivation
  - Basic usage examples with sync and async handlers
  - Configuration options (threading, exceptions, ordering)
  - Complete API reference (Subscribe, Publish, PublishAsync)
  - Best practices and performance considerations
  - Common patterns (observable properties, event aggregator)
  - Diagnostics reference (PKOBS001-003)
  - Troubleshooting guide

- Added real-world examples in src/PatternKit.Examples/ObserverGeneratorDemo/
  - TemperatureMonitor.cs: Basic Observer usage with temperature sensors
    - Demonstrates sync handlers, multiple subscribers
    - Exception handling with OnSubscriberError
    - Subscription lifecycle management
  - NotificationSystem.cs: Advanced async patterns
    - Multi-channel notifications (Email, SMS, Push)
    - Async handlers with PublishAsync
    - Exception policies (Continue vs Aggregate)
    - Cancellation token support
  - README.md: Comprehensive example documentation
    - Quick start guide
    - Configuration examples
    - Common patterns
    - Running instructions

- Updated docs/generators/toc.yml to include observer.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use positional parameters instead of named parameters with incorrect casing
in Notification record instantiation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI changed the title [WIP] Add source generator for Observer pattern implementation Add Observer Pattern Source Generator Feb 19, 2026
Copilot AI requested a review from JerrettDavis February 19, 2026 05:04
@JerrettDavis JerrettDavis requested a review from Copilot February 19, 2026 05:04
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Observer-pattern Roslyn incremental source generator to PatternKit, along with the public attribute surface area, documentation, examples, and generator-focused unit tests.

Changes:

  • Introduces ObserverGenerator plus new Observer attributes/enums in PatternKit.Generators.Abstractions.
  • Adds extensive docs and runnable examples for the generated Observer API.
  • Adds a new test suite for Observer generation and updates test lockfile dependencies.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
test/PatternKit.Generators.Tests/packages.lock.json Updates locked dependencies to support new test/project references.
test/PatternKit.Generators.Tests/ObserverGeneratorTests.cs Adds unit tests covering generation, subscribe/publish, ordering, async, and exception policies.
src/PatternKit.Generators/Observer/ObserverGenerator.cs Implements the incremental generator and emits the generated Observer implementation.
src/PatternKit.Generators/AnalyzerReleases.Unshipped.md Registers new diagnostic IDs PKOBS001–PKOBS003.
src/PatternKit.Generators.Abstractions/Observer/ObserverThreadingPolicy.cs Adds threading policy enum for the generator configuration surface.
src/PatternKit.Generators.Abstractions/Observer/ObserverOrderPolicy.cs Adds ordering policy enum for handler invocation ordering.
src/PatternKit.Generators.Abstractions/Observer/ObserverHubAttribute.cs Adds (future) hub-mode attribute surface.
src/PatternKit.Generators.Abstractions/Observer/ObserverExceptionPolicy.cs Adds exception policy enum for publish behavior.
src/PatternKit.Generators.Abstractions/Observer/ObserverAttribute.cs Adds primary [Observer] attribute and config properties.
src/PatternKit.Generators.Abstractions/Observer/ObservedEventAttribute.cs Adds (future) hub event property marker attribute.
src/PatternKit.Examples/ObserverGeneratorDemo/TemperatureMonitor.cs Adds sync usage examples (subscribe/publish, ordering, disposal, error hook).
src/PatternKit.Examples/ObserverGeneratorDemo/README.md Documents how to run and understand the example scenarios.
src/PatternKit.Examples/ObserverGeneratorDemo/NotificationSystem.cs Adds async usage examples (PublishAsync, exception policies, mixed handlers, cancellation).
docs/generators/toc.yml Adds Observer generator doc entry to generator docs navigation.
docs/generators/observer.md Adds full Observer generator documentation and diagnostics reference.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@JerrettDavis
Copy link
Owner

@copilot apply changes based on the comments in this thread

Copilot AI and others added 2 commits February 19, 2026 05:31
…ion, fire-and-forget async, disable struct support

Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
…ixed handler test, verify struct diagnostics

Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@JerrettDavis
Copy link
Owner

@copilot apply changes based on the comments in this thread

…ets.Struct, improve async error handling, remove unused using

Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 474 to 503
sb.AppendLine(" private void Unsubscribe(int id)");
sb.AppendLine(" {");

switch (config.Threading)
{
case 0: // SingleThreadedFast
sb.AppendLine(" _state.Subscriptions?.RemoveAll(s => s.Id == id);");
break;

case 1: // Locking
sb.AppendLine(" lock (_state.Lock)");
sb.AppendLine(" {");
sb.AppendLine(" _state.Subscriptions?.RemoveAll(s => s.Id == id);");
sb.AppendLine(" }");
break;

case 2: // Concurrent
if (config.Order == 0) // RegistrationOrder
{
sb.AppendLine(" System.Collections.Immutable.ImmutableInterlocked.Update(ref _state.Subscriptions, static (list, id) => list?.RemoveAll(s => s.Id == id) ?? list, id);");
}
else // Undefined - ConcurrentBag doesn't support efficient removal
{
sb.AppendLine(" // ConcurrentBag doesn't support removal; subscription marks itself disposed");
}
break;
}

sb.AppendLine(" }");
sb.AppendLine();
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Unsubscribe method is generated but never called. The Subscription.Dispose method (lines 552-587) handles subscription removal directly, making this method unnecessary. Consider removing the call to GenerateUnsubscribeMethod on line 189 and the entire GenerateUnsubscribeMethod implementation to reduce generated code size and improve maintainability.

Suggested change
sb.AppendLine(" private void Unsubscribe(int id)");
sb.AppendLine(" {");
switch (config.Threading)
{
case 0: // SingleThreadedFast
sb.AppendLine(" _state.Subscriptions?.RemoveAll(s => s.Id == id);");
break;
case 1: // Locking
sb.AppendLine(" lock (_state.Lock)");
sb.AppendLine(" {");
sb.AppendLine(" _state.Subscriptions?.RemoveAll(s => s.Id == id);");
sb.AppendLine(" }");
break;
case 2: // Concurrent
if (config.Order == 0) // RegistrationOrder
{
sb.AppendLine(" System.Collections.Immutable.ImmutableInterlocked.Update(ref _state.Subscriptions, static (list, id) => list?.RemoveAll(s => s.Id == id) ?? list, id);");
}
else // Undefined - ConcurrentBag doesn't support efficient removal
{
sb.AppendLine(" // ConcurrentBag doesn't support removal; subscription marks itself disposed");
}
break;
}
sb.AppendLine(" }");
sb.AppendLine();
// No-op: subscription removal is handled directly by Subscription.Dispose.
// This method is retained to preserve the generator API but no longer emits
// an unused Unsubscribe method into generated types.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in aafc0c0. Removed GenerateUnsubscribeMethod call and implementation. The generated Unsubscribe method was never called - Subscription.Dispose handles subscription removal directly, reducing generated code size.

@JerrettDavis
Copy link
Owner

@copilot apply changes based on the comments in this thread

…y Subscription.Dispose

Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generator: Create Observer Pattern

2 participants

Comments