Add Observer Pattern Source Generator#119
Conversation
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>
There was a problem hiding this comment.
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
ObserverGeneratorplus new Observer attributes/enums inPatternKit.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.
|
@copilot apply changes based on the comments in this thread |
…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>
There was a problem hiding this comment.
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.
src/PatternKit.Generators.Abstractions/Observer/ObserverAttribute.cs
Outdated
Show resolved
Hide resolved
|
@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>
|
@JerrettDavis Unfortunately I hit an unexpected error while processing your comment. I've automatically reported this to GitHub. You can ask me to try again later by mentioning me in a new comment. If you want to contact GitHub about this error, please mention the following identifier so they can better serve you: Sorry for the inconvenience! |
|
@JerrettDavis Unfortunately I hit an unexpected error while processing your comment. I've automatically reported this to GitHub. You can ask me to try again later by mentioning me in a new comment. If you want to contact GitHub about this error, please mention the following identifier so they can better serve you: Sorry for the inconvenience! |
4 similar comments
|
@JerrettDavis Unfortunately I hit an unexpected error while processing your comment. I've automatically reported this to GitHub. You can ask me to try again later by mentioning me in a new comment. If you want to contact GitHub about this error, please mention the following identifier so they can better serve you: Sorry for the inconvenience! |
|
@JerrettDavis Unfortunately I hit an unexpected error while processing your comment. I've automatically reported this to GitHub. You can ask me to try again later by mentioning me in a new comment. If you want to contact GitHub about this error, please mention the following identifier so they can better serve you: Sorry for the inconvenience! |
|
@JerrettDavis Unfortunately I hit an unexpected error while processing your comment. I've automatically reported this to GitHub. You can ask me to try again later by mentioning me in a new comment. If you want to contact GitHub about this error, please mention the following identifier so they can better serve you: Sorry for the inconvenience! |
|
@JerrettDavis Unfortunately I hit an unexpected error while processing your comment. I've automatically reported this to GitHub. You can ask me to try again later by mentioning me in a new comment. If you want to contact GitHub about this error, please mention the following identifier so they can better serve you: Sorry for the inconvenience! |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Dependency ReviewThe following issues were found:
License Issuestest/PatternKit.Generators.Tests/PatternKit.Generators.Tests.csproj
OpenSSF Scorecard
Scanned Files
|
Test Results417 tests 417 ✅ 1m 36s ⏱️ Results for commit 6768ad8. ♻️ This comment has been updated with latest results. |
🔍 PR Validation ResultsVersion: `` ✅ Validation Steps
📊 ArtifactsDry-run artifacts have been uploaded and will be available for 7 days. This comment was automatically generated by the PR validation workflow. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Code Coverage |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #119 +/- ##
==========================================
+ Coverage 82.17% 86.03% +3.85%
==========================================
Files 192 196 +4
Lines 18532 19134 +602
Branches 2663 2712 +49
==========================================
+ Hits 15229 16461 +1232
- Misses 2591 2673 +82
+ Partials 712 0 -712
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Observer Pattern Source GeneratorImplements a fully functional source generator that produces reflection-free Observer pattern implementations with configurable threading, exception handling, and ordering semantics.### Core FeaturesAttributes ( - Fixes #45---✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.
PatternKit.Generators.Abstractions/Observer/)-[Observer(typeof(TPayload))]- marks partial class/record class types for generation- Threading policies:SingleThreadedFast,Locking(default),Concurrent- Exception policies:Continue(default),Stop,Aggregate- Order policies:RegistrationOrder(default),UndefinedGenerated APIcsharp[Observer(typeof(Temperature), Threading = ObserverThreadingPolicy.Locking)]public partial class TemperatureChanged { }// Generates:IDisposable Subscribe(Action<Temperature> handler);IDisposable Subscribe(Func<Temperature, ValueTask> handler);void Publish(Temperature value);ValueTask PublishAsync(Temperature value, CancellationToken ct = default);partial void OnSubscriberError(Exception ex); // optional error hookImplementation Highlights- Reflection-free: Uses ObserverState object with direct method calls, AOT-compatible- Thread-safe: Configurable policies with proper synchronization primitives- Snapshot semantics: Publish iterates stable copy, safe during concurrent subscribe/unsubscribe- Fire-and-forget async: SyncPublish()invokes async handlers viaTask.Runwith proper error routing- Warning-free: Uses discard assignment (_ = Task.Run(...)) to avoid CS4014- Type-safe: Accepts anyITypeSymbolpayload type including arrays- Memory-safe: ConcurrentDictionary-backed undefined-order subscriptions support deterministic removal without rebuild/compaction racesSupported Types- ✅partial class- ✅partial record class- ❌partial struct/partial record struct- rejected with PKOBS003 diagnostic due to lambda capture complexityStruct Handling-AttributeTargets.Structincluded to allow attribute application- Generator provides clear PKOBS003 diagnostic instead of compiler CS0592 error- Users see meaningful message about why structs aren't supportedThreading Policies-SingleThreadedFast: No locks, documented as not thread-safe-Locking: Lock-based thread safety with snapshot under lock-Concurrent+RegistrationOrder: Lock-free withImmutableListand atomic updates-Concurrent+Undefined: Lock-free withConcurrentDictionary<int, Subscription>for deterministic removal without compaction races- Atomic initialization viaLazyInitializer.EnsureInitializedprevents race conditionsException Handling- Sync handlers: Follow configured policy (Continue/Stop/Aggregate)- Async handlers (fire-and-forget in sync Publish): -Continue: Exceptions routed toOnSubscriberErrorhook -Stop: Exceptions routed toOnSubscriberError(cannot stop sync execution deterministically) -Aggregate: Exceptions logged viaOnSubscriberError(cannot aggregate synchronously)- For deterministic exception behavior with async handlers, usePublishAsyncHub Mode-[ObserverHub]and[ObservedEvent]attributes defined but NOT implemented- Explicitly documented as "reserved for future implementation"- Hub generation deferred to v2### Documentation & Examples**/docs/generators/observer.md** (21KB+)- Complete API reference with all configuration options- Performance characteristics for each threading/exception/order policy- Common patterns (observable properties, event aggregators)- Diagnostics reference (PKOBS001-003)- Threading policy comparison table with disposal behavior notes- Dependency notes (System.Collections.Immutable for Concurrent+RegistrationOrder)- ConcurrentDictionary-based undefined-order removal behavior documented for Concurrent+Undefined**/src/PatternKit.Examples/ObserverGeneratorDemo/-TemperatureMonitor.cs- Basic subscription patterns with fault tolerance-NotificationSystem.cs- Async handlers, multi-channel notifications, cancellation-README.md- Comprehensive usage guide with examples### Test CoverageComprehensive Test Suite:- ✅ All target type validation (class, record class, struct diagnostics)- ✅ All threading policies tested (SingleThreadedFast, Locking, Concurrent with both order modes)- ✅ Subscribe/unsubscribe lifecycle- ✅ Disposal during publish (snapshot semantics)- ✅ Registration order preservation- ✅ Async handlers with fire-and-forget- ✅ Mixed sync/async handler invocation- ✅ Exception policy Continue- ✅ Deterministic test synchronization (TaskCompletionSource)- ❌ Exception policy Stop- ❌ Exception policy AggregateTest Improvements Made:**- Added threading policy tests for SingleThreadedFast and both Concurrent modes- Replaced flakyTask.Delaywith deterministicTaskCompletionSource- All tests assertEmitResult.Successwith diagnostic output### Diagnostics- PKOBS001 (Error): Type marked[Observer]must bepartial- PKOBS002 (Error): Observer attribute requires payload type argument- PKOBS003 (Warning): Unsupported observer type (struct, nested, generic) or invalid configuration### Quality Assurance- ✅ Fully reflection-free and AOT-compatible- ✅ No CS4014 warnings in generated code- ✅ Enum range validation with defaults- ✅ForceAsyncimpliesGenerateAsync=trueautomatically- ✅ All examples compile and work correctly- ✅ Nested type examples corrected to top-level- ✅ Thread-safe initialization for all concurrent modes- ✅ Properusing System.Linq;for ImmutableList operations- ✅ Uses Roslyn built-inSyntaxKind.PartialKeywordfor partial detection- ✅ ConcurrentDictionary removal avoids undefined-order compaction racesDefault configuration (Continueexception policy,Lockingthreading) is production-ready and covers typical use cases.Original prompt
----This section details on the original issue you should resolve<issue_title>Generator: Create Observer Pattern</issue_title><issue_description>## SummaryAdd 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 inPatternKit.Generatorsand emits code that is:* reflection-free* allocation-aware* explicit about threading policy* self-contained (no runtime PatternKit dependency)---## Motivation / ProblemObserver is easy to misuse:* leaking subscriptions* nondeterministic invocation order* unclear exception behavior (one subscriber breaks others?)* ad-hoc concurrency policiesWe 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 structTwo 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-basedcsharp[Observer]public partial class TemperatureChanged { }Generated (representative shape):csharppublic 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)csharp[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 AreaNamespace:PatternKit.Generators.Observer### Core*[Observer]on an event stream type*[ObserverHub]on a hub type*[ObservedEvent]on hub properties### ConfigurationObserverAttributesuggested 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 anIDisposabletoken.*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 anAggregateException(or return a result) at the end.Recommended v1 behavior:* For syncPublish:Continueswallows by default but provides an optional hook:OnSubscriberError(Exception ex)if present.* For asyncPublishAsync: same semantics.### Async*Subscribe(Func<T, ValueTask>)must be supported.*PublishAsyncinvokes 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 toUndefinedunless 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:*PKOBS001Type marked[Observer]must bepartial.*PKOBS002Hub type marked[ObserverHub]must bepartialandstatic.*PKOBS003Hub property marked[ObservedEvent]has invalid shape (must bestatic partialand return the event stream type).*PKOBS004Async publish requested but async handler shape unsupported.*PKOBS005...