From ccc20b6088e97e1be72f7a67be8ce909cce9ee43 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 19 Feb 2026 04:17:23 +0000
Subject: [PATCH 01/13] Initial plan
From ba08a179a5843afacc1968204590a26e57432f48 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 19 Feb 2026 04:20:54 +0000
Subject: [PATCH 02/13] Add Observer pattern attribute definitions and enums
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
.../Observer/ObservedEventAttribute.cs | 26 +++++++++
.../Observer/ObserverAttribute.cs | 53 +++++++++++++++++++
.../Observer/ObserverExceptionPolicy.cs | 26 +++++++++
.../Observer/ObserverHubAttribute.cs | 27 ++++++++++
.../Observer/ObserverOrderPolicy.cs | 19 +++++++
.../Observer/ObserverThreadingPolicy.cs | 27 ++++++++++
6 files changed, 178 insertions(+)
create mode 100644 src/PatternKit.Generators.Abstractions/Observer/ObservedEventAttribute.cs
create mode 100644 src/PatternKit.Generators.Abstractions/Observer/ObserverAttribute.cs
create mode 100644 src/PatternKit.Generators.Abstractions/Observer/ObserverExceptionPolicy.cs
create mode 100644 src/PatternKit.Generators.Abstractions/Observer/ObserverHubAttribute.cs
create mode 100644 src/PatternKit.Generators.Abstractions/Observer/ObserverOrderPolicy.cs
create mode 100644 src/PatternKit.Generators.Abstractions/Observer/ObserverThreadingPolicy.cs
diff --git a/src/PatternKit.Generators.Abstractions/Observer/ObservedEventAttribute.cs b/src/PatternKit.Generators.Abstractions/Observer/ObservedEventAttribute.cs
new file mode 100644
index 0000000..d685a58
--- /dev/null
+++ b/src/PatternKit.Generators.Abstractions/Observer/ObservedEventAttribute.cs
@@ -0,0 +1,26 @@
+namespace PatternKit.Generators.Observer;
+
+///
+/// Marks a property in an -decorated class as an observable event stream.
+/// The property must be static, partial, and have a getter only.
+///
+///
+///
+/// The generator will create a singleton instance of the event type for this property.
+///
+///
+/// Example:
+///
+/// [ObserverHub]
+/// public static partial class SystemEvents
+/// {
+/// [ObservedEvent]
+/// public static partial TemperatureChanged TemperatureChanged { get; }
+/// }
+///
+///
+///
+[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
+public sealed class ObservedEventAttribute : Attribute
+{
+}
diff --git a/src/PatternKit.Generators.Abstractions/Observer/ObserverAttribute.cs b/src/PatternKit.Generators.Abstractions/Observer/ObserverAttribute.cs
new file mode 100644
index 0000000..8057e11
--- /dev/null
+++ b/src/PatternKit.Generators.Abstractions/Observer/ObserverAttribute.cs
@@ -0,0 +1,53 @@
+namespace PatternKit.Generators.Observer;
+
+///
+/// Marks a type for Observer pattern code generation.
+/// The type must be declared as partial (class, struct, record class, or record struct).
+///
+///
+///
+/// The generator will produce Subscribe and Publish methods with configurable
+/// threading, exception handling, and ordering semantics.
+///
+///
+/// Example:
+///
+/// [Observer]
+/// public partial class TemperatureChanged { }
+///
+///
+///
+[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
+public sealed class ObserverAttribute : Attribute
+{
+ ///
+ /// Gets or sets the threading policy for Subscribe/Unsubscribe/Publish operations.
+ /// Default is .
+ ///
+ public ObserverThreadingPolicy Threading { get; set; } = ObserverThreadingPolicy.Locking;
+
+ ///
+ /// Gets or sets the exception handling policy during publishing.
+ /// Default is .
+ ///
+ public ObserverExceptionPolicy Exceptions { get; set; } = ObserverExceptionPolicy.Continue;
+
+ ///
+ /// Gets or sets the invocation order policy for event handlers.
+ /// Default is .
+ ///
+ public ObserverOrderPolicy Order { get; set; } = ObserverOrderPolicy.RegistrationOrder;
+
+ ///
+ /// Gets or sets whether to generate async publish methods.
+ /// When not explicitly set, async methods are generated if any async handlers are detected.
+ ///
+ public bool GenerateAsync { get; set; } = true;
+
+ ///
+ /// Gets or sets whether to force all handlers to be async.
+ /// When true, only async Subscribe methods are generated.
+ /// Default is false (both sync and async handlers are supported).
+ ///
+ public bool ForceAsync { get; set; } = false;
+}
diff --git a/src/PatternKit.Generators.Abstractions/Observer/ObserverExceptionPolicy.cs b/src/PatternKit.Generators.Abstractions/Observer/ObserverExceptionPolicy.cs
new file mode 100644
index 0000000..964b243
--- /dev/null
+++ b/src/PatternKit.Generators.Abstractions/Observer/ObserverExceptionPolicy.cs
@@ -0,0 +1,26 @@
+namespace PatternKit.Generators.Observer;
+
+///
+/// Defines how exceptions from event handlers are handled during publishing.
+///
+public enum ObserverExceptionPolicy
+{
+ ///
+ /// Continue invoking all handlers even if some throw exceptions.
+ /// Exceptions are either swallowed or routed to an optional error hook.
+ /// Default and safest for most scenarios.
+ ///
+ Continue = 0,
+
+ ///
+ /// Stop publishing and rethrow the first exception encountered.
+ /// Remaining handlers are not invoked.
+ ///
+ Stop = 1,
+
+ ///
+ /// Invoke all handlers and collect any exceptions.
+ /// Throws an AggregateException at the end if any handlers threw.
+ ///
+ Aggregate = 2
+}
diff --git a/src/PatternKit.Generators.Abstractions/Observer/ObserverHubAttribute.cs b/src/PatternKit.Generators.Abstractions/Observer/ObserverHubAttribute.cs
new file mode 100644
index 0000000..93796be
--- /dev/null
+++ b/src/PatternKit.Generators.Abstractions/Observer/ObserverHubAttribute.cs
@@ -0,0 +1,27 @@
+namespace PatternKit.Generators.Observer;
+
+///
+/// Marks a type as an observer event hub that groups multiple event streams.
+/// The type must be declared as partial and static.
+///
+///
+///
+/// Use this attribute on a static class that will contain multiple
+/// properties, each representing a separate event stream.
+///
+///
+/// Example:
+///
+/// [ObserverHub]
+/// public static partial class SystemEvents
+/// {
+/// [ObservedEvent]
+/// public static partial TemperatureChanged TemperatureChanged { get; }
+/// }
+///
+///
+///
+[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
+public sealed class ObserverHubAttribute : Attribute
+{
+}
diff --git a/src/PatternKit.Generators.Abstractions/Observer/ObserverOrderPolicy.cs b/src/PatternKit.Generators.Abstractions/Observer/ObserverOrderPolicy.cs
new file mode 100644
index 0000000..250767c
--- /dev/null
+++ b/src/PatternKit.Generators.Abstractions/Observer/ObserverOrderPolicy.cs
@@ -0,0 +1,19 @@
+namespace PatternKit.Generators.Observer;
+
+///
+/// Defines the invocation order guarantee for event handlers.
+///
+public enum ObserverOrderPolicy
+{
+ ///
+ /// Handlers are invoked in the order they were registered (FIFO).
+ /// Default and recommended for deterministic behavior.
+ ///
+ RegistrationOrder = 0,
+
+ ///
+ /// No order guarantee. Handlers may be invoked in any order.
+ /// May provide better performance with certain threading policies.
+ ///
+ Undefined = 1
+}
diff --git a/src/PatternKit.Generators.Abstractions/Observer/ObserverThreadingPolicy.cs b/src/PatternKit.Generators.Abstractions/Observer/ObserverThreadingPolicy.cs
new file mode 100644
index 0000000..419d198
--- /dev/null
+++ b/src/PatternKit.Generators.Abstractions/Observer/ObserverThreadingPolicy.cs
@@ -0,0 +1,27 @@
+namespace PatternKit.Generators.Observer;
+
+///
+/// Defines the threading policy for an observer pattern implementation.
+///
+public enum ObserverThreadingPolicy
+{
+ ///
+ /// No thread safety. Fast, but not safe for concurrent Subscribe/Unsubscribe/Publish.
+ /// Use only when all operations occur on a single thread.
+ ///
+ SingleThreadedFast = 0,
+
+ ///
+ /// Uses locking for thread safety. Subscribe/Unsubscribe operations take locks,
+ /// and Publish snapshots the subscriber list under a lock for deterministic iteration.
+ /// Default and recommended for most scenarios.
+ ///
+ Locking = 1,
+
+ ///
+ /// Lock-free concurrent implementation using atomic operations.
+ /// Thread-safe with potentially better performance under high concurrency,
+ /// but ordering may degrade to Undefined unless additional work is done.
+ ///
+ Concurrent = 2
+}
From e65fd7e01a5de1710f9452e5c4efe83211e90afe Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 19 Feb 2026 04:23:13 +0000
Subject: [PATCH 03/13] Add initial Observer generator structure and diagnostic
tracking
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
.../AnalyzerReleases.Unshipped.md | 5 +
.../Observer/ObserverGenerator.cs | 789 ++++++++++++++++++
2 files changed, 794 insertions(+)
create mode 100644 src/PatternKit.Generators/Observer/ObserverGenerator.cs
diff --git a/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md b/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
index 65ad3e8..8eec4e0 100644
--- a/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
+++ b/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
@@ -126,3 +126,8 @@ PKST007 | PatternKit.Generators.State | Error | Entry/Exit hook signature invali
PKST008 | PatternKit.Generators.State | Warning | Async method detected but async generation disabled
PKST009 | PatternKit.Generators.State | Error | Generic types not supported for State pattern
PKST010 | PatternKit.Generators.State | Error | Nested types not supported for State pattern
+PKOBS001 | PatternKit.Generators.Observer | Error | Type marked with [Observer] must be partial
+PKOBS002 | PatternKit.Generators.Observer | Error | Type marked with [ObserverHub] must be partial and static
+PKOBS003 | PatternKit.Generators.Observer | Error | Property marked with [ObservedEvent] has invalid shape
+PKOBS004 | PatternKit.Generators.Observer | Warning | Async publish requested but async handler shape unsupported
+PKOBS005 | PatternKit.Generators.Observer | Warning | Invalid configuration combination
diff --git a/src/PatternKit.Generators/Observer/ObserverGenerator.cs b/src/PatternKit.Generators/Observer/ObserverGenerator.cs
new file mode 100644
index 0000000..384f91c
--- /dev/null
+++ b/src/PatternKit.Generators/Observer/ObserverGenerator.cs
@@ -0,0 +1,789 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace PatternKit.Generators.Observer;
+
+///
+/// Incremental source generator for the Observer pattern.
+/// Generates Subscribe/Publish methods with configurable threading, exception, and ordering policies.
+///
+[Generator]
+public sealed class ObserverGenerator : IIncrementalGenerator
+{
+ // Diagnostic IDs
+ private const string DiagnosticIdNotPartial = "PKOBS001";
+ private const string DiagnosticIdHubNotPartialStatic = "PKOBS002";
+ private const string DiagnosticIdInvalidEventProperty = "PKOBS003";
+ private const string DiagnosticIdAsyncUnsupported = "PKOBS004";
+ private const string DiagnosticIdInvalidConfig = "PKOBS005";
+
+ // Diagnostic descriptors
+ private static readonly DiagnosticDescriptor NotPartialRule = new(
+ DiagnosticIdNotPartial,
+ "Type must be partial",
+ "Type '{0}' marked with [Observer] must be declared as partial",
+ "PatternKit.Generators.Observer",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor HubNotPartialStaticRule = new(
+ DiagnosticIdHubNotPartialStatic,
+ "Hub type must be partial and static",
+ "Type '{0}' marked with [ObserverHub] must be declared as partial static",
+ "PatternKit.Generators.Observer",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor InvalidEventPropertyRule = new(
+ DiagnosticIdInvalidEventProperty,
+ "Invalid event property",
+ "Property '{0}' marked with [ObservedEvent] must be static partial with a getter only",
+ "PatternKit.Generators.Observer",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor AsyncUnsupportedRule = new(
+ DiagnosticIdAsyncUnsupported,
+ "Async not supported",
+ "Async publish requested but async handler shape is unsupported in this context",
+ "PatternKit.Generators.Observer",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor InvalidConfigRule = new(
+ DiagnosticIdInvalidConfig,
+ "Invalid configuration",
+ "{0}",
+ "PatternKit.Generators.Observer",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ // Find all types marked with [Observer]
+ var observerTypes = context.SyntaxProvider.ForAttributeWithMetadataName(
+ fullyQualifiedMetadataName: "PatternKit.Generators.Observer.ObserverAttribute",
+ predicate: static (node, _) => node is TypeDeclarationSyntax,
+ transform: static (ctx, _) => ctx
+ );
+
+ // Generate observer implementations
+ context.RegisterSourceOutput(observerTypes, static (spc, occ) =>
+ {
+ GenerateObserver(spc, occ);
+ });
+
+ // Find all types marked with [ObserverHub]
+ var hubTypes = context.SyntaxProvider.ForAttributeWithMetadataName(
+ fullyQualifiedMetadataName: "PatternKit.Generators.Observer.ObserverHubAttribute",
+ predicate: static (node, _) => node is ClassDeclarationSyntax,
+ transform: static (ctx, _) => ctx
+ );
+
+ // Generate hub implementations
+ context.RegisterSourceOutput(hubTypes, static (spc, occ) =>
+ {
+ GenerateHub(spc, occ);
+ });
+ }
+
+ private static void GenerateObserver(SourceProductionContext context, GeneratorAttributeSyntaxContext occurrence)
+ {
+ var typeSymbol = (INamedTypeSymbol)occurrence.TargetSymbol;
+ var syntax = (TypeDeclarationSyntax)occurrence.TargetNode;
+
+ // Validate that type is partial
+ if (!IsPartial(syntax))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ NotPartialRule,
+ syntax.Identifier.GetLocation(),
+ typeSymbol.Name));
+ return;
+ }
+
+ // Extract configuration from attribute
+ var config = ExtractObserverConfig(occurrence.Attributes[0]);
+
+ // Validate configuration
+ if (!ValidateConfiguration(config, context, syntax.GetLocation()))
+ {
+ return;
+ }
+
+ // Generate the source code
+ var source = GenerateObserverSource(typeSymbol, config);
+ var fileName = $"{typeSymbol.Name}.Observer.g.cs";
+ context.AddSource(fileName, source);
+ }
+
+ private static void GenerateHub(SourceProductionContext context, GeneratorAttributeSyntaxContext occurrence)
+ {
+ var typeSymbol = (INamedTypeSymbol)occurrence.TargetSymbol;
+ var syntax = (ClassDeclarationSyntax)occurrence.TargetNode;
+
+ // Validate that type is partial and static
+ if (!IsPartial(syntax) || !IsStatic(syntax))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ HubNotPartialStaticRule,
+ syntax.Identifier.GetLocation(),
+ typeSymbol.Name));
+ return;
+ }
+
+ // Find all properties marked with [ObservedEvent]
+ var eventProperties = typeSymbol.GetMembers()
+ .OfType()
+ .Where(p => p.GetAttributes()
+ .Any(a => a.AttributeClass?.ToDisplayString() == "PatternKit.Generators.Observer.ObservedEventAttribute"))
+ .ToList();
+
+ // Validate event properties
+ foreach (var prop in eventProperties)
+ {
+ if (!prop.IsStatic || prop.SetMethod != null)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ InvalidEventPropertyRule,
+ prop.Locations.FirstOrDefault() ?? Location.None,
+ prop.Name));
+ return;
+ }
+ }
+
+ // Generate the hub source code
+ var source = GenerateHubSource(typeSymbol, eventProperties);
+ var fileName = $"{typeSymbol.Name}.ObserverHub.g.cs";
+ context.AddSource(fileName, source);
+ }
+
+ private static bool IsPartial(TypeDeclarationSyntax syntax)
+ {
+ return syntax.Modifiers.Any(m => m.Text == "partial");
+ }
+
+ private static bool IsStatic(TypeDeclarationSyntax syntax)
+ {
+ return syntax.Modifiers.Any(m => m.Text == "static");
+ }
+
+ private static ObserverConfig ExtractObserverConfig(AttributeData attribute)
+ {
+ var config = new ObserverConfig();
+
+ foreach (var namedArg in attribute.NamedArguments)
+ {
+ switch (namedArg.Key)
+ {
+ case "Threading":
+ config.Threading = (int)namedArg.Value.Value!;
+ break;
+ case "Exceptions":
+ config.Exceptions = (int)namedArg.Value.Value!;
+ break;
+ case "Order":
+ config.Order = (int)namedArg.Value.Value!;
+ break;
+ case "GenerateAsync":
+ config.GenerateAsync = (bool)namedArg.Value.Value!;
+ break;
+ case "ForceAsync":
+ config.ForceAsync = (bool)namedArg.Value.Value!;
+ break;
+ }
+ }
+
+ return config;
+ }
+
+ private static bool ValidateConfiguration(ObserverConfig config, SourceProductionContext context, Location location)
+ {
+ // Validate: Concurrent + RegistrationOrder requires extra work
+ if (config.Threading == 2 && config.Order == 0) // Concurrent + RegistrationOrder
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ InvalidConfigRule,
+ location,
+ "Concurrent threading with RegistrationOrder requires additional ordering guarantees and may impact performance"));
+ // This is a warning, not an error, so we continue
+ }
+
+ return true;
+ }
+
+ private static string GenerateObserverSource(INamedTypeSymbol typeSymbol, ObserverConfig config)
+ {
+ var ns = typeSymbol.ContainingNamespace.IsGlobalNamespace
+ ? null
+ : typeSymbol.ContainingNamespace.ToDisplayString();
+
+ var typeKind = GetTypeKind(typeSymbol);
+ var typeName = typeSymbol.Name;
+
+ var code = new CodeBuilder();
+
+ code.AppendLine("#nullable enable");
+ code.AppendLine("// ");
+ code.AppendLine();
+
+ if (ns != null)
+ {
+ code.AppendLine($"namespace {ns};");
+ code.AppendLine();
+ }
+
+ // Start type declaration
+ code.Append($"{GetAccessibility(typeSymbol)} partial {typeKind} {typeName}");
+ code.AppendLine();
+ code.AppendLine("{");
+
+ // Generate the implementation based on configuration
+ GenerateObserverImplementation(code, config);
+
+ code.AppendLine("}");
+
+ return code.ToString();
+ }
+
+ private static void GenerateObserverImplementation(CodeBuilder code, ObserverConfig config)
+ {
+ // Generate subscription storage based on threading policy
+ GenerateStorage(code, config);
+ code.AppendLine();
+
+ // Generate Subscribe methods
+ GenerateSubscribeMethods(code, config);
+ code.AppendLine();
+
+ // Generate Publish methods
+ GeneratePublishMethods(code, config);
+ code.AppendLine();
+
+ // Generate Subscription class (IDisposable)
+ GenerateSubscriptionClass(code, config);
+ }
+
+ private static void GenerateStorage(CodeBuilder code, ObserverConfig config)
+ {
+ code.Indent();
+
+ switch (config.Threading)
+ {
+ case 0: // SingleThreadedFast
+ code.AppendLine("private System.Collections.Generic.List _subscriptions = new();");
+ code.AppendLine("private int _nextId;");
+ break;
+
+ case 1: // Locking
+ code.AppendLine("private readonly object _lock = new();");
+ code.AppendLine("private System.Collections.Generic.List _subscriptions = new();");
+ code.AppendLine("private int _nextId;");
+ break;
+
+ case 2: // Concurrent
+ if (config.Order == 0) // RegistrationOrder
+ {
+ // Use ImmutableList for order preservation with concurrent access
+ code.AppendLine("private System.Collections.Immutable.ImmutableList _subscriptions = System.Collections.Immutable.ImmutableList.Empty;");
+ code.AppendLine("private int _nextId;");
+ }
+ else
+ {
+ // Can use ConcurrentBag for better performance
+ code.AppendLine("private System.Collections.Concurrent.ConcurrentBag _subscriptions = new();");
+ code.AppendLine("private int _nextId;");
+ }
+ break;
+ }
+
+ code.Unindent();
+ }
+
+ private static void GenerateSubscribeMethods(CodeBuilder code, ObserverConfig config)
+ {
+ code.Indent();
+
+ // Determine generic parameter (we'll use object for now, but ideally would infer from context)
+ var eventType = "TEvent";
+
+ if (!config.ForceAsync)
+ {
+ // Generate sync Subscribe method
+ code.AppendLine($"public System.IDisposable Subscribe<{eventType}>(System.Action<{eventType}> handler)");
+ code.AppendLine("{");
+ code.Indent();
+ GenerateSubscribeBody(code, config, false);
+ code.Unindent();
+ code.AppendLine("}");
+ code.AppendLine();
+ }
+
+ if (config.GenerateAsync)
+ {
+ // Generate async Subscribe method
+ code.AppendLine($"public System.IDisposable Subscribe<{eventType}>(System.Func<{eventType}, System.Threading.Tasks.ValueTask> handler)");
+ code.AppendLine("{");
+ code.Indent();
+ GenerateSubscribeBody(code, config, true);
+ code.Unindent();
+ code.AppendLine("}");
+ }
+
+ code.Unindent();
+ }
+
+ private static void GenerateSubscribeBody(CodeBuilder code, ObserverConfig config, bool isAsync)
+ {
+ code.AppendLine("var id = System.Threading.Interlocked.Increment(ref _nextId);");
+ code.AppendLine($"var sub = new Subscription(this, id, handler, {(isAsync ? "true" : "false")});");
+ code.AppendLine();
+
+ switch (config.Threading)
+ {
+ case 0: // SingleThreadedFast
+ code.AppendLine("_subscriptions.Add(sub);");
+ break;
+
+ case 1: // Locking
+ code.AppendLine("lock (_lock)");
+ code.AppendLine("{");
+ code.Indent();
+ code.AppendLine("_subscriptions.Add(sub);");
+ code.Unindent();
+ code.AppendLine("}");
+ break;
+
+ case 2: // Concurrent
+ if (config.Order == 0) // RegistrationOrder
+ {
+ code.AppendLine("System.Collections.Immutable.ImmutableInterlocked.Update(ref _subscriptions, static (list, s) => list.Add(s), sub);");
+ }
+ else
+ {
+ code.AppendLine("_subscriptions.Add(sub);");
+ }
+ break;
+ }
+
+ code.AppendLine();
+ code.AppendLine("return sub;");
+ }
+
+ private static void GeneratePublishMethods(CodeBuilder code, ObserverConfig config)
+ {
+ code.Indent();
+
+ var eventType = "TEvent";
+
+ if (!config.ForceAsync)
+ {
+ // Generate sync Publish method
+ code.AppendLine($"public void Publish<{eventType}>({eventType} value)");
+ code.AppendLine("{");
+ code.Indent();
+ GenerateSyncPublishBody(code, config);
+ code.Unindent();
+ code.AppendLine("}");
+ code.AppendLine();
+ }
+
+ if (config.GenerateAsync)
+ {
+ // Generate async Publish method
+ code.AppendLine($"public async System.Threading.Tasks.ValueTask PublishAsync<{eventType}>({eventType} value, System.Threading.CancellationToken cancellationToken = default)");
+ code.AppendLine("{");
+ code.Indent();
+ GenerateAsyncPublishBody(code, config);
+ code.Unindent();
+ code.AppendLine("}");
+ }
+
+ code.Unindent();
+ }
+
+ private static void GenerateSyncPublishBody(CodeBuilder code, ObserverConfig config)
+ {
+ // Take snapshot based on threading policy
+ GenerateSnapshot(code, config);
+ code.AppendLine();
+
+ // Generate exception handling setup
+ if (config.Exceptions == 2) // Aggregate
+ {
+ code.AppendLine("System.Collections.Generic.List? errors = null;");
+ code.AppendLine();
+ }
+
+ // Iterate through snapshot
+ code.AppendLine("foreach (var sub in snapshot)");
+ code.AppendLine("{");
+ code.Indent();
+
+ code.AppendLine("if (sub.IsAsync) continue; // Skip async handlers in sync publish");
+ code.AppendLine();
+
+ // Try-catch based on exception policy
+ if (config.Exceptions == 1) // Stop
+ {
+ code.AppendLine("sub.InvokeSync(value);");
+ }
+ else
+ {
+ code.AppendLine("try");
+ code.AppendLine("{");
+ code.Indent();
+ code.AppendLine("sub.InvokeSync(value);");
+ code.Unindent();
+ code.AppendLine("}");
+ code.AppendLine("catch (System.Exception ex)");
+ code.AppendLine("{");
+ code.Indent();
+
+ if (config.Exceptions == 0) // Continue
+ {
+ code.AppendLine("// Optionally call OnSubscriberError if defined");
+ code.AppendLine("OnSubscriberError(ex);");
+ }
+ else if (config.Exceptions == 2) // Aggregate
+ {
+ code.AppendLine("(errors ??= new()).Add(ex);");
+ }
+
+ code.Unindent();
+ code.AppendLine("}");
+ }
+
+ code.Unindent();
+ code.AppendLine("}");
+
+ // Throw aggregate if needed
+ if (config.Exceptions == 2)
+ {
+ code.AppendLine();
+ code.AppendLine("if (errors is { Count: > 0 })");
+ code.AppendLine("{");
+ code.Indent();
+ code.AppendLine("throw new System.AggregateException(errors);");
+ code.Unindent();
+ code.AppendLine("}");
+ }
+ }
+
+ private static void GenerateAsyncPublishBody(CodeBuilder code, ObserverConfig config)
+ {
+ // Take snapshot
+ GenerateSnapshot(code, config);
+ code.AppendLine();
+
+ // Generate exception handling setup
+ if (config.Exceptions == 2) // Aggregate
+ {
+ code.AppendLine("System.Collections.Generic.List? errors = null;");
+ code.AppendLine();
+ }
+
+ // Iterate through snapshot
+ code.AppendLine("foreach (var sub in snapshot)");
+ code.AppendLine("{");
+ code.Indent();
+
+ // Check cancellation
+ code.AppendLine("if (cancellationToken.IsCancellationRequested) break;");
+ code.AppendLine();
+
+ // Try-catch based on exception policy
+ if (config.Exceptions == 1) // Stop
+ {
+ code.AppendLine("await sub.InvokeAsync(value, cancellationToken).ConfigureAwait(false);");
+ }
+ else
+ {
+ code.AppendLine("try");
+ code.AppendLine("{");
+ code.Indent();
+ code.AppendLine("await sub.InvokeAsync(value, cancellationToken).ConfigureAwait(false);");
+ code.Unindent();
+ code.AppendLine("}");
+ code.AppendLine("catch (System.Exception ex)");
+ code.AppendLine("{");
+ code.Indent();
+
+ if (config.Exceptions == 0) // Continue
+ {
+ code.AppendLine("OnSubscriberError(ex);");
+ }
+ else if (config.Exceptions == 2) // Aggregate
+ {
+ code.AppendLine("(errors ??= new()).Add(ex);");
+ }
+
+ code.Unindent();
+ code.AppendLine("}");
+ }
+
+ code.Unindent();
+ code.AppendLine("}");
+
+ // Throw aggregate if needed
+ if (config.Exceptions == 2)
+ {
+ code.AppendLine();
+ code.AppendLine("if (errors is { Count: > 0 })");
+ code.AppendLine("{");
+ code.Indent();
+ code.AppendLine("throw new System.AggregateException(errors);");
+ code.Unindent();
+ code.AppendLine("}");
+ }
+ }
+
+ private static void GenerateSnapshot(CodeBuilder code, ObserverConfig config)
+ {
+ switch (config.Threading)
+ {
+ case 0: // SingleThreadedFast
+ code.AppendLine("var snapshot = _subscriptions.ToArray();");
+ break;
+
+ case 1: // Locking
+ code.AppendLine("Subscription[] snapshot;");
+ code.AppendLine("lock (_lock)");
+ code.AppendLine("{");
+ code.Indent();
+ code.AppendLine("snapshot = _subscriptions.ToArray();");
+ code.Unindent();
+ code.AppendLine("}");
+ break;
+
+ case 2: // Concurrent
+ if (config.Order == 0) // RegistrationOrder
+ {
+ code.AppendLine("var snapshot = System.Threading.Volatile.Read(ref _subscriptions).ToArray();");
+ }
+ else
+ {
+ code.AppendLine("var snapshot = _subscriptions.ToArray();");
+ }
+ break;
+ }
+ }
+
+ private static void GenerateSubscriptionClass(CodeBuilder code, ObserverConfig config)
+ {
+ code.Indent();
+
+ code.AppendLine("partial void OnSubscriberError(System.Exception ex) { }");
+ code.AppendLine();
+
+ code.AppendLine("private void Unsubscribe(int id)");
+ code.AppendLine("{");
+ code.Indent();
+
+ switch (config.Threading)
+ {
+ case 0: // SingleThreadedFast
+ code.AppendLine("_subscriptions.RemoveAll(s => s.Id == id);");
+ break;
+
+ case 1: // Locking
+ code.AppendLine("lock (_lock)");
+ code.AppendLine("{");
+ code.Indent();
+ code.AppendLine("_subscriptions.RemoveAll(s => s.Id == id);");
+ code.Unindent();
+ code.AppendLine("}");
+ break;
+
+ case 2: // Concurrent
+ if (config.Order == 0) // RegistrationOrder
+ {
+ code.AppendLine("System.Collections.Immutable.ImmutableInterlocked.Update(ref _subscriptions, static (list, id) => list.RemoveAll(s => s.Id == id), id);");
+ }
+ else
+ {
+ code.AppendLine("// Note: ConcurrentBag doesn't support removal efficiently");
+ code.AppendLine("// Mark as removed instead");
+ }
+ break;
+ }
+
+ code.Unindent();
+ code.AppendLine("}");
+ code.AppendLine();
+
+ // Generate Subscription nested class
+ code.AppendLine("private sealed class Subscription : System.IDisposable");
+ code.AppendLine("{");
+ code.Indent();
+
+ code.AppendLine("private readonly object _parent;");
+ code.AppendLine("private readonly int _id;");
+ code.AppendLine("private readonly object _handler;");
+ code.AppendLine("private readonly bool _isAsync;");
+ code.AppendLine("private int _disposed;");
+ code.AppendLine();
+
+ code.AppendLine("public int Id => _id;");
+ code.AppendLine("public bool IsAsync => _isAsync;");
+ code.AppendLine();
+
+ code.AppendLine("public Subscription(object parent, int id, object handler, bool isAsync)");
+ code.AppendLine("{");
+ code.Indent();
+ code.AppendLine("_parent = parent;");
+ code.AppendLine("_id = id;");
+ code.AppendLine("_handler = handler;");
+ code.AppendLine("_isAsync = isAsync;");
+ code.Unindent();
+ code.AppendLine("}");
+ code.AppendLine();
+
+ code.AppendLine("public void InvokeSync(TEvent value)");
+ code.AppendLine("{");
+ code.Indent();
+ code.AppendLine("if (System.Threading.Volatile.Read(ref _disposed) != 0) return;");
+ code.AppendLine("((System.Action)_handler)(value);");
+ code.Unindent();
+ code.AppendLine("}");
+ code.AppendLine();
+
+ code.AppendLine("public System.Threading.Tasks.ValueTask InvokeAsync(TEvent value, System.Threading.CancellationToken ct)");
+ code.AppendLine("{");
+ code.Indent();
+ code.AppendLine("if (System.Threading.Volatile.Read(ref _disposed) != 0) return default;");
+ code.AppendLine("if (_isAsync)");
+ code.AppendLine("{");
+ code.Indent();
+ code.AppendLine("return ((System.Func)_handler)(value);");
+ code.Unindent();
+ code.AppendLine("}");
+ code.AppendLine("else");
+ code.AppendLine("{");
+ code.Indent();
+ code.AppendLine("((System.Action)_handler)(value);");
+ code.AppendLine("return default;");
+ code.Unindent();
+ code.AppendLine("}");
+ code.Unindent();
+ code.AppendLine("}");
+ code.AppendLine();
+
+ code.AppendLine("public void Dispose()");
+ code.AppendLine("{");
+ code.Indent();
+ code.AppendLine("if (System.Threading.Interlocked.Exchange(ref _disposed, 1) != 0) return;");
+ code.AppendLine("((dynamic)_parent).Unsubscribe(_id);");
+ code.Unindent();
+ code.AppendLine("}");
+
+ code.Unindent();
+ code.AppendLine("}");
+
+ code.Unindent();
+ }
+
+ private static string GenerateHubSource(INamedTypeSymbol typeSymbol, List eventProperties)
+ {
+ var ns = typeSymbol.ContainingNamespace.IsGlobalNamespace
+ ? null
+ : typeSymbol.ContainingNamespace.ToDisplayString();
+
+ var typeName = typeSymbol.Name;
+
+ var code = new CodeBuilder();
+
+ code.AppendLine("#nullable enable");
+ code.AppendLine("// ");
+ code.AppendLine();
+
+ if (ns != null)
+ {
+ code.AppendLine($"namespace {ns};");
+ code.AppendLine();
+ }
+
+ code.AppendLine($"{GetAccessibility(typeSymbol)} static partial class {typeName}");
+ code.AppendLine("{");
+ code.Indent();
+
+ // Generate each event property
+ foreach (var prop in eventProperties)
+ {
+ var propType = prop.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+ var propName = prop.Name;
+
+ code.AppendLine($"private static readonly {propType} _{char.ToLower(propName[0])}{propName.Substring(1)} = new();");
+ code.AppendLine();
+ code.AppendLine($"public static partial {propType} {propName}");
+ code.AppendLine("{");
+ code.Indent();
+ code.AppendLine($"get => _{char.ToLower(propName[0])}{propName.Substring(1)};");
+ code.Unindent();
+ code.AppendLine("}");
+ code.AppendLine();
+ }
+
+ code.Unindent();
+ code.AppendLine("}");
+
+ return code.ToString();
+ }
+
+ private static string GetTypeKind(INamedTypeSymbol symbol)
+ {
+ return symbol.TypeKind switch
+ {
+ TypeKind.Class => symbol.IsRecord ? "record class" : "class",
+ TypeKind.Struct => symbol.IsRecord ? "record struct" : "struct",
+ _ => "class"
+ };
+ }
+
+ private static string GetAccessibility(ISymbol symbol)
+ {
+ return symbol.DeclaredAccessibility switch
+ {
+ Accessibility.Public => "public",
+ Accessibility.Internal => "internal",
+ Accessibility.Private => "private",
+ Accessibility.Protected => "protected",
+ Accessibility.ProtectedOrInternal => "protected internal",
+ _ => "internal"
+ };
+ }
+
+ private class ObserverConfig
+ {
+ public int Threading { get; set; } = 1; // Locking
+ public int Exceptions { get; set; } = 0; // Continue
+ public int Order { get; set; } = 0; // RegistrationOrder
+ public bool GenerateAsync { get; set; } = true;
+ public bool ForceAsync { get; set; } = false;
+ }
+
+ private class CodeBuilder
+ {
+ private readonly System.Text.StringBuilder _sb = new();
+ private int _indentLevel = 0;
+ private const string IndentString = " ";
+
+ public void Indent() => _indentLevel++;
+ public void Unindent() => _indentLevel = Math.Max(0, _indentLevel - 1);
+
+ public void Append(string text) => _sb.Append(text);
+
+ public void AppendLine(string text = "")
+ {
+ if (!string.IsNullOrEmpty(text))
+ {
+ for (int i = 0; i < _indentLevel; i++)
+ _sb.Append(IndentString);
+ _sb.Append(text);
+ }
+ _sb.AppendLine();
+ }
+
+ public override string ToString() => _sb.ToString();
+ }
+}
From 6431f6ab81a5e2e4e88157a84aa5294377f399ab Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 19 Feb 2026 04:34:58 +0000
Subject: [PATCH 04/13] Rewrite ObserverGenerator to support payload type
extraction from attribute
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) and Subscribe(Func)
- 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>
---
.../AnalyzerReleases.Unshipped.md | 6 +-
.../Observer/ObserverGenerator.cs | 802 ++++++------------
2 files changed, 257 insertions(+), 551 deletions(-)
diff --git a/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md b/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
index 8eec4e0..5ab1640 100644
--- a/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
+++ b/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
@@ -127,7 +127,5 @@ PKST008 | PatternKit.Generators.State | Warning | Async method detected but asyn
PKST009 | PatternKit.Generators.State | Error | Generic types not supported for State pattern
PKST010 | PatternKit.Generators.State | Error | Nested types not supported for State pattern
PKOBS001 | PatternKit.Generators.Observer | Error | Type marked with [Observer] must be partial
-PKOBS002 | PatternKit.Generators.Observer | Error | Type marked with [ObserverHub] must be partial and static
-PKOBS003 | PatternKit.Generators.Observer | Error | Property marked with [ObservedEvent] has invalid shape
-PKOBS004 | PatternKit.Generators.Observer | Warning | Async publish requested but async handler shape unsupported
-PKOBS005 | PatternKit.Generators.Observer | Warning | Invalid configuration combination
+PKOBS002 | PatternKit.Generators.Observer | Error | Unable to extract payload type from [Observer] attribute
+PKOBS003 | PatternKit.Generators.Observer | Warning | Invalid configuration combination
diff --git a/src/PatternKit.Generators/Observer/ObserverGenerator.cs b/src/PatternKit.Generators/Observer/ObserverGenerator.cs
index 384f91c..8fc0de9 100644
--- a/src/PatternKit.Generators/Observer/ObserverGenerator.cs
+++ b/src/PatternKit.Generators/Observer/ObserverGenerator.cs
@@ -1,5 +1,6 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
+using System.Text;
namespace PatternKit.Generators.Observer;
@@ -10,14 +11,10 @@ namespace PatternKit.Generators.Observer;
[Generator]
public sealed class ObserverGenerator : IIncrementalGenerator
{
- // Diagnostic IDs
private const string DiagnosticIdNotPartial = "PKOBS001";
- private const string DiagnosticIdHubNotPartialStatic = "PKOBS002";
- private const string DiagnosticIdInvalidEventProperty = "PKOBS003";
- private const string DiagnosticIdAsyncUnsupported = "PKOBS004";
- private const string DiagnosticIdInvalidConfig = "PKOBS005";
+ private const string DiagnosticIdMissingPayload = "PKOBS002";
+ private const string DiagnosticIdInvalidConfig = "PKOBS003";
- // Diagnostic descriptors
private static readonly DiagnosticDescriptor NotPartialRule = new(
DiagnosticIdNotPartial,
"Type must be partial",
@@ -26,30 +23,14 @@ public sealed class ObserverGenerator : IIncrementalGenerator
DiagnosticSeverity.Error,
isEnabledByDefault: true);
- private static readonly DiagnosticDescriptor HubNotPartialStaticRule = new(
- DiagnosticIdHubNotPartialStatic,
- "Hub type must be partial and static",
- "Type '{0}' marked with [ObserverHub] must be declared as partial static",
+ private static readonly DiagnosticDescriptor MissingPayloadRule = new(
+ DiagnosticIdMissingPayload,
+ "Missing payload type",
+ "Unable to extract payload type from [Observer] attribute on '{0}'",
"PatternKit.Generators.Observer",
DiagnosticSeverity.Error,
isEnabledByDefault: true);
- private static readonly DiagnosticDescriptor InvalidEventPropertyRule = new(
- DiagnosticIdInvalidEventProperty,
- "Invalid event property",
- "Property '{0}' marked with [ObservedEvent] must be static partial with a getter only",
- "PatternKit.Generators.Observer",
- DiagnosticSeverity.Error,
- isEnabledByDefault: true);
-
- private static readonly DiagnosticDescriptor AsyncUnsupportedRule = new(
- DiagnosticIdAsyncUnsupported,
- "Async not supported",
- "Async publish requested but async handler shape is unsupported in this context",
- "PatternKit.Generators.Observer",
- DiagnosticSeverity.Warning,
- isEnabledByDefault: true);
-
private static readonly DiagnosticDescriptor InvalidConfigRule = new(
DiagnosticIdInvalidConfig,
"Invalid configuration",
@@ -60,31 +41,16 @@ public sealed class ObserverGenerator : IIncrementalGenerator
public void Initialize(IncrementalGeneratorInitializationContext context)
{
- // Find all types marked with [Observer]
var observerTypes = context.SyntaxProvider.ForAttributeWithMetadataName(
fullyQualifiedMetadataName: "PatternKit.Generators.Observer.ObserverAttribute",
predicate: static (node, _) => node is TypeDeclarationSyntax,
transform: static (ctx, _) => ctx
);
- // Generate observer implementations
context.RegisterSourceOutput(observerTypes, static (spc, occ) =>
{
GenerateObserver(spc, occ);
});
-
- // Find all types marked with [ObserverHub]
- var hubTypes = context.SyntaxProvider.ForAttributeWithMetadataName(
- fullyQualifiedMetadataName: "PatternKit.Generators.Observer.ObserverHubAttribute",
- predicate: static (node, _) => node is ClassDeclarationSyntax,
- transform: static (ctx, _) => ctx
- );
-
- // Generate hub implementations
- context.RegisterSourceOutput(hubTypes, static (spc, occ) =>
- {
- GenerateHub(spc, occ);
- });
}
private static void GenerateObserver(SourceProductionContext context, GeneratorAttributeSyntaxContext occurrence)
@@ -92,7 +58,6 @@ private static void GenerateObserver(SourceProductionContext context, GeneratorA
var typeSymbol = (INamedTypeSymbol)occurrence.TargetSymbol;
var syntax = (TypeDeclarationSyntax)occurrence.TargetNode;
- // Validate that type is partial
if (!IsPartial(syntax))
{
context.ReportDiagnostic(Diagnostic.Create(
@@ -102,658 +67,426 @@ private static void GenerateObserver(SourceProductionContext context, GeneratorA
return;
}
- // Extract configuration from attribute
- var config = ExtractObserverConfig(occurrence.Attributes[0]);
-
- // Validate configuration
- if (!ValidateConfiguration(config, context, syntax.GetLocation()))
- {
- return;
- }
-
- // Generate the source code
- var source = GenerateObserverSource(typeSymbol, config);
- var fileName = $"{typeSymbol.Name}.Observer.g.cs";
- context.AddSource(fileName, source);
- }
-
- private static void GenerateHub(SourceProductionContext context, GeneratorAttributeSyntaxContext occurrence)
- {
- var typeSymbol = (INamedTypeSymbol)occurrence.TargetSymbol;
- var syntax = (ClassDeclarationSyntax)occurrence.TargetNode;
-
- // Validate that type is partial and static
- if (!IsPartial(syntax) || !IsStatic(syntax))
+ var attr = occurrence.Attributes[0];
+ if (attr.ConstructorArguments.Length == 0 || attr.ConstructorArguments[0].Value is not INamedTypeSymbol payloadType)
{
context.ReportDiagnostic(Diagnostic.Create(
- HubNotPartialStaticRule,
+ MissingPayloadRule,
syntax.Identifier.GetLocation(),
typeSymbol.Name));
return;
}
- // Find all properties marked with [ObservedEvent]
- var eventProperties = typeSymbol.GetMembers()
- .OfType()
- .Where(p => p.GetAttributes()
- .Any(a => a.AttributeClass?.ToDisplayString() == "PatternKit.Generators.Observer.ObservedEventAttribute"))
- .ToList();
-
- // Validate event properties
- foreach (var prop in eventProperties)
- {
- if (!prop.IsStatic || prop.SetMethod != null)
- {
- context.ReportDiagnostic(Diagnostic.Create(
- InvalidEventPropertyRule,
- prop.Locations.FirstOrDefault() ?? Location.None,
- prop.Name));
- return;
- }
- }
-
- // Generate the hub source code
- var source = GenerateHubSource(typeSymbol, eventProperties);
- var fileName = $"{typeSymbol.Name}.ObserverHub.g.cs";
+ var config = ExtractConfig(attr);
+ var source = GenerateSource(typeSymbol, payloadType, config);
+ var fileName = $"{typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", "").Replace("<", "_").Replace(">", "_").Replace(".", "_")}.Observer.g.cs";
context.AddSource(fileName, source);
}
private static bool IsPartial(TypeDeclarationSyntax syntax)
- {
- return syntax.Modifiers.Any(m => m.Text == "partial");
- }
-
- private static bool IsStatic(TypeDeclarationSyntax syntax)
- {
- return syntax.Modifiers.Any(m => m.Text == "static");
- }
+ => syntax.Modifiers.Any(m => m.Text == "partial");
- private static ObserverConfig ExtractObserverConfig(AttributeData attribute)
+ private static ObserverConfig ExtractConfig(AttributeData attr)
{
var config = new ObserverConfig();
-
- foreach (var namedArg in attribute.NamedArguments)
+ foreach (var arg in attr.NamedArguments)
{
- switch (namedArg.Key)
+ switch (arg.Key)
{
case "Threading":
- config.Threading = (int)namedArg.Value.Value!;
+ config.Threading = (int)arg.Value.Value!;
break;
case "Exceptions":
- config.Exceptions = (int)namedArg.Value.Value!;
+ config.Exceptions = (int)arg.Value.Value!;
break;
case "Order":
- config.Order = (int)namedArg.Value.Value!;
+ config.Order = (int)arg.Value.Value!;
break;
case "GenerateAsync":
- config.GenerateAsync = (bool)namedArg.Value.Value!;
+ config.GenerateAsync = (bool)arg.Value.Value!;
break;
case "ForceAsync":
- config.ForceAsync = (bool)namedArg.Value.Value!;
+ config.ForceAsync = (bool)arg.Value.Value!;
break;
}
}
-
return config;
}
- private static bool ValidateConfiguration(ObserverConfig config, SourceProductionContext context, Location location)
- {
- // Validate: Concurrent + RegistrationOrder requires extra work
- if (config.Threading == 2 && config.Order == 0) // Concurrent + RegistrationOrder
- {
- context.ReportDiagnostic(Diagnostic.Create(
- InvalidConfigRule,
- location,
- "Concurrent threading with RegistrationOrder requires additional ordering guarantees and may impact performance"));
- // This is a warning, not an error, so we continue
- }
-
- return true;
- }
-
- private static string GenerateObserverSource(INamedTypeSymbol typeSymbol, ObserverConfig config)
+ private static string GenerateSource(INamedTypeSymbol typeSymbol, INamedTypeSymbol payloadType, ObserverConfig config)
{
var ns = typeSymbol.ContainingNamespace.IsGlobalNamespace
? null
: typeSymbol.ContainingNamespace.ToDisplayString();
- var typeKind = GetTypeKind(typeSymbol);
- var typeName = typeSymbol.Name;
+ var typeKind = typeSymbol.TypeKind switch
+ {
+ TypeKind.Class => typeSymbol.IsRecord ? "record class" : "class",
+ TypeKind.Struct => typeSymbol.IsRecord ? "record struct" : "struct",
+ _ => "class"
+ };
- var code = new CodeBuilder();
+ var accessibility = typeSymbol.DeclaredAccessibility switch
+ {
+ Accessibility.Public => "public",
+ Accessibility.Internal => "internal",
+ Accessibility.Private => "private",
+ Accessibility.Protected => "protected",
+ Accessibility.ProtectedOrInternal => "protected internal",
+ _ => "internal"
+ };
- code.AppendLine("#nullable enable");
- code.AppendLine("// ");
- code.AppendLine();
+ var typeName = typeSymbol.Name;
+ var payloadTypeName = payloadType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+
+ var sb = new StringBuilder();
+ sb.AppendLine("#nullable enable");
+ sb.AppendLine("// ");
+ sb.AppendLine();
+
+ // Add necessary using directives
+ if (config.Threading == 2 && config.Order == 0) // Concurrent + RegistrationOrder uses ImmutableList
+ {
+ sb.AppendLine("using System.Linq;");
+ sb.AppendLine();
+ }
if (ns != null)
{
- code.AppendLine($"namespace {ns};");
- code.AppendLine();
+ sb.AppendLine($"namespace {ns};");
+ sb.AppendLine();
}
- // Start type declaration
- code.Append($"{GetAccessibility(typeSymbol)} partial {typeKind} {typeName}");
- code.AppendLine();
- code.AppendLine("{");
-
- // Generate the implementation based on configuration
- GenerateObserverImplementation(code, config);
+ var isStruct = typeSymbol.TypeKind == TypeKind.Struct;
- code.AppendLine("}");
+ sb.AppendLine($"{accessibility} partial {typeKind} {typeName}");
+ sb.AppendLine("{");
- return code.ToString();
- }
-
- private static void GenerateObserverImplementation(CodeBuilder code, ObserverConfig config)
- {
- // Generate subscription storage based on threading policy
- GenerateStorage(code, config);
- code.AppendLine();
+ GenerateFields(sb, config, isStruct);
+ GenerateSubscribeMethods(sb, payloadTypeName, config);
+ GeneratePublishMethods(sb, payloadTypeName, config);
+ GenerateUnsubscribeMethod(sb, config);
+ GenerateOnErrorHook(sb);
+ GenerateSubscriptionClass(sb, payloadTypeName, config);
- // Generate Subscribe methods
- GenerateSubscribeMethods(code, config);
- code.AppendLine();
+ sb.AppendLine("}");
- // Generate Publish methods
- GeneratePublishMethods(code, config);
- code.AppendLine();
-
- // Generate Subscription class (IDisposable)
- GenerateSubscriptionClass(code, config);
+ return sb.ToString();
}
- private static void GenerateStorage(CodeBuilder code, ObserverConfig config)
+ private static void GenerateFields(StringBuilder sb, ObserverConfig config, bool isStruct)
{
- code.Indent();
-
+ // For all types, use nullable fields and ensure initialization in helper methods
switch (config.Threading)
{
case 0: // SingleThreadedFast
- code.AppendLine("private System.Collections.Generic.List _subscriptions = new();");
- code.AppendLine("private int _nextId;");
+ sb.AppendLine(" private System.Collections.Generic.List? _subscriptions;");
+ sb.AppendLine(" private int _nextId;");
break;
case 1: // Locking
- code.AppendLine("private readonly object _lock = new();");
- code.AppendLine("private System.Collections.Generic.List _subscriptions = new();");
- code.AppendLine("private int _nextId;");
+ sb.AppendLine(" private object? _lock;");
+ sb.AppendLine(" private System.Collections.Generic.List? _subscriptions;");
+ sb.AppendLine(" private int _nextId;");
break;
case 2: // Concurrent
if (config.Order == 0) // RegistrationOrder
{
- // Use ImmutableList for order preservation with concurrent access
- code.AppendLine("private System.Collections.Immutable.ImmutableList _subscriptions = System.Collections.Immutable.ImmutableList.Empty;");
- code.AppendLine("private int _nextId;");
+ sb.AppendLine(" private System.Collections.Immutable.ImmutableList? _subscriptions;");
+ sb.AppendLine(" private int _nextId;");
}
- else
+ else // Undefined
{
- // Can use ConcurrentBag for better performance
- code.AppendLine("private System.Collections.Concurrent.ConcurrentBag _subscriptions = new();");
- code.AppendLine("private int _nextId;");
+ sb.AppendLine(" private System.Collections.Concurrent.ConcurrentBag? _subscriptions;");
+ sb.AppendLine(" private int _nextId;");
}
break;
}
-
- code.Unindent();
+ sb.AppendLine();
}
- private static void GenerateSubscribeMethods(CodeBuilder code, ObserverConfig config)
+ private static void GenerateSubscribeMethods(StringBuilder sb, string payloadType, ObserverConfig config)
{
- code.Indent();
-
- // Determine generic parameter (we'll use object for now, but ideally would infer from context)
- var eventType = "TEvent";
-
if (!config.ForceAsync)
{
- // Generate sync Subscribe method
- code.AppendLine($"public System.IDisposable Subscribe<{eventType}>(System.Action<{eventType}> handler)");
- code.AppendLine("{");
- code.Indent();
- GenerateSubscribeBody(code, config, false);
- code.Unindent();
- code.AppendLine("}");
- code.AppendLine();
+ sb.AppendLine($" public System.IDisposable Subscribe(System.Action<{payloadType}> handler)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" var id = System.Threading.Interlocked.Increment(ref _nextId);");
+ sb.AppendLine(" var sub = new Subscription(this, id, handler, false);");
+ GenerateAddSubscription(sb, config, " ");
+ sb.AppendLine(" return sub;");
+ sb.AppendLine(" }");
+ sb.AppendLine();
}
if (config.GenerateAsync)
{
- // Generate async Subscribe method
- code.AppendLine($"public System.IDisposable Subscribe<{eventType}>(System.Func<{eventType}, System.Threading.Tasks.ValueTask> handler)");
- code.AppendLine("{");
- code.Indent();
- GenerateSubscribeBody(code, config, true);
- code.Unindent();
- code.AppendLine("}");
+ sb.AppendLine($" public System.IDisposable Subscribe(System.Func<{payloadType}, System.Threading.Tasks.ValueTask> handler)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" var id = System.Threading.Interlocked.Increment(ref _nextId);");
+ sb.AppendLine(" var sub = new Subscription(this, id, handler, true);");
+ GenerateAddSubscription(sb, config, " ");
+ sb.AppendLine(" return sub;");
+ sb.AppendLine(" }");
+ sb.AppendLine();
}
-
- code.Unindent();
}
- private static void GenerateSubscribeBody(CodeBuilder code, ObserverConfig config, bool isAsync)
+ private static void GenerateAddSubscription(StringBuilder sb, ObserverConfig config, string indent)
{
- code.AppendLine("var id = System.Threading.Interlocked.Increment(ref _nextId);");
- code.AppendLine($"var sub = new Subscription(this, id, handler, {(isAsync ? "true" : "false")});");
- code.AppendLine();
-
switch (config.Threading)
{
case 0: // SingleThreadedFast
- code.AppendLine("_subscriptions.Add(sub);");
+ sb.AppendLine($"{indent}(_subscriptions ??= new()).Add(sub);");
break;
case 1: // Locking
- code.AppendLine("lock (_lock)");
- code.AppendLine("{");
- code.Indent();
- code.AppendLine("_subscriptions.Add(sub);");
- code.Unindent();
- code.AppendLine("}");
+ sb.AppendLine($"{indent}lock (_lock ??= new())");
+ sb.AppendLine($"{indent}{{");
+ sb.AppendLine($"{indent} (_subscriptions ??= new()).Add(sub);");
+ sb.AppendLine($"{indent}}}");
break;
case 2: // Concurrent
if (config.Order == 0) // RegistrationOrder
{
- code.AppendLine("System.Collections.Immutable.ImmutableInterlocked.Update(ref _subscriptions, static (list, s) => list.Add(s), sub);");
+ sb.AppendLine($"{indent}System.Collections.Immutable.ImmutableInterlocked.Update(ref _subscriptions, static (list, s) => (list ?? System.Collections.Immutable.ImmutableList.Empty).Add(s), sub);");
}
- else
+ else // Undefined
{
- code.AppendLine("_subscriptions.Add(sub);");
+ sb.AppendLine($"{indent}(_subscriptions ??= new()).Add(sub);");
}
break;
}
-
- code.AppendLine();
- code.AppendLine("return sub;");
}
- private static void GeneratePublishMethods(CodeBuilder code, ObserverConfig config)
+ private static void GeneratePublishMethods(StringBuilder sb, string payloadType, ObserverConfig config)
{
- code.Indent();
-
- var eventType = "TEvent";
-
if (!config.ForceAsync)
{
- // Generate sync Publish method
- code.AppendLine($"public void Publish<{eventType}>({eventType} value)");
- code.AppendLine("{");
- code.Indent();
- GenerateSyncPublishBody(code, config);
- code.Unindent();
- code.AppendLine("}");
- code.AppendLine();
- }
-
- if (config.GenerateAsync)
- {
- // Generate async Publish method
- code.AppendLine($"public async System.Threading.Tasks.ValueTask PublishAsync<{eventType}>({eventType} value, System.Threading.CancellationToken cancellationToken = default)");
- code.AppendLine("{");
- code.Indent();
- GenerateAsyncPublishBody(code, config);
- code.Unindent();
- code.AppendLine("}");
- }
-
- code.Unindent();
- }
+ sb.AppendLine($" public void Publish({payloadType} payload)");
+ sb.AppendLine(" {");
+ GenerateSnapshot(sb, config, " ");
+ sb.AppendLine();
- private static void GenerateSyncPublishBody(CodeBuilder code, ObserverConfig config)
- {
- // Take snapshot based on threading policy
- GenerateSnapshot(code, config);
- code.AppendLine();
-
- // Generate exception handling setup
- if (config.Exceptions == 2) // Aggregate
- {
- code.AppendLine("System.Collections.Generic.List? errors = null;");
- code.AppendLine();
- }
-
- // Iterate through snapshot
- code.AppendLine("foreach (var sub in snapshot)");
- code.AppendLine("{");
- code.Indent();
+ if (config.Exceptions == 2) // Aggregate
+ {
+ sb.AppendLine(" System.Collections.Generic.List? errors = null;");
+ sb.AppendLine();
+ }
- code.AppendLine("if (sub.IsAsync) continue; // Skip async handlers in sync publish");
- code.AppendLine();
+ sb.AppendLine(" foreach (var sub in snapshot)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" if (sub.IsAsync) continue;");
- // Try-catch based on exception policy
- if (config.Exceptions == 1) // Stop
- {
- code.AppendLine("sub.InvokeSync(value);");
- }
- else
- {
- code.AppendLine("try");
- code.AppendLine("{");
- code.Indent();
- code.AppendLine("sub.InvokeSync(value);");
- code.Unindent();
- code.AppendLine("}");
- code.AppendLine("catch (System.Exception ex)");
- code.AppendLine("{");
- code.Indent();
-
- if (config.Exceptions == 0) // Continue
+ if (config.Exceptions == 1) // Stop
{
- code.AppendLine("// Optionally call OnSubscriberError if defined");
- code.AppendLine("OnSubscriberError(ex);");
+ sb.AppendLine(" sub.InvokeSync(payload);");
}
- else if (config.Exceptions == 2) // Aggregate
+ else
{
- code.AppendLine("(errors ??= new()).Add(ex);");
+ sb.AppendLine(" try");
+ sb.AppendLine(" {");
+ sb.AppendLine(" sub.InvokeSync(payload);");
+ sb.AppendLine(" }");
+ sb.AppendLine(" catch (System.Exception ex)");
+ sb.AppendLine(" {");
+ if (config.Exceptions == 0) // Continue
+ {
+ sb.AppendLine(" OnSubscriberError(ex);");
+ }
+ else // Aggregate
+ {
+ sb.AppendLine(" (errors ??= new()).Add(ex);");
+ }
+ sb.AppendLine(" }");
}
- code.Unindent();
- code.AppendLine("}");
- }
+ sb.AppendLine(" }");
- code.Unindent();
- code.AppendLine("}");
+ if (config.Exceptions == 2)
+ {
+ sb.AppendLine();
+ sb.AppendLine(" if (errors is { Count: > 0 })");
+ sb.AppendLine(" throw new System.AggregateException(errors);");
+ }
- // Throw aggregate if needed
- if (config.Exceptions == 2)
- {
- code.AppendLine();
- code.AppendLine("if (errors is { Count: > 0 })");
- code.AppendLine("{");
- code.Indent();
- code.AppendLine("throw new System.AggregateException(errors);");
- code.Unindent();
- code.AppendLine("}");
+ sb.AppendLine(" }");
+ sb.AppendLine();
}
- }
-
- private static void GenerateAsyncPublishBody(CodeBuilder code, ObserverConfig config)
- {
- // Take snapshot
- GenerateSnapshot(code, config);
- code.AppendLine();
- // Generate exception handling setup
- if (config.Exceptions == 2) // Aggregate
+ if (config.GenerateAsync)
{
- code.AppendLine("System.Collections.Generic.List? errors = null;");
- code.AppendLine();
- }
+ sb.AppendLine($" public async System.Threading.Tasks.ValueTask PublishAsync({payloadType} payload, System.Threading.CancellationToken cancellationToken = default)");
+ sb.AppendLine(" {");
+ GenerateSnapshot(sb, config, " ");
+ sb.AppendLine();
- // Iterate through snapshot
- code.AppendLine("foreach (var sub in snapshot)");
- code.AppendLine("{");
- code.Indent();
+ if (config.Exceptions == 2)
+ {
+ sb.AppendLine(" System.Collections.Generic.List? errors = null;");
+ sb.AppendLine();
+ }
- // Check cancellation
- code.AppendLine("if (cancellationToken.IsCancellationRequested) break;");
- code.AppendLine();
+ sb.AppendLine(" foreach (var sub in snapshot)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" cancellationToken.ThrowIfCancellationRequested();");
- // Try-catch based on exception policy
- if (config.Exceptions == 1) // Stop
- {
- code.AppendLine("await sub.InvokeAsync(value, cancellationToken).ConfigureAwait(false);");
- }
- else
- {
- code.AppendLine("try");
- code.AppendLine("{");
- code.Indent();
- code.AppendLine("await sub.InvokeAsync(value, cancellationToken).ConfigureAwait(false);");
- code.Unindent();
- code.AppendLine("}");
- code.AppendLine("catch (System.Exception ex)");
- code.AppendLine("{");
- code.Indent();
-
- if (config.Exceptions == 0) // Continue
+ if (config.Exceptions == 1) // Stop
{
- code.AppendLine("OnSubscriberError(ex);");
+ sb.AppendLine(" await sub.InvokeAsync(payload, cancellationToken).ConfigureAwait(false);");
}
- else if (config.Exceptions == 2) // Aggregate
+ else
{
- code.AppendLine("(errors ??= new()).Add(ex);");
+ sb.AppendLine(" try");
+ sb.AppendLine(" {");
+ sb.AppendLine(" await sub.InvokeAsync(payload, cancellationToken).ConfigureAwait(false);");
+ sb.AppendLine(" }");
+ sb.AppendLine(" catch (System.Exception ex)");
+ sb.AppendLine(" {");
+ if (config.Exceptions == 0) // Continue
+ {
+ sb.AppendLine(" OnSubscriberError(ex);");
+ }
+ else // Aggregate
+ {
+ sb.AppendLine(" (errors ??= new()).Add(ex);");
+ }
+ sb.AppendLine(" }");
}
- code.Unindent();
- code.AppendLine("}");
- }
+ sb.AppendLine(" }");
- code.Unindent();
- code.AppendLine("}");
+ if (config.Exceptions == 2)
+ {
+ sb.AppendLine();
+ sb.AppendLine(" if (errors is { Count: > 0 })");
+ sb.AppendLine(" throw new System.AggregateException(errors);");
+ }
- // Throw aggregate if needed
- if (config.Exceptions == 2)
- {
- code.AppendLine();
- code.AppendLine("if (errors is { Count: > 0 })");
- code.AppendLine("{");
- code.Indent();
- code.AppendLine("throw new System.AggregateException(errors);");
- code.Unindent();
- code.AppendLine("}");
+ sb.AppendLine(" }");
+ sb.AppendLine();
}
}
- private static void GenerateSnapshot(CodeBuilder code, ObserverConfig config)
+ private static void GenerateSnapshot(StringBuilder sb, ObserverConfig config, string indent)
{
switch (config.Threading)
{
case 0: // SingleThreadedFast
- code.AppendLine("var snapshot = _subscriptions.ToArray();");
+ sb.AppendLine($"{indent}var snapshot = _subscriptions?.ToArray() ?? System.Array.Empty();");
break;
case 1: // Locking
- code.AppendLine("Subscription[] snapshot;");
- code.AppendLine("lock (_lock)");
- code.AppendLine("{");
- code.Indent();
- code.AppendLine("snapshot = _subscriptions.ToArray();");
- code.Unindent();
- code.AppendLine("}");
+ sb.AppendLine($"{indent}Subscription[] snapshot;");
+ sb.AppendLine($"{indent}lock (_lock ?? new object())");
+ sb.AppendLine($"{indent}{{");
+ sb.AppendLine($"{indent} snapshot = _subscriptions?.ToArray() ?? System.Array.Empty();");
+ sb.AppendLine($"{indent}}}");
break;
case 2: // Concurrent
if (config.Order == 0) // RegistrationOrder
{
- code.AppendLine("var snapshot = System.Threading.Volatile.Read(ref _subscriptions).ToArray();");
+ sb.AppendLine($"{indent}var snapshot = System.Threading.Volatile.Read(ref _subscriptions)?.ToArray() ?? System.Array.Empty();");
}
- else
+ else // Undefined
{
- code.AppendLine("var snapshot = _subscriptions.ToArray();");
+ sb.AppendLine($"{indent}var snapshot = _subscriptions?.ToArray() ?? System.Array.Empty();");
}
break;
}
}
- private static void GenerateSubscriptionClass(CodeBuilder code, ObserverConfig config)
+ private static void GenerateUnsubscribeMethod(StringBuilder sb, ObserverConfig config)
{
- code.Indent();
-
- code.AppendLine("partial void OnSubscriberError(System.Exception ex) { }");
- code.AppendLine();
-
- code.AppendLine("private void Unsubscribe(int id)");
- code.AppendLine("{");
- code.Indent();
+ sb.AppendLine(" private void Unsubscribe(int id)");
+ sb.AppendLine(" {");
switch (config.Threading)
{
case 0: // SingleThreadedFast
- code.AppendLine("_subscriptions.RemoveAll(s => s.Id == id);");
+ sb.AppendLine(" _subscriptions?.RemoveAll(s => s.Id == id);");
break;
case 1: // Locking
- code.AppendLine("lock (_lock)");
- code.AppendLine("{");
- code.Indent();
- code.AppendLine("_subscriptions.RemoveAll(s => s.Id == id);");
- code.Unindent();
- code.AppendLine("}");
+ sb.AppendLine(" lock (_lock ?? new object())");
+ sb.AppendLine(" {");
+ sb.AppendLine(" _subscriptions?.RemoveAll(s => s.Id == id);");
+ sb.AppendLine(" }");
break;
case 2: // Concurrent
if (config.Order == 0) // RegistrationOrder
{
- code.AppendLine("System.Collections.Immutable.ImmutableInterlocked.Update(ref _subscriptions, static (list, id) => list.RemoveAll(s => s.Id == id), id);");
+ sb.AppendLine(" System.Collections.Immutable.ImmutableInterlocked.Update(ref _subscriptions, static (list, id) => list?.RemoveAll(s => s.Id == id) ?? list, id);");
}
- else
+ else // Undefined - ConcurrentBag doesn't support efficient removal
{
- code.AppendLine("// Note: ConcurrentBag doesn't support removal efficiently");
- code.AppendLine("// Mark as removed instead");
+ sb.AppendLine(" // ConcurrentBag doesn't support removal; subscription marks itself disposed");
}
break;
}
- code.Unindent();
- code.AppendLine("}");
- code.AppendLine();
-
- // Generate Subscription nested class
- code.AppendLine("private sealed class Subscription : System.IDisposable");
- code.AppendLine("{");
- code.Indent();
-
- code.AppendLine("private readonly object _parent;");
- code.AppendLine("private readonly int _id;");
- code.AppendLine("private readonly object _handler;");
- code.AppendLine("private readonly bool _isAsync;");
- code.AppendLine("private int _disposed;");
- code.AppendLine();
-
- code.AppendLine("public int Id => _id;");
- code.AppendLine("public bool IsAsync => _isAsync;");
- code.AppendLine();
-
- code.AppendLine("public Subscription(object parent, int id, object handler, bool isAsync)");
- code.AppendLine("{");
- code.Indent();
- code.AppendLine("_parent = parent;");
- code.AppendLine("_id = id;");
- code.AppendLine("_handler = handler;");
- code.AppendLine("_isAsync = isAsync;");
- code.Unindent();
- code.AppendLine("}");
- code.AppendLine();
-
- code.AppendLine("public void InvokeSync(TEvent value)");
- code.AppendLine("{");
- code.Indent();
- code.AppendLine("if (System.Threading.Volatile.Read(ref _disposed) != 0) return;");
- code.AppendLine("((System.Action)_handler)(value);");
- code.Unindent();
- code.AppendLine("}");
- code.AppendLine();
-
- code.AppendLine("public System.Threading.Tasks.ValueTask InvokeAsync(TEvent value, System.Threading.CancellationToken ct)");
- code.AppendLine("{");
- code.Indent();
- code.AppendLine("if (System.Threading.Volatile.Read(ref _disposed) != 0) return default;");
- code.AppendLine("if (_isAsync)");
- code.AppendLine("{");
- code.Indent();
- code.AppendLine("return ((System.Func)_handler)(value);");
- code.Unindent();
- code.AppendLine("}");
- code.AppendLine("else");
- code.AppendLine("{");
- code.Indent();
- code.AppendLine("((System.Action)_handler)(value);");
- code.AppendLine("return default;");
- code.Unindent();
- code.AppendLine("}");
- code.Unindent();
- code.AppendLine("}");
- code.AppendLine();
-
- code.AppendLine("public void Dispose()");
- code.AppendLine("{");
- code.Indent();
- code.AppendLine("if (System.Threading.Interlocked.Exchange(ref _disposed, 1) != 0) return;");
- code.AppendLine("((dynamic)_parent).Unsubscribe(_id);");
- code.Unindent();
- code.AppendLine("}");
-
- code.Unindent();
- code.AppendLine("}");
-
- code.Unindent();
- }
-
- private static string GenerateHubSource(INamedTypeSymbol typeSymbol, List eventProperties)
- {
- var ns = typeSymbol.ContainingNamespace.IsGlobalNamespace
- ? null
- : typeSymbol.ContainingNamespace.ToDisplayString();
-
- var typeName = typeSymbol.Name;
-
- var code = new CodeBuilder();
-
- code.AppendLine("#nullable enable");
- code.AppendLine("// ");
- code.AppendLine();
-
- if (ns != null)
- {
- code.AppendLine($"namespace {ns};");
- code.AppendLine();
- }
-
- code.AppendLine($"{GetAccessibility(typeSymbol)} static partial class {typeName}");
- code.AppendLine("{");
- code.Indent();
-
- // Generate each event property
- foreach (var prop in eventProperties)
- {
- var propType = prop.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
- var propName = prop.Name;
-
- code.AppendLine($"private static readonly {propType} _{char.ToLower(propName[0])}{propName.Substring(1)} = new();");
- code.AppendLine();
- code.AppendLine($"public static partial {propType} {propName}");
- code.AppendLine("{");
- code.Indent();
- code.AppendLine($"get => _{char.ToLower(propName[0])}{propName.Substring(1)};");
- code.Unindent();
- code.AppendLine("}");
- code.AppendLine();
- }
-
- code.Unindent();
- code.AppendLine("}");
-
- return code.ToString();
+ sb.AppendLine(" }");
+ sb.AppendLine();
}
- private static string GetTypeKind(INamedTypeSymbol symbol)
+ private static void GenerateOnErrorHook(StringBuilder sb)
{
- return symbol.TypeKind switch
- {
- TypeKind.Class => symbol.IsRecord ? "record class" : "class",
- TypeKind.Struct => symbol.IsRecord ? "record struct" : "struct",
- _ => "class"
- };
+ sb.AppendLine(" partial void OnSubscriberError(System.Exception ex);");
+ sb.AppendLine();
}
- private static string GetAccessibility(ISymbol symbol)
+ private static void GenerateSubscriptionClass(StringBuilder sb, string payloadType, ObserverConfig config)
{
- return symbol.DeclaredAccessibility switch
- {
- Accessibility.Public => "public",
- Accessibility.Internal => "internal",
- Accessibility.Private => "private",
- Accessibility.Protected => "protected",
- Accessibility.ProtectedOrInternal => "protected internal",
- _ => "internal"
- };
+ sb.AppendLine(" private sealed class Subscription : System.IDisposable");
+ sb.AppendLine(" {");
+ sb.AppendLine(" private readonly object _parent;");
+ sb.AppendLine(" private readonly int _id;");
+ sb.AppendLine(" private readonly object _handler;");
+ sb.AppendLine(" private readonly bool _isAsync;");
+ sb.AppendLine(" private int _disposed;");
+ sb.AppendLine();
+ sb.AppendLine(" public int Id => _id;");
+ sb.AppendLine(" public bool IsAsync => _isAsync;");
+ sb.AppendLine();
+ sb.AppendLine(" public Subscription(object parent, int id, object handler, bool isAsync)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" _parent = parent;");
+ sb.AppendLine(" _id = id;");
+ sb.AppendLine(" _handler = handler;");
+ sb.AppendLine(" _isAsync = isAsync;");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine($" public void InvokeSync({payloadType} payload)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" if (System.Threading.Volatile.Read(ref _disposed) != 0) return;");
+ sb.AppendLine($" ((System.Action<{payloadType}>)_handler)(payload);");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine($" public System.Threading.Tasks.ValueTask InvokeAsync({payloadType} payload, System.Threading.CancellationToken ct)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" if (System.Threading.Volatile.Read(ref _disposed) != 0) return default;");
+ sb.AppendLine(" if (_isAsync)");
+ sb.AppendLine($" return ((System.Func<{payloadType}, System.Threading.Tasks.ValueTask>)_handler)(payload);");
+ sb.AppendLine(" else");
+ sb.AppendLine(" {");
+ sb.AppendLine($" ((System.Action<{payloadType}>)_handler)(payload);");
+ sb.AppendLine(" return default;");
+ sb.AppendLine(" }");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(" public void Dispose()");
+ sb.AppendLine(" {");
+ sb.AppendLine(" if (System.Threading.Interlocked.Exchange(ref _disposed, 1) != 0) return;");
+ sb.AppendLine(" ((dynamic)_parent).Unsubscribe(_id);");
+ sb.AppendLine(" }");
+ sb.AppendLine(" }");
}
- private class ObserverConfig
+ private sealed class ObserverConfig
{
public int Threading { get; set; } = 1; // Locking
public int Exceptions { get; set; } = 0; // Continue
@@ -761,29 +494,4 @@ private class ObserverConfig
public bool GenerateAsync { get; set; } = true;
public bool ForceAsync { get; set; } = false;
}
-
- private class CodeBuilder
- {
- private readonly System.Text.StringBuilder _sb = new();
- private int _indentLevel = 0;
- private const string IndentString = " ";
-
- public void Indent() => _indentLevel++;
- public void Unindent() => _indentLevel = Math.Max(0, _indentLevel - 1);
-
- public void Append(string text) => _sb.Append(text);
-
- public void AppendLine(string text = "")
- {
- if (!string.IsNullOrEmpty(text))
- {
- for (int i = 0; i < _indentLevel; i++)
- _sb.Append(IndentString);
- _sb.Append(text);
- }
- _sb.AppendLine();
- }
-
- public override string ToString() => _sb.ToString();
- }
}
From 7d51442761f78923724288530e55ebd2a185ad54 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 19 Feb 2026 04:38:58 +0000
Subject: [PATCH 05/13] Add payload type constructor to ObserverAttribute and
fix thread safety 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>
---
.../Observer/ObserverAttribute.cs | 16 +++++++++++++++-
.../Observer/ObserverGenerator.cs | 10 ++++++----
2 files changed, 21 insertions(+), 5 deletions(-)
diff --git a/src/PatternKit.Generators.Abstractions/Observer/ObserverAttribute.cs b/src/PatternKit.Generators.Abstractions/Observer/ObserverAttribute.cs
index 8057e11..7d40bdc 100644
--- a/src/PatternKit.Generators.Abstractions/Observer/ObserverAttribute.cs
+++ b/src/PatternKit.Generators.Abstractions/Observer/ObserverAttribute.cs
@@ -12,7 +12,7 @@ namespace PatternKit.Generators.Observer;
///
/// Example:
///
-/// [Observer]
+/// [Observer(typeof(Temperature))]
/// public partial class TemperatureChanged { }
///
///
@@ -20,6 +20,20 @@ namespace PatternKit.Generators.Observer;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
public sealed class ObserverAttribute : Attribute
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The type of the event payload.
+ public ObserverAttribute(Type payloadType)
+ {
+ PayloadType = payloadType;
+ }
+
+ ///
+ /// Gets the type of the event payload.
+ ///
+ public Type PayloadType { get; }
+
///
/// Gets or sets the threading policy for Subscribe/Unsubscribe/Publish operations.
/// Default is .
diff --git a/src/PatternKit.Generators/Observer/ObserverGenerator.cs b/src/PatternKit.Generators/Observer/ObserverGenerator.cs
index 8fc0de9..74280b7 100644
--- a/src/PatternKit.Generators/Observer/ObserverGenerator.cs
+++ b/src/PatternKit.Generators/Observer/ObserverGenerator.cs
@@ -67,8 +67,8 @@ private static void GenerateObserver(SourceProductionContext context, GeneratorA
return;
}
- var attr = occurrence.Attributes[0];
- if (attr.ConstructorArguments.Length == 0 || attr.ConstructorArguments[0].Value is not INamedTypeSymbol payloadType)
+ var attr = occurrence.Attributes.Length > 0 ? occurrence.Attributes[0] : null;
+ if (attr == null || attr.ConstructorArguments.Length == 0 || attr.ConstructorArguments[0].Value is not INamedTypeSymbol payloadType)
{
context.ReportDiagnostic(Diagnostic.Create(
MissingPayloadRule,
@@ -380,7 +380,8 @@ private static void GenerateSnapshot(StringBuilder sb, ObserverConfig config, st
case 1: // Locking
sb.AppendLine($"{indent}Subscription[] snapshot;");
- sb.AppendLine($"{indent}lock (_lock ?? new object())");
+ sb.AppendLine($"{indent}var lockObj = _lock ??= new object();");
+ sb.AppendLine($"{indent}lock (lockObj)");
sb.AppendLine($"{indent}{{");
sb.AppendLine($"{indent} snapshot = _subscriptions?.ToArray() ?? System.Array.Empty();");
sb.AppendLine($"{indent}}}");
@@ -411,7 +412,8 @@ private static void GenerateUnsubscribeMethod(StringBuilder sb, ObserverConfig c
break;
case 1: // Locking
- sb.AppendLine(" lock (_lock ?? new object())");
+ sb.AppendLine(" var lockObj = _lock ??= new object();");
+ sb.AppendLine(" lock (lockObj)");
sb.AppendLine(" {");
sb.AppendLine(" _subscriptions?.RemoveAll(s => s.Id == id);");
sb.AppendLine(" }");
From ca8f6c8bdede00575bd75019719571ac78c52bf7 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 19 Feb 2026 04:42:41 +0000
Subject: [PATCH 06/13] Complete rewrite of ObserverGenerator with payload type
support
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
.../packages.lock.json | 16 ++++++++++++++--
1 file changed, 14 insertions(+), 2 deletions(-)
diff --git a/test/PatternKit.Generators.Tests/packages.lock.json b/test/PatternKit.Generators.Tests/packages.lock.json
index 55fe82a..00b627e 100644
--- a/test/PatternKit.Generators.Tests/packages.lock.json
+++ b/test/PatternKit.Generators.Tests/packages.lock.json
@@ -386,7 +386,10 @@
"Microsoft.TestPlatform.ObjectModel": {
"type": "Transitive",
"resolved": "18.0.1",
- "contentHash": "qT/mwMcLF9BieRkzOBPL2qCopl8hQu6A1P7JWAoj/FMu5i9vds/7cjbJ/LLtaiwWevWLAeD5v5wjQJ/l6jvhWQ=="
+ "contentHash": "qT/mwMcLF9BieRkzOBPL2qCopl8hQu6A1P7JWAoj/FMu5i9vds/7cjbJ/LLtaiwWevWLAeD5v5wjQJ/l6jvhWQ==",
+ "dependencies": {
+ "System.Reflection.Metadata": "8.0.0"
+ }
},
"Microsoft.TestPlatform.TestHost": {
"type": "Transitive",
@@ -407,6 +410,14 @@
"resolved": "10.0.3",
"contentHash": "+bZnyzt0/vt4g3QSllhsRNGTpa09p7Juy5K8spcK73cOTOefu4+HoY89hZOgIOmzB5A4hqPyEDKnzra7KKnhZw=="
},
+ "System.Reflection.Metadata": {
+ "type": "Transitive",
+ "resolved": "8.0.0",
+ "contentHash": "ptvgrFh7PvWI8bcVqG5rsA/weWM09EnthFHR5SCnS6IN+P4mj6rE1lBDC4U8HL9/57htKAqy4KQ3bBj84cfYyQ==",
+ "dependencies": {
+ "System.Collections.Immutable": "8.0.0"
+ }
+ },
"xunit.abstractions": {
"type": "Transitive",
"resolved": "2.0.3",
@@ -469,7 +480,8 @@
"patternkit.generators": {
"type": "Project",
"dependencies": {
- "PatternKit.Generators.Abstractions": "[1.0.0, )"
+ "PatternKit.Generators.Abstractions": "[1.0.0, )",
+ "System.Collections.Immutable": "[10.0.3, )"
}
},
"patternkit.generators.abstractions": {
From 1a04bcdf79ccf5563555a9e53753a78509c5f0f1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 19 Feb 2026 04:46:56 +0000
Subject: [PATCH 07/13] Fix dynamic cast issue in ObserverGenerator, 10/12
tests passing
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
.../Observer/ObserverGenerator.cs | 11 +-
.../ObserverGeneratorTests.cs | 613 ++++++++++++++++++
2 files changed, 621 insertions(+), 3 deletions(-)
create mode 100644 test/PatternKit.Generators.Tests/ObserverGeneratorTests.cs
diff --git a/src/PatternKit.Generators/Observer/ObserverGenerator.cs b/src/PatternKit.Generators/Observer/ObserverGenerator.cs
index 74280b7..b8c5c4c 100644
--- a/src/PatternKit.Generators/Observer/ObserverGenerator.cs
+++ b/src/PatternKit.Generators/Observer/ObserverGenerator.cs
@@ -443,9 +443,10 @@ private static void GenerateOnErrorHook(StringBuilder sb)
private static void GenerateSubscriptionClass(StringBuilder sb, string payloadType, ObserverConfig config)
{
- sb.AppendLine(" private sealed class Subscription : System.IDisposable");
+ // Use generic parent reference to avoid dynamic
+ sb.AppendLine($" private sealed class Subscription : System.IDisposable");
sb.AppendLine(" {");
- sb.AppendLine(" private readonly object _parent;");
+ sb.AppendLine(" private object? _parent;");
sb.AppendLine(" private readonly int _id;");
sb.AppendLine(" private readonly object _handler;");
sb.AppendLine(" private readonly bool _isAsync;");
@@ -483,7 +484,11 @@ private static void GenerateSubscriptionClass(StringBuilder sb, string payloadTy
sb.AppendLine(" public void Dispose()");
sb.AppendLine(" {");
sb.AppendLine(" if (System.Threading.Interlocked.Exchange(ref _disposed, 1) != 0) return;");
- sb.AppendLine(" ((dynamic)_parent).Unsubscribe(_id);");
+ sb.AppendLine(" // Use reflection to call Unsubscribe since parent type is generic");
+ sb.AppendLine(" var parent = System.Threading.Interlocked.Exchange(ref _parent, null);");
+ sb.AppendLine(" if (parent == null) return;");
+ sb.AppendLine(" var method = parent.GetType().GetMethod(\"Unsubscribe\", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);");
+ sb.AppendLine(" method?.Invoke(parent, new object[] { _id });");
sb.AppendLine(" }");
sb.AppendLine(" }");
}
diff --git a/test/PatternKit.Generators.Tests/ObserverGeneratorTests.cs b/test/PatternKit.Generators.Tests/ObserverGeneratorTests.cs
new file mode 100644
index 0000000..e2fc4b8
--- /dev/null
+++ b/test/PatternKit.Generators.Tests/ObserverGeneratorTests.cs
@@ -0,0 +1,613 @@
+using Microsoft.CodeAnalysis;
+using System.Runtime.Loader;
+
+namespace PatternKit.Generators.Tests;
+
+public class ObserverGeneratorTests
+{
+ private const string SimpleObserver = """
+ using PatternKit.Generators.Observer;
+
+ namespace PatternKit.Examples.Generators;
+
+ public record Temperature(double Celsius);
+
+ [Observer(typeof(Temperature))]
+ public partial class TemperatureChanged
+ {
+ }
+ """;
+
+ [Fact]
+ public void Generates_Observer_Without_Diagnostics()
+ {
+ var comp = RoslynTestHelpers.CreateCompilation(
+ SimpleObserver,
+ assemblyName: nameof(Generates_Observer_Without_Diagnostics));
+
+ var gen = new Observer.ObserverGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var run, out var updated);
+
+ // No generator diagnostics
+ Assert.All(run.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Confirm we generated expected file
+ var sources = run.Results.SelectMany(r => r.GeneratedSources).ToArray();
+ Assert.Single(sources);
+ Assert.Contains("Observer.g.cs", sources[0].HintName);
+
+ // The updated compilation should compile
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void Reports_Error_When_Type_Not_Partial()
+ {
+ var code = """
+ using PatternKit.Generators.Observer;
+ namespace Test;
+ public record Temperature(double Celsius);
+
+ [Observer(typeof(Temperature))]
+ public class TemperatureChanged { }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(
+ code,
+ assemblyName: nameof(Reports_Error_When_Type_Not_Partial));
+
+ var gen = new Observer.ObserverGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var run, out _);
+
+ var diagnostics = run.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKOBS001");
+ }
+
+ [Fact]
+ public void Subscribe_And_Publish_Works()
+ {
+ var user = SimpleObserver + """
+
+ public static class Demo
+ {
+ public static string Run()
+ {
+ var log = new System.Collections.Generic.List();
+ var evt = new TemperatureChanged();
+
+ evt.Subscribe((Temperature t) => log.Add($"Handler1:{t.Celsius}"));
+ evt.Subscribe((Temperature t) => log.Add($"Handler2:{t.Celsius}"));
+
+ evt.Publish(new Temperature(23.5));
+
+ return string.Join("|", log);
+ }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(
+ user,
+ assemblyName: nameof(Subscribe_And_Publish_Works));
+
+ var gen = new Observer.ObserverGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out _, out var updated);
+
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+
+ // Load and invoke Demo.Run
+ using var pe = new MemoryStream();
+ using var pdb = new MemoryStream();
+ var res = updated.Emit(pe, pdb);
+ Assert.True(res.Success);
+
+ pe.Position = 0;
+ pdb.Position = 0;
+
+ var alc = new AssemblyLoadContext("ObserverTest", isCollectible: true);
+ try
+ {
+ var asm = alc.LoadFromStream(pe, pdb);
+ var demoType = asm.GetType("PatternKit.Examples.Generators.Demo");
+ Assert.NotNull(demoType);
+
+ var runMethod = demoType.GetMethod("Run");
+ Assert.NotNull(runMethod);
+
+ var result = (string)runMethod.Invoke(null, null)!;
+ Assert.Equal("Handler1:23.5|Handler2:23.5", result);
+ }
+ finally
+ {
+ alc.Unload();
+ }
+ }
+
+ [Fact]
+ public void Dispose_Removes_Subscription()
+ {
+ var user = SimpleObserver + """
+
+ public static class Demo
+ {
+ public static string Run()
+ {
+ var log = new System.Collections.Generic.List();
+ var evt = new TemperatureChanged();
+
+ var sub1 = evt.Subscribe((Temperature t) => log.Add($"H1:{t.Celsius}"));
+ var sub2 = evt.Subscribe((Temperature t) => log.Add($"H2:{t.Celsius}"));
+
+ evt.Publish(new Temperature(10));
+ sub1.Dispose();
+ evt.Publish(new Temperature(20));
+
+ return string.Join("|", log);
+ }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(
+ user,
+ assemblyName: nameof(Dispose_Removes_Subscription));
+
+ var gen = new Observer.ObserverGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out _, out var updated);
+
+ using var pe = new MemoryStream();
+ updated.Emit(pe);
+ pe.Position = 0;
+
+ var alc = new AssemblyLoadContext("ObserverTest", isCollectible: true);
+ try
+ {
+ var asm = alc.LoadFromStream(pe);
+ var demoType = asm.GetType("PatternKit.Examples.Generators.Demo");
+ var runMethod = demoType!.GetMethod("Run");
+ var result = (string)runMethod!.Invoke(null, null)!;
+
+ // After first publish: both handlers; after second: only H2
+ Assert.Equal("H1:10|H2:10|H2:20", result);
+ }
+ finally
+ {
+ alc.Unload();
+ }
+ }
+
+ [Fact]
+ public void Registration_Order_Preserved()
+ {
+ var user = SimpleObserver + """
+
+ public static class Demo
+ {
+ public static string Run()
+ {
+ var log = new System.Collections.Generic.List();
+ var evt = new TemperatureChanged();
+
+ evt.Subscribe((Temperature t) => log.Add("A"));
+ evt.Subscribe((Temperature t) => log.Add("B"));
+ evt.Subscribe((Temperature t) => log.Add("C"));
+
+ evt.Publish(new Temperature(0));
+
+ return string.Join("", log);
+ }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(
+ user,
+ assemblyName: nameof(Registration_Order_Preserved));
+
+ var gen = new Observer.ObserverGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out _, out var updated);
+
+ using var pe = new MemoryStream();
+ updated.Emit(pe);
+ pe.Position = 0;
+
+ var alc = new AssemblyLoadContext("ObserverTest", isCollectible: true);
+ try
+ {
+ var asm = alc.LoadFromStream(pe);
+ var demoType = asm.GetType("PatternKit.Examples.Generators.Demo");
+ var runMethod = demoType!.GetMethod("Run");
+ var result = (string)runMethod!.Invoke(null, null)!;
+ Assert.Equal("ABC", result);
+ }
+ finally
+ {
+ alc.Unload();
+ }
+ }
+
+ [Fact]
+ public void Async_Subscribe_And_PublishAsync_Works()
+ {
+ var user = SimpleObserver + """
+
+ public static class Demo
+ {
+ public static async System.Threading.Tasks.Task Run()
+ {
+ var log = new System.Collections.Generic.List();
+ var evt = new TemperatureChanged();
+
+ evt.Subscribe(async (Temperature t) =>
+ {
+ await System.Threading.Tasks.Task.Delay(1);
+ log.Add($"AsyncHandler:{t.Celsius}");
+ });
+
+ await evt.PublishAsync(new Temperature(42));
+
+ return string.Join("|", log);
+ }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(
+ user,
+ assemblyName: nameof(Async_Subscribe_And_PublishAsync_Works));
+
+ var gen = new Observer.ObserverGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out _, out var updated);
+
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+
+ using var pe = new MemoryStream();
+ updated.Emit(pe);
+ pe.Position = 0;
+
+ var alc = new AssemblyLoadContext("ObserverTest", isCollectible: true);
+ try
+ {
+ var asm = alc.LoadFromStream(pe);
+ var demoType = asm.GetType("PatternKit.Examples.Generators.Demo");
+ var runMethod = demoType!.GetMethod("Run");
+ var task = (System.Threading.Tasks.Task)runMethod!.Invoke(null, null)!;
+ task.Wait();
+ var result = task.Result;
+ Assert.Equal("AsyncHandler:42", result);
+ }
+ finally
+ {
+ alc.Unload();
+ }
+ }
+
+ [Fact]
+ public void Exception_Policy_Continue_Does_Not_Stop_Execution()
+ {
+ var user = """
+ using PatternKit.Generators.Observer;
+
+ namespace PatternKit.Examples.Generators;
+
+ public record Temperature(double Celsius);
+
+ [Observer(typeof(Temperature), Exceptions = ObserverExceptionPolicy.Continue)]
+ public partial class TemperatureChanged
+ {
+ partial void OnSubscriberError(System.Exception ex)
+ {
+ // Swallow the error
+ }
+ }
+
+ public static class Demo
+ {
+ public static string Run()
+ {
+ var log = new System.Collections.Generic.List();
+ var evt = new TemperatureChanged();
+
+ evt.Subscribe((Temperature t) => log.Add("H1"));
+ evt.Subscribe((Temperature t) => throw new System.Exception("Oops"));
+ evt.Subscribe((Temperature t) => log.Add("H3"));
+
+ evt.Publish(new Temperature(0));
+
+ return string.Join("|", log);
+ }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(
+ user,
+ assemblyName: nameof(Exception_Policy_Continue_Does_Not_Stop_Execution));
+
+ var gen = new Observer.ObserverGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out _, out var updated);
+
+ using var pe = new MemoryStream();
+ updated.Emit(pe);
+ pe.Position = 0;
+
+ var alc = new AssemblyLoadContext("ObserverTest", isCollectible: true);
+ try
+ {
+ var asm = alc.LoadFromStream(pe);
+ var demoType = asm.GetType("PatternKit.Examples.Generators.Demo");
+ var runMethod = demoType!.GetMethod("Run");
+ var result = (string)runMethod!.Invoke(null, null)!;
+
+ // All three handlers should execute (H1, exception, H3)
+ Assert.Equal("H1|H3", result);
+ }
+ finally
+ {
+ alc.Unload();
+ }
+ }
+
+ [Fact]
+ public void Exception_Policy_Stop_Throws_First_Exception()
+ {
+ var user = """
+ using PatternKit.Generators.Observer;
+
+ namespace PatternKit.Examples.Generators;
+
+ public record Temperature(double Celsius);
+
+ [Observer(typeof(Temperature), Exceptions = ObserverExceptionPolicy.Stop)]
+ public partial class TemperatureChanged
+ {
+ }
+
+ public static class Demo
+ {
+ public static string Run()
+ {
+ var evt = new TemperatureChanged();
+
+ evt.Subscribe((Temperature t) => { });
+ evt.Subscribe((Temperature t) => throw new System.Exception("Oops"));
+ evt.Subscribe((Temperature t) => { });
+
+ try
+ {
+ evt.Publish(new Temperature(0));
+ return "No exception";
+ }
+ catch (System.Exception ex)
+ {
+ return ex.Message;
+ }
+ }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(
+ user,
+ assemblyName: nameof(Exception_Policy_Stop_Throws_First_Exception));
+
+ var gen = new Observer.ObserverGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out _, out var updated);
+
+ using var pe = new MemoryStream();
+ updated.Emit(pe);
+ pe.Position = 0;
+
+ var alc = new AssemblyLoadContext("ObserverTest", isCollectible: true);
+ try
+ {
+ var asm = alc.LoadFromStream(pe);
+ var demoType = asm.GetType("PatternKit.Examples.Generators.Demo");
+ var runMethod = demoType!.GetMethod("Run");
+ var result = (string)runMethod!.Invoke(null, null)!;
+ Assert.Equal("Oops", result);
+ }
+ finally
+ {
+ alc.Unload();
+ }
+ }
+
+ [Fact]
+ public void Exception_Policy_Aggregate_Throws_AggregateException()
+ {
+ var user = """
+ using PatternKit.Generators.Observer;
+
+ namespace PatternKit.Examples.Generators;
+
+ public record Temperature(double Celsius);
+
+ [Observer(typeof(Temperature), Exceptions = ObserverExceptionPolicy.Aggregate)]
+ public partial class TemperatureChanged
+ {
+ }
+
+ public static class Demo
+ {
+ public static string Run()
+ {
+ var evt = new TemperatureChanged();
+
+ evt.Subscribe((Temperature t) => throw new System.Exception("Error1"));
+ evt.Subscribe((Temperature t) => throw new System.Exception("Error2"));
+
+ try
+ {
+ evt.Publish(new Temperature(0));
+ return "No exception";
+ }
+ catch (System.AggregateException ex)
+ {
+ return $"{ex.InnerExceptions.Count}:{ex.InnerExceptions[0].Message}:{ex.InnerExceptions[1].Message}";
+ }
+ }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(
+ user,
+ assemblyName: nameof(Exception_Policy_Aggregate_Throws_AggregateException));
+
+ var gen = new Observer.ObserverGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out _, out var updated);
+
+ using var pe = new MemoryStream();
+ updated.Emit(pe);
+ pe.Position = 0;
+
+ var alc = new AssemblyLoadContext("ObserverTest", isCollectible: true);
+ try
+ {
+ var asm = alc.LoadFromStream(pe);
+ var demoType = asm.GetType("PatternKit.Examples.Generators.Demo");
+ var runMethod = demoType!.GetMethod("Run");
+ var result = (string)runMethod!.Invoke(null, null)!;
+ Assert.Equal("2:Error1:Error2", result);
+ }
+ finally
+ {
+ alc.Unload();
+ }
+ }
+
+ [Fact]
+ public void Supports_Struct_Type()
+ {
+ var user = """
+ using PatternKit.Generators.Observer;
+
+ namespace PatternKit.Examples.Generators;
+
+ public record Temperature(double Celsius);
+
+ [Observer(typeof(Temperature))]
+ public partial struct TemperatureChanged
+ {
+ }
+
+ public static class Demo
+ {
+ public static string Run()
+ {
+ var log = new System.Collections.Generic.List();
+ var evt = new TemperatureChanged();
+
+ evt.Subscribe((Temperature t) => log.Add($"T:{t.Celsius}"));
+ evt.Publish(new Temperature(99));
+
+ return string.Join("|", log);
+ }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(
+ user,
+ assemblyName: nameof(Supports_Struct_Type));
+
+ var gen = new Observer.ObserverGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out _, out var updated);
+
+ using var pe = new MemoryStream();
+ updated.Emit(pe);
+ pe.Position = 0;
+
+ var alc = new AssemblyLoadContext("ObserverTest", isCollectible: true);
+ try
+ {
+ var asm = alc.LoadFromStream(pe);
+ var demoType = asm.GetType("PatternKit.Examples.Generators.Demo");
+ var runMethod = demoType!.GetMethod("Run");
+ var result = (string)runMethod!.Invoke(null, null)!;
+ Assert.Equal("T:99", result);
+ }
+ finally
+ {
+ alc.Unload();
+ }
+ }
+
+ [Fact]
+ public void Supports_Record_Class()
+ {
+ var user = """
+ using PatternKit.Generators.Observer;
+
+ namespace PatternKit.Examples.Generators;
+
+ public record Temperature(double Celsius);
+
+ [Observer(typeof(Temperature))]
+ public partial record class TemperatureChanged
+ {
+ }
+
+ public static class Demo
+ {
+ public static string Run()
+ {
+ var log = new System.Collections.Generic.List();
+ var evt = new TemperatureChanged();
+
+ evt.Subscribe((Temperature t) => log.Add("OK"));
+ evt.Publish(new Temperature(0));
+
+ return string.Join("", log);
+ }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(
+ user,
+ assemblyName: nameof(Supports_Record_Class));
+
+ var gen = new Observer.ObserverGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out _, out var updated);
+
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void Supports_Record_Struct()
+ {
+ var user = """
+ using PatternKit.Generators.Observer;
+
+ namespace PatternKit.Examples.Generators;
+
+ public record Temperature(double Celsius);
+
+ [Observer(typeof(Temperature))]
+ public partial record struct TemperatureChanged
+ {
+ }
+
+ public static class Demo
+ {
+ public static string Run()
+ {
+ var log = new System.Collections.Generic.List();
+ var evt = new TemperatureChanged();
+
+ evt.Subscribe((Temperature t) => log.Add("OK"));
+ evt.Publish(new Temperature(0));
+
+ return string.Join("", log);
+ }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(
+ user,
+ assemblyName: nameof(Supports_Record_Struct));
+
+ var gen = new Observer.ObserverGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out _, out var updated);
+
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+}
From 9102caa60077b69460c7fb53a8a45011aeb4d85b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 19 Feb 2026 04:53:50 +0000
Subject: [PATCH 08/13] Add comprehensive Observer pattern documentation and
examples
- 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>
---
docs/generators/observer.md | 818 ++++++++++++++++++
docs/generators/toc.yml | 3 +
.../NotificationSystem.cs | 375 ++++++++
.../ObserverGeneratorDemo/README.md | 288 ++++++
.../TemperatureMonitor.cs | 244 ++++++
5 files changed, 1728 insertions(+)
create mode 100644 docs/generators/observer.md
create mode 100644 src/PatternKit.Examples/ObserverGeneratorDemo/NotificationSystem.cs
create mode 100644 src/PatternKit.Examples/ObserverGeneratorDemo/README.md
create mode 100644 src/PatternKit.Examples/ObserverGeneratorDemo/TemperatureMonitor.cs
diff --git a/docs/generators/observer.md b/docs/generators/observer.md
new file mode 100644
index 0000000..f9d702e
--- /dev/null
+++ b/docs/generators/observer.md
@@ -0,0 +1,818 @@
+# Observer Pattern Generator
+
+## Overview
+
+The **Observer Generator** creates type-safe, high-performance Observer pattern implementations with configurable threading, exception handling, and ordering semantics. It eliminates the need to manually write subscription management code, providing compile-time safety and optimal runtime performance.
+
+## When to Use
+
+Use the Observer generator when you need:
+
+- **Event notification systems**: Publish events to multiple subscribers
+- **Reactive programming**: Build observable data streams and change notifications
+- **Decoupled communication**: Publishers don't need to know about subscribers
+- **Type-safe event handling**: Compile-time verification of handler signatures
+- **Configurable behavior**: Control threading, exceptions, and ordering
+
+## Installation
+
+The generator is included in the `PatternKit.Generators` package:
+
+```bash
+dotnet add package PatternKit.Generators
+```
+
+## Quick Start
+
+```csharp
+using PatternKit.Generators.Observer;
+
+public record Temperature(double Celsius);
+
+[Observer(typeof(Temperature))]
+public partial class TemperatureChanged
+{
+}
+```
+
+Generated methods:
+
+```csharp
+public partial class TemperatureChanged
+{
+ // Subscribe with sync handler
+ public IDisposable Subscribe(Action handler) { ... }
+
+ // Subscribe with async handler
+ public IDisposable Subscribe(Func handler) { ... }
+
+ // Publish to all subscribers
+ public void Publish(Temperature payload) { ... }
+
+ // Publish asynchronously
+ public ValueTask PublishAsync(Temperature payload, CancellationToken cancellationToken = default) { ... }
+}
+```
+
+Usage:
+
+```csharp
+var tempEvent = new TemperatureChanged();
+
+// Subscribe to events
+var subscription = tempEvent.Subscribe(temp =>
+ Console.WriteLine($"Temperature: {temp.Celsius}°C"));
+
+// Publish events
+tempEvent.Publish(new Temperature(23.5));
+tempEvent.Publish(new Temperature(24.0));
+
+// Unsubscribe
+subscription.Dispose();
+```
+
+## Basic Usage
+
+### Synchronous Handlers
+
+```csharp
+public record StockPrice(string Symbol, decimal Price);
+
+[Observer(typeof(StockPrice))]
+public partial class StockPriceChanged
+{
+}
+
+// Usage
+var priceEvent = new StockPriceChanged();
+
+priceEvent.Subscribe(price =>
+ Console.WriteLine($"{price.Symbol}: ${price.Price}"));
+
+priceEvent.Subscribe(price =>
+ LogToDatabase(price));
+
+priceEvent.Publish(new StockPrice("MSFT", 420.50m));
+```
+
+### Asynchronous Handlers
+
+```csharp
+public record UserRegistration(string Email, DateTime Timestamp);
+
+[Observer(typeof(UserRegistration))]
+public partial class UserRegistered
+{
+}
+
+// Usage
+var userEvent = new UserRegistered();
+
+userEvent.Subscribe(async user =>
+{
+ await SendWelcomeEmailAsync(user.Email);
+ await CreateUserProfileAsync(user);
+});
+
+await userEvent.PublishAsync(
+ new UserRegistration("user@example.com", DateTime.UtcNow));
+```
+
+### Managing Subscriptions
+
+Subscriptions return `IDisposable` for cleanup:
+
+```csharp
+var subscription1 = tempEvent.Subscribe(t => Console.WriteLine(t.Celsius));
+var subscription2 = tempEvent.Subscribe(t => LogTemperature(t));
+
+// Unsubscribe individual handlers
+subscription1.Dispose();
+
+// Using 'using' for automatic cleanup
+using (var sub = tempEvent.Subscribe(t => ProcessTemperature(t)))
+{
+ tempEvent.Publish(new Temperature(25.0));
+} // Automatically unsubscribed
+```
+
+## Configuration Options
+
+### Threading Policies
+
+Control how Subscribe/Publish operations handle concurrency:
+
+#### Locking (Default)
+
+Uses locking for thread safety. Recommended for most scenarios:
+
+```csharp
+[Observer(typeof(Temperature), Threading = ObserverThreadingPolicy.Locking)]
+public partial class TemperatureChanged { }
+```
+
+**Characteristics:**
+- Thread-safe Subscribe/Unsubscribe/Publish
+- Snapshots subscriber list under lock for predictable iteration
+- Moderate overhead for lock acquisition
+
+**Use when:**
+- Multiple threads may publish or subscribe concurrently
+- You need deterministic ordering
+- Default choice for most applications
+
+#### SingleThreadedFast
+
+No thread safety, maximum performance:
+
+```csharp
+[Observer(typeof(UiEvent), Threading = ObserverThreadingPolicy.SingleThreadedFast)]
+public partial class UiEventOccurred { }
+```
+
+**Characteristics:**
+- No synchronization overhead
+- Not thread-safe
+- Lowest memory footprint
+
+**Use when:**
+- All operations occur on a single thread (e.g., UI thread)
+- Performance is critical
+- You can guarantee no concurrent access
+
+⚠️ **Warning:** Using this policy with concurrent access will cause data corruption and race conditions.
+
+#### Concurrent
+
+Lock-free atomic operations for high concurrency:
+
+```csharp
+[Observer(typeof(MetricUpdate), Threading = ObserverThreadingPolicy.Concurrent)]
+public partial class MetricUpdated { }
+```
+
+**Characteristics:**
+- Lock-free concurrent operations
+- Thread-safe with better performance under high concurrency
+- May have undefined ordering unless RegistrationOrder is used
+
+**Use when:**
+- High-throughput scenarios with many concurrent publishers
+- Minimizing lock contention is important
+- Can tolerate potential ordering variations
+
+### Exception Policies
+
+Control how exceptions from handlers are managed:
+
+#### Continue (Default)
+
+Continue invoking all handlers even if some throw:
+
+```csharp
+[Observer(typeof(Message), Exceptions = ObserverExceptionPolicy.Continue)]
+public partial class MessageReceived
+{
+ // Optional: handle errors from subscribers
+ partial void OnSubscriberError(Exception ex)
+ {
+ Logger.LogError(ex, "Subscriber failed");
+ }
+}
+```
+
+**Characteristics:**
+- All handlers get invoked
+- Exceptions are caught and optionally logged
+- Publishing never throws
+
+**Use when:**
+- Subscriber failures shouldn't affect other subscribers
+- You want best-effort delivery
+- Fault tolerance is important
+
+**Optional Hook:** Implement `partial void OnSubscriberError(Exception ex)` to log or handle errors.
+
+#### Stop
+
+Stop at first exception and rethrow:
+
+```csharp
+[Observer(typeof(CriticalCommand), Exceptions = ObserverExceptionPolicy.Stop)]
+public partial class CommandExecuted { }
+```
+
+**Characteristics:**
+- First exception stops publishing
+- Exception is rethrown to caller
+- Remaining handlers are not invoked
+
+**Use when:**
+- Any handler failure should abort the operation
+- You need to handle errors at the call site
+- Order matters and failures are critical
+
+#### Aggregate
+
+Collect all exceptions and throw AggregateException:
+
+```csharp
+[Observer(typeof(ValidationRequest), Exceptions = ObserverExceptionPolicy.Aggregate)]
+public partial class ValidationRequested { }
+```
+
+**Characteristics:**
+- All handlers are invoked
+- Exceptions are collected
+- AggregateException thrown if any failed
+
+**Use when:**
+- You need to know about all failures
+- All handlers should run regardless of failures
+- Collecting multiple validation errors
+
+```csharp
+try
+{
+ validationEvent.Publish(request);
+}
+catch (AggregateException aex)
+{
+ foreach (var ex in aex.InnerExceptions)
+ {
+ Console.WriteLine($"Validation error: {ex.Message}");
+ }
+}
+```
+
+### Order Policies
+
+Control handler invocation order:
+
+#### RegistrationOrder (Default)
+
+Handlers invoked in subscription order (FIFO):
+
+```csharp
+[Observer(typeof(Event), Order = ObserverOrderPolicy.RegistrationOrder)]
+public partial class EventOccurred { }
+```
+
+**Characteristics:**
+- Deterministic, predictable order
+- Handlers invoked in the order they were subscribed
+- Slightly higher memory overhead
+
+**Use when:**
+- Order matters (e.g., validation → processing → logging)
+- Debugging requires predictable behavior
+- Default choice for most scenarios
+
+#### Undefined
+
+No order guarantee, potential performance benefit:
+
+```csharp
+[Observer(typeof(Metric), Order = ObserverOrderPolicy.Undefined)]
+public partial class MetricRecorded { }
+```
+
+**Characteristics:**
+- No ordering guarantee
+- May provide better performance with Concurrent threading
+- Lower memory overhead
+
+**Use when:**
+- Order doesn't matter (e.g., independent metrics collection)
+- Maximum performance is needed
+- Handlers are truly independent
+
+### Async Configuration
+
+Control async method generation:
+
+```csharp
+// Generate async methods (default)
+[Observer(typeof(Data), GenerateAsync = true)]
+public partial class DataAvailable { }
+
+// Don't generate async methods
+[Observer(typeof(Data), GenerateAsync = false)]
+public partial class DataAvailable { }
+
+// Force async-only (no sync Subscribe)
+[Observer(typeof(Data), ForceAsync = true)]
+public partial class DataAvailable { }
+```
+
+## Supported Types
+
+The generator supports:
+
+| Type | Supported | Example |
+|------|-----------|---------|
+| `partial class` | ✅ | `public partial class Event { }` |
+| `partial struct` | ✅ | `public partial struct Event { }` |
+| `partial record class` | ✅ | `public partial record class Event;` |
+| `partial record struct` | ✅ | `public partial record struct Event;` |
+| Non-partial types | ❌ | Generates PKOBS001 error |
+
+## API Reference
+
+### Subscribe Methods
+
+#### Synchronous Handler
+
+```csharp
+public IDisposable Subscribe(Action handler)
+```
+
+Subscribes a synchronous handler to the event.
+
+**Parameters:**
+- `handler`: Action to invoke when events are published
+
+**Returns:** `IDisposable` that removes the subscription when disposed
+
+**Example:**
+```csharp
+var sub = observable.Subscribe(payload =>
+ Console.WriteLine(payload));
+sub.Dispose(); // Unsubscribe
+```
+
+#### Asynchronous Handler
+
+```csharp
+public IDisposable Subscribe(Func handler)
+```
+
+Subscribes an asynchronous handler to the event.
+
+**Parameters:**
+- `handler`: Async function to invoke when events are published
+
+**Returns:** `IDisposable` that removes the subscription when disposed
+
+**Example:**
+```csharp
+var sub = observable.Subscribe(async payload =>
+ await ProcessAsync(payload));
+```
+
+### Publish Methods
+
+#### Synchronous Publish
+
+```csharp
+public void Publish(TPayload payload)
+```
+
+Publishes an event to all subscribers synchronously.
+
+**Parameters:**
+- `payload`: The event data to publish
+
+**Behavior:**
+- Invokes synchronous handlers directly
+- Invokes async handlers synchronously (fire-and-forget)
+- Exception handling per configured policy
+
+**Example:**
+```csharp
+observable.Publish(new Temperature(25.0));
+```
+
+#### Asynchronous Publish
+
+```csharp
+public ValueTask PublishAsync(TPayload payload, CancellationToken cancellationToken = default)
+```
+
+Publishes an event to all subscribers asynchronously.
+
+**Parameters:**
+- `payload`: The event data to publish
+- `cancellationToken`: Optional cancellation token
+
+**Returns:** `ValueTask` that completes when all async handlers finish
+
+**Behavior:**
+- Waits for async handlers to complete
+- Synchronous handlers run on calling thread
+- Exception handling per configured policy
+- Honors cancellation token
+
+**Example:**
+```csharp
+await observable.PublishAsync(
+ new UserAction("click"),
+ cancellationToken);
+```
+
+### Optional Hooks
+
+#### OnSubscriberError
+
+```csharp
+partial void OnSubscriberError(Exception ex);
+```
+
+Optional method for handling subscriber exceptions when using `Exceptions = ObserverExceptionPolicy.Continue`.
+
+**Parameters:**
+- `ex`: The exception thrown by a subscriber
+
+**Example:**
+```csharp
+[Observer(typeof(Event), Exceptions = ObserverExceptionPolicy.Continue)]
+public partial class EventOccurred
+{
+ partial void OnSubscriberError(Exception ex)
+ {
+ Logger.LogError(ex, "Subscriber threw exception");
+ Telemetry.RecordError(ex);
+ }
+}
+```
+
+## Performance Considerations
+
+### Memory and Allocations
+
+- **SingleThreadedFast**: Uses `List`, minimal allocations
+- **Locking**: Uses `List` with lock, snapshots on publish
+- **Concurrent**: Uses `ImmutableList` (RegistrationOrder) or `ConcurrentBag` (Undefined)
+
+### Thread Safety Overhead
+
+| Policy | Subscribe/Unsubscribe | Publish | Notes |
+|--------|----------------------|---------|-------|
+| SingleThreadedFast | None | None | Fastest, not thread-safe |
+| Locking | Lock acquisition | Snapshot + lock | Good for moderate concurrency |
+| Concurrent | Atomic operations | Lock-free | Best for high concurrency |
+
+### Async Performance
+
+- `PublishAsync` uses `ValueTask` to reduce allocations
+- Synchronous handlers in `PublishAsync` don't allocate
+- Async handlers only allocate if they don't complete synchronously
+
+### Best Practices
+
+1. **Use Locking by default** unless you have specific needs
+2. **Profile before optimizing** - start with defaults
+3. **Dispose subscriptions** to prevent memory leaks
+4. **Use SingleThreadedFast** only when guaranteed single-threaded
+5. **Prefer Continue exception policy** for fault tolerance
+6. **Use weak references** if subscribers have long lifetimes and publishers are short-lived (implement manually)
+
+## Common Patterns
+
+### Observable Properties
+
+```csharp
+public record PropertyChanged(string PropertyName, object? NewValue);
+
+[Observer(typeof(PropertyChanged))]
+public partial class PropertyChangeNotifier
+{
+}
+
+public class ViewModel
+{
+ private readonly PropertyChangeNotifier _notifier = new();
+ private string _name = "";
+
+ public IDisposable SubscribeToChanges(Action handler) =>
+ _notifier.Subscribe(handler);
+
+ public string Name
+ {
+ get => _name;
+ set
+ {
+ if (_name != value)
+ {
+ _name = value;
+ _notifier.Publish(new PropertyChanged(nameof(Name), value));
+ }
+ }
+ }
+}
+```
+
+### Event Aggregator
+
+```csharp
+public partial class EventAggregator
+{
+ [Observer(typeof(UserLoggedIn))]
+ private partial class UserLoggedInEvent { }
+
+ [Observer(typeof(OrderPlaced))]
+ private partial class OrderPlacedEvent { }
+
+ private readonly UserLoggedInEvent _userLoggedIn = new();
+ private readonly OrderPlacedEvent _orderPlaced = new();
+
+ public IDisposable Subscribe(Action handler)
+ {
+ return typeof(T).Name switch
+ {
+ nameof(UserLoggedIn) => _userLoggedIn.Subscribe(e => handler((T)(object)e)),
+ nameof(OrderPlaced) => _orderPlaced.Subscribe(e => handler((T)(object)e)),
+ _ => throw new NotSupportedException()
+ };
+ }
+
+ public void Publish(T @event)
+ {
+ switch (@event)
+ {
+ case UserLoggedIn e: _userLoggedIn.Publish(e); break;
+ case OrderPlaced e: _orderPlaced.Publish(e); break;
+ }
+ }
+}
+```
+
+### Composite Subscriptions
+
+```csharp
+public class CompositeDisposable : IDisposable
+{
+ private readonly List _subscriptions = new();
+
+ public void Add(IDisposable subscription) => _subscriptions.Add(subscription);
+
+ public void Dispose()
+ {
+ foreach (var sub in _subscriptions)
+ sub.Dispose();
+ _subscriptions.Clear();
+ }
+}
+
+// Usage
+var subscriptions = new CompositeDisposable();
+subscriptions.Add(tempEvent.Subscribe(HandleTemperature));
+subscriptions.Add(pressureEvent.Subscribe(HandlePressure));
+subscriptions.Add(humidityEvent.Subscribe(HandleHumidity));
+
+// Unsubscribe all at once
+subscriptions.Dispose();
+```
+
+## Diagnostics
+
+| ID | Severity | Description |
+|----|----------|-------------|
+| **PKOBS001** | Error | Type marked with `[Observer]` must be declared as `partial` |
+| **PKOBS002** | Error | Unable to extract payload type from `[Observer]` attribute |
+| **PKOBS003** | Error | Invalid configuration (conflicting settings) |
+
+### PKOBS001: Type must be partial
+
+**Cause:** Missing `partial` keyword on observer type.
+
+**Fix:**
+```csharp
+// ❌ Wrong
+[Observer(typeof(Message))]
+public class MessageReceived { }
+
+// ✅ Correct
+[Observer(typeof(Message))]
+public partial class MessageReceived { }
+```
+
+### PKOBS002: Missing payload type
+
+**Cause:** Payload type could not be determined from attribute.
+
+**Fix:** Ensure you provide a valid type to the attribute:
+```csharp
+// ✅ Correct
+[Observer(typeof(MyEventData))]
+public partial class MyEvent { }
+```
+
+## Best Practices
+
+### 1. Always Dispose Subscriptions
+
+Prevent memory leaks by disposing subscriptions:
+
+```csharp
+// ✅ Good: Using statement
+using var subscription = observable.Subscribe(HandleEvent);
+
+// ✅ Good: Explicit disposal
+var subscription = observable.Subscribe(HandleEvent);
+// ... later ...
+subscription.Dispose();
+
+// ⚠️ Bad: Never disposed - memory leak!
+observable.Subscribe(HandleEvent);
+```
+
+### 2. Use Immutable Payload Types
+
+Records make excellent event payloads:
+
+```csharp
+// ✅ Good: Immutable record
+public record OrderPlaced(int OrderId, decimal Amount, DateTime Timestamp);
+
+[Observer(typeof(OrderPlaced))]
+public partial class OrderPlacedEvent { }
+
+// ⚠️ Avoid: Mutable payload
+public class OrderPlaced
+{
+ public int OrderId { get; set; } // Can be modified by handlers
+}
+```
+
+### 3. Keep Handlers Fast
+
+Long-running handlers block other subscribers:
+
+```csharp
+// ⚠️ Bad: Slow handler blocks others
+observable.Subscribe(data =>
+{
+ Thread.Sleep(1000); // Blocks!
+ ProcessData(data);
+});
+
+// ✅ Good: Offload work
+observable.Subscribe(data =>
+ Task.Run(() => ProcessData(data)));
+
+// ✅ Better: Use async
+observable.Subscribe(async data =>
+ await ProcessDataAsync(data));
+```
+
+### 4. Choose the Right Threading Policy
+
+```csharp
+// ✅ UI thread events
+[Observer(typeof(UiEvent), Threading = ObserverThreadingPolicy.SingleThreadedFast)]
+
+// ✅ General application events
+[Observer(typeof(AppEvent), Threading = ObserverThreadingPolicy.Locking)]
+
+// ✅ High-throughput metrics
+[Observer(typeof(Metric), Threading = ObserverThreadingPolicy.Concurrent)]
+```
+
+### 5. Handle Exceptions Appropriately
+
+```csharp
+// ✅ Good: Fault tolerant
+[Observer(typeof(Notification), Exceptions = ObserverExceptionPolicy.Continue)]
+public partial class NotificationSent
+{
+ partial void OnSubscriberError(Exception ex)
+ {
+ Logger.LogWarning(ex, "Notification handler failed");
+ }
+}
+
+// ✅ Good: Critical operations
+[Observer(typeof(Payment), Exceptions = ObserverExceptionPolicy.Stop)]
+public partial class PaymentProcessed { }
+```
+
+### 6. Use Meaningful Event Names
+
+```csharp
+// ✅ Good: Clear, action-based names
+[Observer(typeof(User))]
+public partial class UserRegistered { }
+
+[Observer(typeof(Order))]
+public partial class OrderShipped { }
+
+// ⚠️ Unclear
+[Observer(typeof(User))]
+public partial class UserEvent { } // What happened to the user?
+```
+
+## Examples
+
+See the [ObserverGeneratorDemo](/src/PatternKit.Examples/ObserverGeneratorDemo/) for complete, runnable examples including:
+
+- **TemperatureMonitor.cs**: Basic observer usage with temperature sensors
+- **NotificationSystem.cs**: Async handlers and exception handling
+- **README.md**: Example explanations and usage
+
+## Troubleshooting
+
+### Handlers not being called
+
+**Possible causes:**
+1. Subscription was disposed
+2. Wrong payload type
+3. Exception thrown and swallowed (check `OnSubscriberError`)
+
+**Debug steps:**
+```csharp
+var sub = observable.Subscribe(payload =>
+{
+ Console.WriteLine("Handler called!"); // Add logging
+});
+observable.Publish(payload);
+```
+
+### Memory leaks
+
+**Cause:** Subscriptions not disposed.
+
+**Fix:** Always dispose subscriptions, especially in long-lived objects:
+```csharp
+public class Service : IDisposable
+{
+ private readonly CompositeDisposable _subscriptions = new();
+
+ public Service(SomeObservable observable)
+ {
+ _subscriptions.Add(observable.Subscribe(HandleEvent));
+ }
+
+ public void Dispose() => _subscriptions.Dispose();
+}
+```
+
+### Race conditions with SingleThreadedFast
+
+**Cause:** Using SingleThreadedFast with multiple threads.
+
+**Fix:** Use `Locking` or `Concurrent` policy:
+```csharp
+[Observer(typeof(Data), Threading = ObserverThreadingPolicy.Locking)]
+public partial class DataReceived { }
+```
+
+### Async handlers not awaited in Publish
+
+**Behavior:** `Publish` calls async handlers in fire-and-forget mode.
+
+**Solution:** Use `PublishAsync` to await async handlers:
+```csharp
+// ⚠️ Async handlers not awaited
+observable.Publish(data);
+
+// ✅ Async handlers are awaited
+await observable.PublishAsync(data);
+```
+
+## See Also
+
+- [Memento Generator](memento.md) — For saving/restoring observable state
+- [State Machine Generator](state-machine.md) — For state-based event handling
+- [Observer Pattern (Classic)](https://en.wikipedia.org/wiki/Observer_pattern)
+- [Reactive Extensions](https://reactivex.io/) — Advanced reactive programming
diff --git a/docs/generators/toc.yml b/docs/generators/toc.yml
index ed0dbad..16d8555 100644
--- a/docs/generators/toc.yml
+++ b/docs/generators/toc.yml
@@ -28,6 +28,9 @@
- name: Visitor Generator
href: visitor-generator.md
+- name: Observer
+ href: observer.md
+
- name: Examples
href: examples.md
diff --git a/src/PatternKit.Examples/ObserverGeneratorDemo/NotificationSystem.cs b/src/PatternKit.Examples/ObserverGeneratorDemo/NotificationSystem.cs
new file mode 100644
index 0000000..43b4cf8
--- /dev/null
+++ b/src/PatternKit.Examples/ObserverGeneratorDemo/NotificationSystem.cs
@@ -0,0 +1,375 @@
+using PatternKit.Generators.Observer;
+
+namespace PatternKit.Examples.ObserverGeneratorDemo;
+
+///
+/// A notification message to be sent.
+///
+/// ID of the recipient.
+/// The notification message.
+/// Priority level (0=low, 1=normal, 2=high).
+public record Notification(string RecipientId, string Message, int Priority);
+
+///
+/// Result of attempting to send a notification.
+///
+/// Whether the send was successful.
+/// Which channel was used (Email, SMS, Push).
+/// Error message if failed.
+public record NotificationResult(bool Success, string Channel, string? Error = null);
+
+///
+/// Observable event for notifications with async support.
+/// Demonstrates async handlers and PublishAsync.
+///
+[Observer(typeof(Notification),
+ Threading = ObserverThreadingPolicy.Locking,
+ Exceptions = ObserverExceptionPolicy.Continue,
+ GenerateAsync = true)]
+public partial class NotificationPublished
+{
+ partial void OnSubscriberError(Exception ex)
+ {
+ Console.WriteLine($"❌ Notification handler error: {ex.Message}");
+ }
+}
+
+///
+/// Observable event for notification results.
+/// Uses Aggregate exception policy to collect all failures.
+///
+[Observer(typeof(NotificationResult),
+ Threading = ObserverThreadingPolicy.Locking,
+ Exceptions = ObserverExceptionPolicy.Aggregate)]
+public partial class NotificationSent
+{
+}
+
+///
+/// Multi-channel notification system with async handlers.
+///
+public class NotificationSystem
+{
+ private readonly NotificationPublished _notificationPublished = new();
+ private readonly NotificationSent _notificationSent = new();
+ private readonly Random _random = new();
+
+ ///
+ /// Subscribes to notifications with a synchronous handler.
+ ///
+ public IDisposable Subscribe(Action handler) =>
+ _notificationPublished.Subscribe(handler);
+
+ ///
+ /// Subscribes to notifications with an async handler.
+ ///
+ public IDisposable SubscribeAsync(Func handler) =>
+ _notificationPublished.Subscribe(handler);
+
+ ///
+ /// Subscribes to notification send results.
+ ///
+ public IDisposable OnNotificationSent(Action handler) =>
+ _notificationSent.Subscribe(handler);
+
+ ///
+ /// Sends a notification through all registered channels asynchronously.
+ ///
+ public async Task SendAsync(Notification notification, CancellationToken cancellationToken = default)
+ {
+ Console.WriteLine($"\n📤 Sending notification (Priority: {notification.Priority})...");
+ await _notificationPublished.PublishAsync(notification, cancellationToken);
+ }
+
+ ///
+ /// Reports that a notification was sent through a channel.
+ ///
+ public void ReportSent(NotificationResult result)
+ {
+ _notificationSent.Publish(result);
+ }
+
+ ///
+ /// Simulates sending an email (async operation).
+ ///
+ public async Task SendEmailAsync(Notification notification)
+ {
+ await Task.Delay(100); // Simulate network delay
+
+ // Simulate random failures (20% chance)
+ if (_random.NextDouble() < 0.2)
+ {
+ return new NotificationResult(false, "Email", "SMTP server unavailable");
+ }
+
+ Console.WriteLine($" ✉️ Email sent to {notification.RecipientId}");
+ return new NotificationResult(true, "Email");
+ }
+
+ ///
+ /// Simulates sending an SMS (async operation).
+ ///
+ public async Task SendSmsAsync(Notification notification)
+ {
+ await Task.Delay(80); // Simulate network delay
+
+ // High priority only
+ if (notification.Priority < 2)
+ {
+ return new NotificationResult(false, "SMS", "Priority too low for SMS");
+ }
+
+ Console.WriteLine($" 📱 SMS sent to {notification.RecipientId}");
+ return new NotificationResult(true, "SMS");
+ }
+
+ ///
+ /// Simulates sending a push notification (async operation).
+ ///
+ public async Task SendPushAsync(Notification notification)
+ {
+ await Task.Delay(50); // Simulate network delay
+ Console.WriteLine($" 🔔 Push notification sent to {notification.RecipientId}");
+ return new NotificationResult(true, "Push");
+ }
+}
+
+///
+/// Demonstrates async handlers with PublishAsync.
+///
+public static class AsyncNotificationDemo
+{
+ public static async Task RunAsync()
+ {
+ Console.WriteLine("=== Async Notification System ===\n");
+
+ var system = new NotificationSystem();
+
+ // Subscribe email channel (async handler)
+ using var emailSub = system.SubscribeAsync(async notification =>
+ {
+ var result = await system.SendEmailAsync(notification);
+ system.ReportSent(result);
+ });
+
+ // Subscribe SMS channel (async handler)
+ using var smsSub = system.SubscribeAsync(async notification =>
+ {
+ var result = await system.SendSmsAsync(notification);
+ system.ReportSent(result);
+ });
+
+ // Subscribe push channel (async handler)
+ using var pushSub = system.SubscribeAsync(async notification =>
+ {
+ var result = await system.SendPushAsync(notification);
+ system.ReportSent(result);
+ });
+
+ // Subscribe to results to track success/failure
+ var successCount = 0;
+ var failureCount = 0;
+ using var resultSub = system.OnNotificationSent(result =>
+ {
+ if (result.Success)
+ {
+ successCount++;
+ }
+ else
+ {
+ failureCount++;
+ Console.WriteLine($" ⚠️ {result.Channel} failed: {result.Error}");
+ }
+ });
+
+ // Send notifications with different priorities
+ var notifications = new[]
+ {
+ new Notification("user123", "Welcome to our service!", Priority: 1),
+ new Notification("user456", "Your order has shipped", Priority: 1),
+ new Notification("user789", "URGENT: Security alert", Priority: 2),
+ new Notification("user999", "Daily digest available", Priority: 0)
+ };
+
+ foreach (var notification in notifications)
+ {
+ await system.SendAsync(notification);
+ await Task.Delay(200); // Space out notifications
+ }
+
+ Console.WriteLine($"\n📊 Results: {successCount} successful, {failureCount} failed");
+ }
+}
+
+///
+/// Demonstrates exception handling with different policies.
+///
+public static class ExceptionHandlingDemo
+{
+ public static void Run()
+ {
+ Console.WriteLine("\n=== Exception Handling Demo ===\n");
+
+ // Demo 1: Continue policy (default) - all handlers run despite errors
+ Console.WriteLine("1. Continue Policy (fault-tolerant):");
+ DemoContinuePolicy();
+
+ // Demo 2: Aggregate policy - collect all errors
+ Console.WriteLine("\n2. Aggregate Policy (collect all errors):");
+ DemoAggregatePolicy();
+ }
+
+ private static void DemoContinuePolicy()
+ {
+ var notification = new NotificationPublished();
+
+ // Handler 1: Works fine
+ notification.Subscribe(n =>
+ Console.WriteLine(" ✅ Handler 1: Success"));
+
+ // Handler 2: Throws exception
+ notification.Subscribe(n =>
+ {
+ Console.WriteLine(" ❌ Handler 2: Throwing exception...");
+ throw new InvalidOperationException("Handler 2 failed");
+ });
+
+ // Handler 3: Also works fine
+ notification.Subscribe(n =>
+ Console.WriteLine(" ✅ Handler 3: Success (ran despite Handler 2 error)"));
+
+ notification.Publish(new Notification("test", "Test message", 1));
+ Console.WriteLine(" ℹ️ All handlers attempted, errors logged via OnSubscriberError");
+ }
+
+ private static void DemoAggregatePolicy()
+ {
+ var results = new NotificationSent();
+
+ // Handler 1: Throws
+ results.Subscribe(r =>
+ {
+ Console.WriteLine(" ❌ Validator 1: Failed");
+ throw new InvalidOperationException("Validation 1 failed");
+ });
+
+ // Handler 2: Also throws
+ results.Subscribe(r =>
+ {
+ Console.WriteLine(" ❌ Validator 2: Failed");
+ throw new ArgumentException("Validation 2 failed");
+ });
+
+ // Handler 3: Would succeed
+ results.Subscribe(r =>
+ Console.WriteLine(" ✅ Validator 3: Success"));
+
+ try
+ {
+ results.Publish(new NotificationResult(true, "Test"));
+ Console.WriteLine(" ℹ️ No exception thrown (shouldn't reach here)");
+ }
+ catch (AggregateException ex)
+ {
+ Console.WriteLine($" 🔥 AggregateException caught with {ex.InnerExceptions.Count} errors:");
+ foreach (var inner in ex.InnerExceptions)
+ {
+ Console.WriteLine($" - {inner.GetType().Name}: {inner.Message}");
+ }
+ }
+ }
+}
+
+///
+/// Demonstrates mixing sync and async handlers.
+///
+public static class MixedHandlersDemo
+{
+ public static async Task RunAsync()
+ {
+ Console.WriteLine("\n=== Mixed Sync/Async Handlers Demo ===\n");
+
+ var notification = new NotificationPublished();
+
+ // Sync handler
+ notification.Subscribe(n =>
+ Console.WriteLine($" 🔹 Sync handler: {n.Message}"));
+
+ // Async handler
+ notification.Subscribe(async n =>
+ {
+ await Task.Delay(50);
+ Console.WriteLine($" 🔸 Async handler: {n.Message}");
+ });
+
+ // Another sync handler
+ notification.Subscribe(n =>
+ Console.WriteLine($" 🔹 Sync handler 2: Priority={n.Priority}"));
+
+ Console.WriteLine("Publishing with Publish (sync):");
+ notification.Publish(new Notification("user", "Hello World", 1));
+
+ // Note: async handlers run fire-and-forget with Publish
+ await Task.Delay(100); // Wait for async handlers
+
+ Console.WriteLine("\nPublishing with PublishAsync (awaits async handlers):");
+ await notification.PublishAsync(new Notification("user", "Goodbye World", 2));
+
+ Console.WriteLine("\nNote: PublishAsync waits for all async handlers to complete.");
+ }
+}
+
+///
+/// Demonstrates cancellation token support in async handlers.
+///
+public static class CancellationDemo
+{
+ public static async Task RunAsync()
+ {
+ Console.WriteLine("\n=== Cancellation Demo ===\n");
+
+ var notification = new NotificationPublished();
+ var processedCount = 0;
+
+ // Long-running async handler
+ notification.Subscribe(async n =>
+ {
+ Console.WriteLine(" ⏳ Starting long operation...");
+ try
+ {
+ // This will be cancelled
+ await Task.Delay(5000);
+ processedCount++;
+ Console.WriteLine(" ✅ Long operation completed");
+ }
+ catch (TaskCanceledException)
+ {
+ Console.WriteLine(" 🚫 Long operation cancelled");
+ }
+ });
+
+ // Quick handler
+ notification.Subscribe(async n =>
+ {
+ await Task.Delay(10);
+ processedCount++;
+ Console.WriteLine(" ✅ Quick operation completed");
+ });
+
+ using var cts = new CancellationTokenSource(100); // Cancel after 100ms
+
+ try
+ {
+ await notification.PublishAsync(
+ new Notification("user", "Test", 1),
+ cts.Token);
+ }
+ catch (OperationCanceledException)
+ {
+ Console.WriteLine("\n ℹ️ PublishAsync was cancelled");
+ }
+
+ Console.WriteLine($"\n Handlers completed: {processedCount}/2");
+ Console.WriteLine(" (Quick handler completed, long handler was cancelled)");
+ }
+}
diff --git a/src/PatternKit.Examples/ObserverGeneratorDemo/README.md b/src/PatternKit.Examples/ObserverGeneratorDemo/README.md
new file mode 100644
index 0000000..27e3b2e
--- /dev/null
+++ b/src/PatternKit.Examples/ObserverGeneratorDemo/README.md
@@ -0,0 +1,288 @@
+# Observer Generator Examples
+
+This directory contains comprehensive examples demonstrating the Observer pattern source generator.
+
+## Examples Overview
+
+### 1. TemperatureMonitor.cs
+
+Demonstrates fundamental Observer pattern usage with a temperature monitoring system.
+
+**Key Concepts:**
+- Basic `[Observer(typeof(T))]` attribute usage
+- Synchronous event handling
+- Multiple subscribers to the same event
+- Exception handling with `OnSubscriberError` hook
+- Subscription lifecycle management (Subscribe/Dispose)
+- Default configuration (Locking, Continue, RegistrationOrder)
+
+**Demos Included:**
+- `TemperatureMonitorDemo.Run()` - Complete monitoring system with alerts
+- `MultipleSubscribersDemo.Run()` - Multiple handlers with fault tolerance
+- `SubscriptionLifecycleDemo.Run()` - Subscription management patterns
+
+**Run Example:**
+```csharp
+TemperatureMonitorDemo.Run();
+MultipleSubscribersDemo.Run();
+SubscriptionLifecycleDemo.Run();
+```
+
+### 2. NotificationSystem.cs
+
+Demonstrates advanced Observer features with a multi-channel notification system.
+
+**Key Concepts:**
+- Async event handlers with `Func`
+- `PublishAsync` for awaiting async handlers
+- Exception policies: Continue vs Aggregate
+- Mixing sync and async handlers
+- Cancellation token support
+- Real-world async patterns (email, SMS, push notifications)
+
+**Demos Included:**
+- `AsyncNotificationDemo.RunAsync()` - Multi-channel async notifications
+- `ExceptionHandlingDemo.Run()` - Exception policy comparison
+- `MixedHandlersDemo.RunAsync()` - Sync and async handlers together
+- `CancellationDemo.RunAsync()` - Cancellation token propagation
+
+**Run Example:**
+```csharp
+await AsyncNotificationDemo.RunAsync();
+ExceptionHandlingDemo.Run();
+await MixedHandlersDemo.RunAsync();
+await CancellationDemo.RunAsync();
+```
+
+## Quick Start
+
+### Basic Usage
+
+```csharp
+// Define your event payload
+public record TemperatureReading(string SensorId, double Celsius, DateTime Timestamp);
+
+// Generate Observer implementation
+[Observer(typeof(TemperatureReading))]
+public partial class TemperatureChanged
+{
+}
+
+// Use it
+var tempEvent = new TemperatureChanged();
+
+// Subscribe
+var subscription = tempEvent.Subscribe(reading =>
+{
+ Console.WriteLine($"{reading.SensorId}: {reading.Celsius}°C");
+});
+
+// Publish
+tempEvent.Publish(new TemperatureReading("Sensor-01", 23.5, DateTime.UtcNow));
+
+// Unsubscribe
+subscription.Dispose();
+```
+
+### Async Usage
+
+```csharp
+public record Notification(string Message);
+
+[Observer(typeof(Notification))]
+public partial class NotificationSent
+{
+}
+
+var notif = new NotificationSent();
+
+// Async handler
+notif.Subscribe(async n =>
+{
+ await SendEmailAsync(n.Message);
+ await LogToDbAsync(n);
+});
+
+// Await all async handlers
+await notif.PublishAsync(new Notification("Hello!"));
+```
+
+## Configuration Examples
+
+### Threading Policies
+
+```csharp
+// Default: Thread-safe with locks
+[Observer(typeof(Message), Threading = ObserverThreadingPolicy.Locking)]
+public partial class MessageReceived { }
+
+// Single-threaded: No thread safety, maximum performance
+[Observer(typeof(UiEvent), Threading = ObserverThreadingPolicy.SingleThreadedFast)]
+public partial class UiEventOccurred { }
+
+// Concurrent: Lock-free for high throughput
+[Observer(typeof(Metric), Threading = ObserverThreadingPolicy.Concurrent)]
+public partial class MetricRecorded { }
+```
+
+### Exception Policies
+
+```csharp
+// Continue: Fault-tolerant, all handlers run (default)
+[Observer(typeof(Event), Exceptions = ObserverExceptionPolicy.Continue)]
+public partial class EventOccurred
+{
+ partial void OnSubscriberError(Exception ex)
+ {
+ Logger.LogError(ex, "Handler failed");
+ }
+}
+
+// Stop: Fail-fast, stop on first error
+[Observer(typeof(Payment), Exceptions = ObserverExceptionPolicy.Stop)]
+public partial class PaymentProcessed { }
+
+// Aggregate: Collect all errors, throw AggregateException
+[Observer(typeof(Validation), Exceptions = ObserverExceptionPolicy.Aggregate)]
+public partial class ValidationRequested { }
+```
+
+### Order Policies
+
+```csharp
+// RegistrationOrder: FIFO, deterministic (default)
+[Observer(typeof(Event), Order = ObserverOrderPolicy.RegistrationOrder)]
+public partial class EventRaised { }
+
+// Undefined: No order guarantee, potential performance benefit
+[Observer(typeof(Metric), Order = ObserverOrderPolicy.Undefined)]
+public partial class MetricCollected { }
+```
+
+## Common Patterns
+
+### 1. Observable Property
+
+```csharp
+public record PropertyChanged(string Name, object? Value);
+
+[Observer(typeof(PropertyChanged))]
+public partial class PropertyChangedEvent { }
+
+public class ViewModel
+{
+ private readonly PropertyChangedEvent _propertyChanged = new();
+ private string _name = "";
+
+ public IDisposable OnPropertyChanged(Action handler) =>
+ _propertyChanged.Subscribe(handler);
+
+ public string Name
+ {
+ get => _name;
+ set
+ {
+ if (_name != value)
+ {
+ _name = value;
+ _propertyChanged.Publish(new PropertyChanged(nameof(Name), value));
+ }
+ }
+ }
+}
+```
+
+### 2. Event Aggregator
+
+```csharp
+public class EventBus
+{
+ [Observer(typeof(UserLoggedIn))]
+ private partial class UserLoggedInEvent { }
+
+ [Observer(typeof(OrderPlaced))]
+ private partial class OrderPlacedEvent { }
+
+ private readonly UserLoggedInEvent _userLoggedIn = new();
+ private readonly OrderPlacedEvent _orderPlaced = new();
+
+ public IDisposable OnUserLoggedIn(Action handler) =>
+ _userLoggedIn.Subscribe(handler);
+
+ public IDisposable OnOrderPlaced(Action handler) =>
+ _orderPlaced.Subscribe(handler);
+
+ public void Publish(UserLoggedIn e) => _userLoggedIn.Publish(e);
+ public void Publish(OrderPlaced e) => _orderPlaced.Publish(e);
+}
+```
+
+### 3. Composite Subscriptions
+
+```csharp
+public class CompositeDisposable : IDisposable
+{
+ private readonly List _subscriptions = new();
+
+ public void Add(IDisposable subscription) => _subscriptions.Add(subscription);
+
+ public void Dispose()
+ {
+ foreach (var sub in _subscriptions)
+ sub.Dispose();
+ _subscriptions.Clear();
+ }
+}
+
+// Usage
+var subscriptions = new CompositeDisposable();
+subscriptions.Add(eventA.Subscribe(HandleA));
+subscriptions.Add(eventB.Subscribe(HandleB));
+subscriptions.Add(eventC.Subscribe(HandleC));
+
+// Unsubscribe all at once
+subscriptions.Dispose();
+```
+
+## Running All Examples
+
+To run all examples in sequence:
+
+```csharp
+public static async Task RunAllExamples()
+{
+ // Temperature monitoring examples
+ TemperatureMonitorDemo.Run();
+ MultipleSubscribersDemo.Run();
+ SubscriptionLifecycleDemo.Run();
+
+ // Notification system examples
+ await AsyncNotificationDemo.RunAsync();
+ ExceptionHandlingDemo.Run();
+ await MixedHandlersDemo.RunAsync();
+ await CancellationDemo.RunAsync();
+}
+```
+
+## Key Takeaways
+
+1. **Always dispose subscriptions** - Prevents memory leaks
+2. **Use immutable payload types** - Records work great
+3. **Choose appropriate policies** - Default (Locking + Continue + RegistrationOrder) is good for most cases
+4. **Use PublishAsync for async handlers** - `Publish` fires and forgets; `PublishAsync` awaits
+5. **Handle errors gracefully** - Implement `OnSubscriberError` with Continue policy
+6. **Keep handlers fast** - Offload work to background tasks if needed
+
+## Performance Tips
+
+- **SingleThreadedFast**: Use for UI thread events (20-30% faster than Locking)
+- **Concurrent**: Use for high-throughput metrics (better under contention)
+- **Locking**: Default choice, good balance of safety and performance
+- **Undefined Order**: Slight performance benefit if order doesn't matter
+- **ValueTask**: Async handlers use ValueTask for reduced allocations
+
+## See Also
+
+- [Observer Generator Documentation](/docs/generators/observer.md)
+- [PatternKit.Generators API Reference](https://patternkit.dev/api)
+- [Observer Pattern (Wikipedia)](https://en.wikipedia.org/wiki/Observer_pattern)
diff --git a/src/PatternKit.Examples/ObserverGeneratorDemo/TemperatureMonitor.cs b/src/PatternKit.Examples/ObserverGeneratorDemo/TemperatureMonitor.cs
new file mode 100644
index 0000000..f4a3a1a
--- /dev/null
+++ b/src/PatternKit.Examples/ObserverGeneratorDemo/TemperatureMonitor.cs
@@ -0,0 +1,244 @@
+using PatternKit.Generators.Observer;
+
+namespace PatternKit.Examples.ObserverGeneratorDemo;
+
+///
+/// Temperature reading from a sensor.
+///
+/// Unique identifier of the sensor.
+/// Temperature in Celsius.
+/// When the reading was taken.
+public record TemperatureReading(string SensorId, double Celsius, DateTime Timestamp);
+
+///
+/// Temperature alert when temperature exceeds threshold.
+///
+/// Sensor that triggered the alert.
+/// The temperature that triggered the alert.
+/// The threshold that was exceeded.
+public record TemperatureAlert(string SensorId, double Temperature, double Threshold);
+
+///
+/// Observable event for temperature readings using default configuration.
+/// Threading: Locking (thread-safe)
+/// Exceptions: Continue (fault-tolerant)
+/// Order: RegistrationOrder (FIFO)
+///
+[Observer(typeof(TemperatureReading))]
+public partial class TemperatureChanged
+{
+ // Optional: Log errors from subscribers
+ partial void OnSubscriberError(Exception ex)
+ {
+ Console.WriteLine($"⚠️ Subscriber error: {ex.Message}");
+ }
+}
+
+///
+/// Observable event for temperature alerts with custom configuration.
+/// Uses Stop exception policy to ensure critical alerts aren't missed.
+///
+[Observer(typeof(TemperatureAlert),
+ Threading = ObserverThreadingPolicy.Locking,
+ Exceptions = ObserverExceptionPolicy.Stop,
+ Order = ObserverOrderPolicy.RegistrationOrder)]
+public partial class TemperatureAlertRaised
+{
+}
+
+///
+/// Temperature monitoring system that simulates sensors and raises events.
+///
+public class TemperatureMonitoringSystem
+{
+ private readonly TemperatureChanged _temperatureChanged = new();
+ private readonly TemperatureAlertRaised _alertRaised = new();
+ private readonly Dictionary _thresholds = new();
+ private readonly Random _random = new();
+
+ ///
+ /// Sets the alert threshold for a specific sensor.
+ ///
+ public void SetThreshold(string sensorId, double thresholdCelsius)
+ {
+ _thresholds[sensorId] = thresholdCelsius;
+ }
+
+ ///
+ /// Subscribes to temperature change events.
+ ///
+ public IDisposable OnTemperatureChanged(Action handler) =>
+ _temperatureChanged.Subscribe(handler);
+
+ ///
+ /// Subscribes to temperature alert events.
+ ///
+ public IDisposable OnTemperatureAlert(Action handler) =>
+ _alertRaised.Subscribe(handler);
+
+ ///
+ /// Simulates a sensor reading and publishes events.
+ ///
+ public void SimulateReading(string sensorId)
+ {
+ // Generate random temperature between 15°C and 35°C
+ var temperature = 15 + (_random.NextDouble() * 20);
+ var reading = new TemperatureReading(sensorId, temperature, DateTime.UtcNow);
+
+ // Publish temperature change
+ _temperatureChanged.Publish(reading);
+
+ // Check threshold and raise alert if exceeded
+ if (_thresholds.TryGetValue(sensorId, out var threshold) && temperature > threshold)
+ {
+ var alert = new TemperatureAlert(sensorId, temperature, threshold);
+ _alertRaised.Publish(alert);
+ }
+ }
+}
+
+///
+/// Demonstrates basic Observer pattern usage with temperature monitoring.
+///
+public static class TemperatureMonitorDemo
+{
+ public static void Run()
+ {
+ Console.WriteLine("=== Temperature Monitoring System ===\n");
+
+ var system = new TemperatureMonitoringSystem();
+
+ // Configure thresholds
+ system.SetThreshold("Sensor-01", 28.0);
+ system.SetThreshold("Sensor-02", 25.0);
+
+ // Subscribe to temperature changes
+ using var tempSubscription = system.OnTemperatureChanged(reading =>
+ {
+ Console.WriteLine($"📊 {reading.SensorId}: {reading.Celsius:F1}°C at {reading.Timestamp:HH:mm:ss}");
+ });
+
+ // Subscribe to alerts with critical handler
+ using var alertSubscription = system.OnTemperatureAlert(alert =>
+ {
+ Console.WriteLine($"🔥 ALERT! {alert.SensorId} exceeded {alert.Threshold:F1}°C: {alert.Temperature:F1}°C");
+ });
+
+ // Additional alert handler for logging
+ using var logSubscription = system.OnTemperatureAlert(alert =>
+ {
+ LogToFile($"Temperature alert: {alert.SensorId} - {alert.Temperature:F1}°C");
+ });
+
+ Console.WriteLine("Simulating sensor readings...\n");
+
+ // Simulate readings from multiple sensors
+ for (int i = 0; i < 10; i++)
+ {
+ system.SimulateReading("Sensor-01");
+ system.SimulateReading("Sensor-02");
+ Thread.Sleep(500); // Simulate time between readings
+ }
+
+ Console.WriteLine("\n--- End of simulation ---");
+ }
+
+ private static void LogToFile(string message)
+ {
+ // In real application, write to file
+ Console.WriteLine($" 📝 Logged: {message}");
+ }
+}
+
+///
+/// Demonstrates multiple subscribers with different behaviors.
+///
+public static class MultipleSubscribersDemo
+{
+ public static void Run()
+ {
+ Console.WriteLine("\n=== Multiple Subscribers Demo ===\n");
+
+ var temperatureChanged = new TemperatureChanged();
+
+ // Subscriber 1: Console display
+ var sub1 = temperatureChanged.Subscribe(reading =>
+ Console.WriteLine($"Display: {reading.Celsius:F1}°C"));
+
+ // Subscriber 2: Statistics tracker
+ var temperatures = new List();
+ var sub2 = temperatureChanged.Subscribe(reading =>
+ {
+ temperatures.Add(reading.Celsius);
+ var avg = temperatures.Average();
+ Console.WriteLine($"Stats: Current={reading.Celsius:F1}°C, Avg={avg:F1}°C, Count={temperatures.Count}");
+ });
+
+ // Subscriber 3: Faulty handler (demonstrates exception handling)
+ var sub3 = temperatureChanged.Subscribe(_ =>
+ {
+ throw new InvalidOperationException("Simulated error in subscriber");
+ });
+
+ // Subscriber 4: Continues to work despite sub3's error
+ var sub4 = temperatureChanged.Subscribe(reading =>
+ Console.WriteLine($"Monitor: Temperature is {(reading.Celsius > 25 ? "HOT" : "NORMAL")}"));
+
+ Console.WriteLine("Publishing temperature readings...\n");
+
+ temperatureChanged.Publish(new TemperatureReading("Test", 22.5, DateTime.UtcNow));
+ Thread.Sleep(100);
+ temperatureChanged.Publish(new TemperatureReading("Test", 28.3, DateTime.UtcNow));
+ Thread.Sleep(100);
+ temperatureChanged.Publish(new TemperatureReading("Test", 24.1, DateTime.UtcNow));
+
+ Console.WriteLine("\nNote: Subscriber 3 threw exceptions, but others continued working.");
+ Console.WriteLine("This is because we use ObserverExceptionPolicy.Continue (default).\n");
+
+ // Cleanup
+ sub1.Dispose();
+ sub2.Dispose();
+ sub3.Dispose();
+ sub4.Dispose();
+ }
+}
+
+///
+/// Demonstrates subscription lifecycle management.
+///
+public static class SubscriptionLifecycleDemo
+{
+ public static void Run()
+ {
+ Console.WriteLine("\n=== Subscription Lifecycle Demo ===\n");
+
+ var temperatureChanged = new TemperatureChanged();
+
+ Console.WriteLine("1. Creating three subscriptions...");
+ var sub1 = temperatureChanged.Subscribe(r => Console.WriteLine($" Sub1: {r.Celsius:F1}°C"));
+ var sub2 = temperatureChanged.Subscribe(r => Console.WriteLine($" Sub2: {r.Celsius:F1}°C"));
+ var sub3 = temperatureChanged.Subscribe(r => Console.WriteLine($" Sub3: {r.Celsius:F1}°C"));
+
+ Console.WriteLine("\n2. Publishing with all three active:");
+ temperatureChanged.Publish(new TemperatureReading("Test", 20.0, DateTime.UtcNow));
+
+ Console.WriteLine("\n3. Disposing sub2...");
+ sub2.Dispose();
+
+ Console.WriteLine("\n4. Publishing with sub1 and sub3:");
+ temperatureChanged.Publish(new TemperatureReading("Test", 21.0, DateTime.UtcNow));
+
+ Console.WriteLine("\n5. Using 'using' for automatic disposal:");
+ using (var tempSub = temperatureChanged.Subscribe(r => Console.WriteLine($" Temp: {r.Celsius:F1}°C")))
+ {
+ temperatureChanged.Publish(new TemperatureReading("Test", 22.0, DateTime.UtcNow));
+ } // tempSub automatically disposed here
+
+ Console.WriteLine("\n6. After 'using' block (tempSub disposed):");
+ temperatureChanged.Publish(new TemperatureReading("Test", 23.0, DateTime.UtcNow));
+
+ // Cleanup
+ sub1.Dispose();
+ sub3.Dispose();
+ }
+}
From f10e2ccff63ed57218ac65ae2947ba1cb91417f9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 19 Feb 2026 04:55:07 +0000
Subject: [PATCH 09/13] Fix parameter naming convention in NotificationSystem
example
Use positional parameters instead of named parameters with incorrect casing
in Notification record instantiation.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../ObserverGeneratorDemo/NotificationSystem.cs | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/PatternKit.Examples/ObserverGeneratorDemo/NotificationSystem.cs b/src/PatternKit.Examples/ObserverGeneratorDemo/NotificationSystem.cs
index 43b4cf8..255a2e4 100644
--- a/src/PatternKit.Examples/ObserverGeneratorDemo/NotificationSystem.cs
+++ b/src/PatternKit.Examples/ObserverGeneratorDemo/NotificationSystem.cs
@@ -185,10 +185,10 @@ public static async Task RunAsync()
// Send notifications with different priorities
var notifications = new[]
{
- new Notification("user123", "Welcome to our service!", Priority: 1),
- new Notification("user456", "Your order has shipped", Priority: 1),
- new Notification("user789", "URGENT: Security alert", Priority: 2),
- new Notification("user999", "Daily digest available", Priority: 0)
+ new Notification("user123", "Welcome to our service!", 1),
+ new Notification("user456", "Your order has shipped", 1),
+ new Notification("user789", "URGENT: Security alert", 2),
+ new Notification("user999", "Daily digest available", 0)
};
foreach (var notification in notifications)
From 6b8e6986c01200e1719467549353939a80372205 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 19 Feb 2026 05:31:56 +0000
Subject: [PATCH 10/13] Fix Observer generator: remove reflection, add
nested/generic validation, fire-and-forget async, disable struct support
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
.../Observer/ObserverGenerator.cs | 184 +++++++++++++-----
1 file changed, 135 insertions(+), 49 deletions(-)
diff --git a/src/PatternKit.Generators/Observer/ObserverGenerator.cs b/src/PatternKit.Generators/Observer/ObserverGenerator.cs
index b8c5c4c..fe81357 100644
--- a/src/PatternKit.Generators/Observer/ObserverGenerator.cs
+++ b/src/PatternKit.Generators/Observer/ObserverGenerator.cs
@@ -67,6 +67,36 @@ private static void GenerateObserver(SourceProductionContext context, GeneratorA
return;
}
+ // Check for generic types
+ if (typeSymbol.IsGenericType)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ InvalidConfigRule,
+ syntax.Identifier.GetLocation(),
+ "Generic observer types are not supported"));
+ return;
+ }
+
+ // Check for nested types
+ if (typeSymbol.ContainingType != null)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ InvalidConfigRule,
+ syntax.Identifier.GetLocation(),
+ "Nested observer types are not supported"));
+ return;
+ }
+
+ // Structs have complex lifetime and capture semantics, especially with fire-and-forget async
+ if (typeSymbol.TypeKind == TypeKind.Struct)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ InvalidConfigRule,
+ syntax.Identifier.GetLocation(),
+ "Struct observer types are not currently supported due to capture and boxing complexity"));
+ return;
+ }
+
var attr = occurrence.Attributes.Length > 0 ? occurrence.Attributes[0] : null;
if (attr == null || attr.ConstructorArguments.Length == 0 || attr.ConstructorArguments[0].Value is not INamedTypeSymbol payloadType)
{
@@ -157,12 +187,12 @@ private static string GenerateSource(INamedTypeSymbol typeSymbol, INamedTypeSymb
sb.AppendLine();
}
- var isStruct = typeSymbol.TypeKind == TypeKind.Struct;
+ var isStruct = false; // Structs are now rejected above
sb.AppendLine($"{accessibility} partial {typeKind} {typeName}");
sb.AppendLine("{");
- GenerateFields(sb, config, isStruct);
+ GenerateFields(sb, config);
GenerateSubscribeMethods(sb, payloadTypeName, config);
GeneratePublishMethods(sb, payloadTypeName, config);
GenerateUnsubscribeMethod(sb, config);
@@ -174,47 +204,54 @@ private static string GenerateSource(INamedTypeSymbol typeSymbol, INamedTypeSymb
return sb.ToString();
}
- private static void GenerateFields(StringBuilder sb, ObserverConfig config, bool isStruct)
+ private static void GenerateFields(StringBuilder sb, ObserverConfig config)
{
- // For all types, use nullable fields and ensure initialization in helper methods
+ // Generate a shared state object to avoid any issues with subscriptions
+ sb.AppendLine(" private sealed class ObserverState");
+ sb.AppendLine(" {");
+
switch (config.Threading)
{
case 0: // SingleThreadedFast
- sb.AppendLine(" private System.Collections.Generic.List? _subscriptions;");
- sb.AppendLine(" private int _nextId;");
+ sb.AppendLine(" public System.Collections.Generic.List? Subscriptions;");
+ sb.AppendLine(" public int NextId;");
break;
case 1: // Locking
- sb.AppendLine(" private object? _lock;");
- sb.AppendLine(" private System.Collections.Generic.List? _subscriptions;");
- sb.AppendLine(" private int _nextId;");
+ sb.AppendLine(" public readonly object Lock = new();");
+ sb.AppendLine(" public System.Collections.Generic.List? Subscriptions;");
+ sb.AppendLine(" public int NextId;");
break;
case 2: // Concurrent
if (config.Order == 0) // RegistrationOrder
{
- sb.AppendLine(" private System.Collections.Immutable.ImmutableList? _subscriptions;");
- sb.AppendLine(" private int _nextId;");
+ sb.AppendLine(" public System.Collections.Immutable.ImmutableList? Subscriptions;");
+ sb.AppendLine(" public int NextId;");
}
else // Undefined
{
- sb.AppendLine(" private System.Collections.Concurrent.ConcurrentBag? _subscriptions;");
- sb.AppendLine(" private int _nextId;");
+ sb.AppendLine(" public System.Collections.Concurrent.ConcurrentBag? Subscriptions;");
+ sb.AppendLine(" public int NextId;");
}
break;
}
+
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(" private readonly ObserverState _state = new();");
sb.AppendLine();
}
private static void GenerateSubscribeMethods(StringBuilder sb, string payloadType, ObserverConfig config)
- {
+ {
if (!config.ForceAsync)
{
sb.AppendLine($" public System.IDisposable Subscribe(System.Action<{payloadType}> handler)");
sb.AppendLine(" {");
- sb.AppendLine(" var id = System.Threading.Interlocked.Increment(ref _nextId);");
- sb.AppendLine(" var sub = new Subscription(this, id, handler, false);");
- GenerateAddSubscription(sb, config, " ");
+ sb.AppendLine(" var id = System.Threading.Interlocked.Increment(ref _state.NextId);");
+ sb.AppendLine(" var sub = new Subscription(_state, id, handler, false);");
+ GenerateAddSubscription(sb, config, " ", "_state");
sb.AppendLine(" return sub;");
sb.AppendLine(" }");
sb.AppendLine();
@@ -224,38 +261,38 @@ private static void GenerateSubscribeMethods(StringBuilder sb, string payloadTyp
{
sb.AppendLine($" public System.IDisposable Subscribe(System.Func<{payloadType}, System.Threading.Tasks.ValueTask> handler)");
sb.AppendLine(" {");
- sb.AppendLine(" var id = System.Threading.Interlocked.Increment(ref _nextId);");
- sb.AppendLine(" var sub = new Subscription(this, id, handler, true);");
- GenerateAddSubscription(sb, config, " ");
+ sb.AppendLine(" var id = System.Threading.Interlocked.Increment(ref _state.NextId);");
+ sb.AppendLine(" var sub = new Subscription(_state, id, handler, true);");
+ GenerateAddSubscription(sb, config, " ", "_state");
sb.AppendLine(" return sub;");
sb.AppendLine(" }");
sb.AppendLine();
}
}
- private static void GenerateAddSubscription(StringBuilder sb, ObserverConfig config, string indent)
+ private static void GenerateAddSubscription(StringBuilder sb, ObserverConfig config, string indent, string stateVar)
{
switch (config.Threading)
{
case 0: // SingleThreadedFast
- sb.AppendLine($"{indent}(_subscriptions ??= new()).Add(sub);");
+ sb.AppendLine($"{indent}({stateVar}.Subscriptions ??= new()).Add(sub);");
break;
case 1: // Locking
- sb.AppendLine($"{indent}lock (_lock ??= new())");
+ sb.AppendLine($"{indent}lock ({stateVar}.Lock)");
sb.AppendLine($"{indent}{{");
- sb.AppendLine($"{indent} (_subscriptions ??= new()).Add(sub);");
+ sb.AppendLine($"{indent} ({stateVar}.Subscriptions ??= new()).Add(sub);");
sb.AppendLine($"{indent}}}");
break;
case 2: // Concurrent
- if (config.Order == 0) // RegistrationOrder
+ if (config.Order == 0) // RegistrationOrder (requires ImmutableList and Linq for .ToArray())
{
- sb.AppendLine($"{indent}System.Collections.Immutable.ImmutableInterlocked.Update(ref _subscriptions, static (list, s) => (list ?? System.Collections.Immutable.ImmutableList.Empty).Add(s), sub);");
+ sb.AppendLine($"{indent}System.Collections.Immutable.ImmutableInterlocked.Update(ref {stateVar}.Subscriptions, static (list, s) => (list ?? System.Collections.Immutable.ImmutableList.Empty).Add(s), sub);");
}
else // Undefined
{
- sb.AppendLine($"{indent}(_subscriptions ??= new()).Add(sub);");
+ sb.AppendLine($"{indent}({stateVar}.Subscriptions ??= new()).Add(sub);");
}
break;
}
@@ -278,7 +315,33 @@ private static void GeneratePublishMethods(StringBuilder sb, string payloadType,
sb.AppendLine(" foreach (var sub in snapshot)");
sb.AppendLine(" {");
- sb.AppendLine(" if (sub.IsAsync) continue;");
+
+ // Handle async subscriptions in fire-and-forget mode
+ sb.AppendLine(" if (sub.IsAsync)");
+ sb.AppendLine(" {");
+ if (config.Exceptions == 0) // Continue - fire and forget with error handling
+ {
+ sb.AppendLine(" System.Threading.Tasks.Task.Run(async () =>");
+ sb.AppendLine(" {");
+ sb.AppendLine(" try");
+ sb.AppendLine(" {");
+ sb.AppendLine(" await sub.InvokeAsync(payload, System.Threading.CancellationToken.None).ConfigureAwait(false);");
+ sb.AppendLine(" }");
+ sb.AppendLine(" catch (System.Exception ex)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" OnSubscriberError(ex);");
+ sb.AppendLine(" }");
+ sb.AppendLine(" });");
+ }
+ else // Stop or Aggregate - fire and forget without error handling (exceptions logged but not propagated)
+ {
+ sb.AppendLine(" System.Threading.Tasks.Task.Run(async () =>");
+ sb.AppendLine(" {");
+ sb.AppendLine(" await sub.InvokeAsync(payload, System.Threading.CancellationToken.None).ConfigureAwait(false);");
+ sb.AppendLine(" });");
+ }
+ sb.AppendLine(" continue;");
+ sb.AppendLine(" }");
if (config.Exceptions == 1) // Stop
{
@@ -371,30 +434,29 @@ private static void GeneratePublishMethods(StringBuilder sb, string payloadType,
}
private static void GenerateSnapshot(StringBuilder sb, ObserverConfig config, string indent)
- {
+ {
switch (config.Threading)
{
case 0: // SingleThreadedFast
- sb.AppendLine($"{indent}var snapshot = _subscriptions?.ToArray() ?? System.Array.Empty();");
+ sb.AppendLine($"{indent}var snapshot = _state.Subscriptions?.ToArray() ?? System.Array.Empty();");
break;
case 1: // Locking
sb.AppendLine($"{indent}Subscription[] snapshot;");
- sb.AppendLine($"{indent}var lockObj = _lock ??= new object();");
- sb.AppendLine($"{indent}lock (lockObj)");
+ sb.AppendLine($"{indent}lock (_state.Lock)");
sb.AppendLine($"{indent}{{");
- sb.AppendLine($"{indent} snapshot = _subscriptions?.ToArray() ?? System.Array.Empty();");
+ sb.AppendLine($"{indent} snapshot = _state.Subscriptions?.ToArray() ?? System.Array.Empty();");
sb.AppendLine($"{indent}}}");
break;
case 2: // Concurrent
if (config.Order == 0) // RegistrationOrder
{
- sb.AppendLine($"{indent}var snapshot = System.Threading.Volatile.Read(ref _subscriptions)?.ToArray() ?? System.Array.Empty();");
+ sb.AppendLine($"{indent}var snapshot = System.Threading.Volatile.Read(ref _state.Subscriptions)?.ToArray() ?? System.Array.Empty();");
}
else // Undefined
{
- sb.AppendLine($"{indent}var snapshot = _subscriptions?.ToArray() ?? System.Array.Empty();");
+ sb.AppendLine($"{indent}var snapshot = _state.Subscriptions?.ToArray() ?? System.Array.Empty();");
}
break;
}
@@ -408,21 +470,20 @@ private static void GenerateUnsubscribeMethod(StringBuilder sb, ObserverConfig c
switch (config.Threading)
{
case 0: // SingleThreadedFast
- sb.AppendLine(" _subscriptions?.RemoveAll(s => s.Id == id);");
+ sb.AppendLine(" _state.Subscriptions?.RemoveAll(s => s.Id == id);");
break;
case 1: // Locking
- sb.AppendLine(" var lockObj = _lock ??= new object();");
- sb.AppendLine(" lock (lockObj)");
+ sb.AppendLine(" lock (_state.Lock)");
sb.AppendLine(" {");
- sb.AppendLine(" _subscriptions?.RemoveAll(s => s.Id == id);");
+ 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 _subscriptions, static (list, id) => list?.RemoveAll(s => s.Id == id) ?? list, id);");
+ 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
{
@@ -443,10 +504,10 @@ private static void GenerateOnErrorHook(StringBuilder sb)
private static void GenerateSubscriptionClass(StringBuilder sb, string payloadType, ObserverConfig config)
{
- // Use generic parent reference to avoid dynamic
+ // Subscription now uses a delegate callback instead of reflection
sb.AppendLine($" private sealed class Subscription : System.IDisposable");
sb.AppendLine(" {");
- sb.AppendLine(" private object? _parent;");
+ sb.AppendLine(" private ObserverState? _state;");
sb.AppendLine(" private readonly int _id;");
sb.AppendLine(" private readonly object _handler;");
sb.AppendLine(" private readonly bool _isAsync;");
@@ -455,9 +516,9 @@ private static void GenerateSubscriptionClass(StringBuilder sb, string payloadTy
sb.AppendLine(" public int Id => _id;");
sb.AppendLine(" public bool IsAsync => _isAsync;");
sb.AppendLine();
- sb.AppendLine(" public Subscription(object parent, int id, object handler, bool isAsync)");
+ sb.AppendLine(" public Subscription(ObserverState state, int id, object handler, bool isAsync)");
sb.AppendLine(" {");
- sb.AppendLine(" _parent = parent;");
+ sb.AppendLine(" _state = state;");
sb.AppendLine(" _id = id;");
sb.AppendLine(" _handler = handler;");
sb.AppendLine(" _isAsync = isAsync;");
@@ -484,11 +545,36 @@ private static void GenerateSubscriptionClass(StringBuilder sb, string payloadTy
sb.AppendLine(" public void Dispose()");
sb.AppendLine(" {");
sb.AppendLine(" if (System.Threading.Interlocked.Exchange(ref _disposed, 1) != 0) return;");
- sb.AppendLine(" // Use reflection to call Unsubscribe since parent type is generic");
- sb.AppendLine(" var parent = System.Threading.Interlocked.Exchange(ref _parent, null);");
- sb.AppendLine(" if (parent == null) return;");
- sb.AppendLine(" var method = parent.GetType().GetMethod(\"Unsubscribe\", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);");
- sb.AppendLine(" method?.Invoke(parent, new object[] { _id });");
+ sb.AppendLine(" var state = System.Threading.Interlocked.Exchange(ref _state, null);");
+ sb.AppendLine(" if (state == null) return;");
+ sb.AppendLine();
+
+ // Generate the appropriate unsubscribe logic based on threading policy
+ 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
+ {
+ sb.AppendLine(" // ConcurrentBag doesn't support removal efficiently");
+ }
+ break;
+ }
+
sb.AppendLine(" }");
sb.AppendLine(" }");
}
From 62d3e7805f7281129a3c3cfe7614fbda835d7b3e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 19 Feb 2026 05:34:40 +0000
Subject: [PATCH 11/13] Update docs/examples/tests: fix diagnostics,
cancellation demo, add mixed handler test, verify struct diagnostics
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
docs/generators/observer.md | 2 +-
.../NotificationSystem.cs | 21 ++--
.../Observer/ObserverAttribute.cs | 2 +-
.../ObserverGeneratorTests.cs | 107 +++++++++++-------
4 files changed, 74 insertions(+), 58 deletions(-)
diff --git a/docs/generators/observer.md b/docs/generators/observer.md
index f9d702e..d3aa47f 100644
--- a/docs/generators/observer.md
+++ b/docs/generators/observer.md
@@ -610,7 +610,7 @@ subscriptions.Dispose();
|----|----------|-------------|
| **PKOBS001** | Error | Type marked with `[Observer]` must be declared as `partial` |
| **PKOBS002** | Error | Unable to extract payload type from `[Observer]` attribute |
-| **PKOBS003** | Error | Invalid configuration (conflicting settings) |
+| **PKOBS003** | Warning | Invalid configuration or unsupported type (generic, nested, or struct types) |
### PKOBS001: Type must be partial
diff --git a/src/PatternKit.Examples/ObserverGeneratorDemo/NotificationSystem.cs b/src/PatternKit.Examples/ObserverGeneratorDemo/NotificationSystem.cs
index 255a2e4..45bfbe8 100644
--- a/src/PatternKit.Examples/ObserverGeneratorDemo/NotificationSystem.cs
+++ b/src/PatternKit.Examples/ObserverGeneratorDemo/NotificationSystem.cs
@@ -331,21 +331,14 @@ public static async Task RunAsync()
var notification = new NotificationPublished();
var processedCount = 0;
- // Long-running async handler
+ // Long-running async handler
+ // Note: Cancellation is checked between handlers, not during handler execution
notification.Subscribe(async n =>
{
Console.WriteLine(" ⏳ Starting long operation...");
- try
- {
- // This will be cancelled
- await Task.Delay(5000);
- processedCount++;
- Console.WriteLine(" ✅ Long operation completed");
- }
- catch (TaskCanceledException)
- {
- Console.WriteLine(" 🚫 Long operation cancelled");
- }
+ await Task.Delay(100); // Shorter delay for demo
+ processedCount++;
+ Console.WriteLine(" ✅ Long operation completed");
});
// Quick handler
@@ -356,7 +349,7 @@ public static async Task RunAsync()
Console.WriteLine(" ✅ Quick operation completed");
});
- using var cts = new CancellationTokenSource(100); // Cancel after 100ms
+ using var cts = new CancellationTokenSource(50); // Cancel after 50ms - before first handler completes
try
{
@@ -370,6 +363,6 @@ await notification.PublishAsync(
}
Console.WriteLine($"\n Handlers completed: {processedCount}/2");
- Console.WriteLine(" (Quick handler completed, long handler was cancelled)");
+ Console.WriteLine(" Note: Cancellation is checked between handler invocations, not during execution");
}
}
diff --git a/src/PatternKit.Generators.Abstractions/Observer/ObserverAttribute.cs b/src/PatternKit.Generators.Abstractions/Observer/ObserverAttribute.cs
index 7d40bdc..8de6241 100644
--- a/src/PatternKit.Generators.Abstractions/Observer/ObserverAttribute.cs
+++ b/src/PatternKit.Generators.Abstractions/Observer/ObserverAttribute.cs
@@ -54,7 +54,7 @@ public ObserverAttribute(Type payloadType)
///
/// Gets or sets whether to generate async publish methods.
- /// When not explicitly set, async methods are generated if any async handlers are detected.
+ /// Default is true (async methods are always generated).
///
public bool GenerateAsync { get; set; } = true;
diff --git a/test/PatternKit.Generators.Tests/ObserverGeneratorTests.cs b/test/PatternKit.Generators.Tests/ObserverGeneratorTests.cs
index e2fc4b8..c84a998 100644
--- a/test/PatternKit.Generators.Tests/ObserverGeneratorTests.cs
+++ b/test/PatternKit.Generators.Tests/ObserverGeneratorTests.cs
@@ -474,7 +474,7 @@ public static string Run()
}
[Fact]
- public void Supports_Struct_Type()
+ public void Struct_Types_Are_Not_Supported()
{
var user = """
using PatternKit.Generators.Observer;
@@ -487,46 +487,18 @@ public record Temperature(double Celsius);
public partial struct TemperatureChanged
{
}
-
- public static class Demo
- {
- public static string Run()
- {
- var log = new System.Collections.Generic.List();
- var evt = new TemperatureChanged();
-
- evt.Subscribe((Temperature t) => log.Add($"T:{t.Celsius}"));
- evt.Publish(new Temperature(99));
-
- return string.Join("|", log);
- }
- }
""";
var comp = RoslynTestHelpers.CreateCompilation(
user,
- assemblyName: nameof(Supports_Struct_Type));
+ assemblyName: nameof(Struct_Types_Are_Not_Supported));
var gen = new Observer.ObserverGenerator();
- _ = RoslynTestHelpers.Run(comp, gen, out _, out var updated);
-
- using var pe = new MemoryStream();
- updated.Emit(pe);
- pe.Position = 0;
+ _ = RoslynTestHelpers.Run(comp, gen, out var run, out _);
- var alc = new AssemblyLoadContext("ObserverTest", isCollectible: true);
- try
- {
- var asm = alc.LoadFromStream(pe);
- var demoType = asm.GetType("PatternKit.Examples.Generators.Demo");
- var runMethod = demoType!.GetMethod("Run");
- var result = (string)runMethod!.Invoke(null, null)!;
- Assert.Equal("T:99", result);
- }
- finally
- {
- alc.Unload();
- }
+ // Should report PKOBS003 diagnostic for struct types
+ var diagnostics = run.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKOBS003" && d.GetMessage().Contains("Struct observer types are not currently supported"));
}
[Fact]
@@ -571,7 +543,7 @@ public static string Run()
}
[Fact]
- public void Supports_Record_Struct()
+ public void Record_Struct_Types_Are_Not_Supported()
{
var user = """
using PatternKit.Generators.Observer;
@@ -584,30 +556,81 @@ public record Temperature(double Celsius);
public partial record struct TemperatureChanged
{
}
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(
+ user,
+ assemblyName: nameof(Record_Struct_Types_Are_Not_Supported));
+
+ var gen = new Observer.ObserverGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var run, out _);
+
+ // Should report PKOBS003 diagnostic for record struct types
+ var diagnostics = run.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKOBS003" && d.GetMessage().Contains("Struct observer types are not currently supported"));
+ }
+
+ [Fact]
+ public void Mixed_Sync_And_Async_Handlers_Both_Invoked()
+ {
+ var user = SimpleObserver + """
public static class Demo
{
- public static string Run()
+ public static async System.Threading.Tasks.Task Run()
{
var log = new System.Collections.Generic.List();
var evt = new TemperatureChanged();
- evt.Subscribe((Temperature t) => log.Add("OK"));
- evt.Publish(new Temperature(0));
+ // Subscribe sync handler
+ evt.Subscribe((Temperature t) => log.Add("Sync"));
- return string.Join("", log);
+ // Subscribe async handler
+ evt.Subscribe(async (Temperature t) =>
+ {
+ await System.Threading.Tasks.Task.Delay(1);
+ log.Add("Async");
+ });
+
+ // Sync Publish should invoke async handlers fire-and-forget
+ evt.Publish(new Temperature(10));
+
+ // Wait a bit for fire-and-forget to complete
+ await System.Threading.Tasks.Task.Delay(50);
+
+ return string.Join("|", log);
}
}
""";
var comp = RoslynTestHelpers.CreateCompilation(
user,
- assemblyName: nameof(Supports_Record_Struct));
+ assemblyName: nameof(Mixed_Sync_And_Async_Handlers_Both_Invoked));
var gen = new Observer.ObserverGenerator();
_ = RoslynTestHelpers.Run(comp, gen, out _, out var updated);
- var emit = updated.Emit(Stream.Null);
- Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ using var pe = new MemoryStream();
+ updated.Emit(pe);
+ pe.Position = 0;
+
+ var alc = new AssemblyLoadContext("ObserverTest", isCollectible: true);
+ try
+ {
+ var asm = alc.LoadFromStream(pe);
+ var demoType = asm.GetType("PatternKit.Examples.Generators.Demo");
+ var runMethod = demoType!.GetMethod("Run");
+ var task = (System.Threading.Tasks.Task)runMethod!.Invoke(null, null)!;
+ task.Wait();
+ var result = task.Result;
+
+ // Both handlers should have been invoked
+ Assert.Contains("Sync", result);
+ Assert.Contains("Async", result);
+ }
+ finally
+ {
+ alc.Unload();
+ }
}
}
From 47f52c3d43d7af999ad65bce39ef0cb15e50adbe Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 19 Feb 2026 14:51:28 +0000
Subject: [PATCH 12/13] Fix review feedback: update struct support docs, remove
AttributeTargets.Struct, improve async error handling, remove unused using
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
docs/generators/observer.md | 10 +++---
.../Observer/ObserverAttribute.cs | 4 +--
.../Observer/ObserverGenerator.cs | 31 ++++++++++++-------
3 files changed, 28 insertions(+), 17 deletions(-)
diff --git a/docs/generators/observer.md b/docs/generators/observer.md
index d3aa47f..439c7ac 100644
--- a/docs/generators/observer.md
+++ b/docs/generators/observer.md
@@ -349,14 +349,16 @@ public partial class DataAvailable { }
The generator supports:
-| Type | Supported | Example |
-|------|-----------|---------|
+| Type | Supported | Example / Notes |
+|------|-----------|------------------|
| `partial class` | ✅ | `public partial class Event { }` |
-| `partial struct` | ✅ | `public partial struct Event { }` |
+| `partial struct` | ❌ | Generates PKOBS003 diagnostic (struct observers are not supported) |
| `partial record class` | ✅ | `public partial record class Event;` |
-| `partial record struct` | ✅ | `public partial record struct Event;` |
+| `partial record struct` | ❌ | Generates PKOBS003 diagnostic (struct observers are not supported) |
| Non-partial types | ❌ | Generates PKOBS001 error |
+> **Note:** Struct-based observer types (`partial struct`, `partial record struct`) are rejected with PKOBS003 diagnostic. Supporting struct observers would require complex capture and boxing semantics, so only class-based observer types are currently supported.
+
## API Reference
### Subscribe Methods
diff --git a/src/PatternKit.Generators.Abstractions/Observer/ObserverAttribute.cs b/src/PatternKit.Generators.Abstractions/Observer/ObserverAttribute.cs
index 8de6241..31eae07 100644
--- a/src/PatternKit.Generators.Abstractions/Observer/ObserverAttribute.cs
+++ b/src/PatternKit.Generators.Abstractions/Observer/ObserverAttribute.cs
@@ -2,7 +2,7 @@ namespace PatternKit.Generators.Observer;
///
/// Marks a type for Observer pattern code generation.
-/// The type must be declared as partial (class, struct, record class, or record struct).
+/// The type must be declared as partial class or partial record class.
///
///
///
@@ -17,7 +17,7 @@ namespace PatternKit.Generators.Observer;
///
///
///
-[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
+[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class ObserverAttribute : Attribute
{
///
diff --git a/src/PatternKit.Generators/Observer/ObserverGenerator.cs b/src/PatternKit.Generators/Observer/ObserverGenerator.cs
index fe81357..a3ad4c2 100644
--- a/src/PatternKit.Generators/Observer/ObserverGenerator.cs
+++ b/src/PatternKit.Generators/Observer/ObserverGenerator.cs
@@ -173,13 +173,6 @@ private static string GenerateSource(INamedTypeSymbol typeSymbol, INamedTypeSymb
sb.AppendLine("#nullable enable");
sb.AppendLine("// ");
sb.AppendLine();
-
- // Add necessary using directives
- if (config.Threading == 2 && config.Order == 0) // Concurrent + RegistrationOrder uses ImmutableList
- {
- sb.AppendLine("using System.Linq;");
- sb.AppendLine();
- }
if (ns != null)
{
@@ -187,8 +180,6 @@ private static string GenerateSource(INamedTypeSymbol typeSymbol, INamedTypeSymb
sb.AppendLine();
}
- var isStruct = false; // Structs are now rejected above
-
sb.AppendLine($"{accessibility} partial {typeKind} {typeName}");
sb.AppendLine("{");
@@ -333,13 +324,29 @@ private static void GeneratePublishMethods(StringBuilder sb, string payloadType,
sb.AppendLine(" }");
sb.AppendLine(" });");
}
- else // Stop or Aggregate - fire and forget without error handling (exceptions logged but not propagated)
+ else if (config.Exceptions == 1) // Stop - fire and forget; exceptions are unobserved
{
+ sb.AppendLine(" // Fire-and-forget: exceptions from async handlers cannot stop sync execution");
sb.AppendLine(" System.Threading.Tasks.Task.Run(async () =>");
sb.AppendLine(" {");
sb.AppendLine(" await sub.InvokeAsync(payload, System.Threading.CancellationToken.None).ConfigureAwait(false);");
sb.AppendLine(" });");
}
+ else // Aggregate - fire and forget with error logging via OnSubscriberError
+ {
+ sb.AppendLine(" // Fire-and-forget: async exceptions logged via OnSubscriberError (cannot aggregate synchronously)");
+ sb.AppendLine(" System.Threading.Tasks.Task.Run(async () =>");
+ sb.AppendLine(" {");
+ sb.AppendLine(" try");
+ sb.AppendLine(" {");
+ sb.AppendLine(" await sub.InvokeAsync(payload, System.Threading.CancellationToken.None).ConfigureAwait(false);");
+ sb.AppendLine(" }");
+ sb.AppendLine(" catch (System.Exception ex)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" OnSubscriberError(ex);");
+ sb.AppendLine(" }");
+ sb.AppendLine(" });");
+ }
sb.AppendLine(" continue;");
sb.AppendLine(" }");
@@ -570,7 +577,9 @@ private static void GenerateSubscriptionClass(StringBuilder sb, string payloadTy
}
else
{
- sb.AppendLine(" // ConcurrentBag doesn't support removal efficiently");
+ sb.AppendLine(" // ConcurrentBag doesn't support efficient removal.");
+ sb.AppendLine(" // Disposed subscriptions remain in the bag but are marked as disposed and won't be invoked.");
+ sb.AppendLine(" // Note: This can cause memory growth if many subscriptions are created and disposed.");
}
break;
}
From aafc0c0252b8cf8909530745451a6aebc2c89afb Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 19 Feb 2026 16:44:00 +0000
Subject: [PATCH 13/13] Remove unused GenerateUnsubscribeMethod - disposal
handled directly by Subscription.Dispose
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
.../Observer/ObserverGenerator.cs | 35 -------------------
1 file changed, 35 deletions(-)
diff --git a/src/PatternKit.Generators/Observer/ObserverGenerator.cs b/src/PatternKit.Generators/Observer/ObserverGenerator.cs
index a3ad4c2..3edb4a7 100644
--- a/src/PatternKit.Generators/Observer/ObserverGenerator.cs
+++ b/src/PatternKit.Generators/Observer/ObserverGenerator.cs
@@ -186,7 +186,6 @@ private static string GenerateSource(INamedTypeSymbol typeSymbol, INamedTypeSymb
GenerateFields(sb, config);
GenerateSubscribeMethods(sb, payloadTypeName, config);
GeneratePublishMethods(sb, payloadTypeName, config);
- GenerateUnsubscribeMethod(sb, config);
GenerateOnErrorHook(sb);
GenerateSubscriptionClass(sb, payloadTypeName, config);
@@ -469,40 +468,6 @@ private static void GenerateSnapshot(StringBuilder sb, ObserverConfig config, st
}
}
- private static void GenerateUnsubscribeMethod(StringBuilder sb, ObserverConfig config)
- {
- 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();
- }
-
private static void GenerateOnErrorHook(StringBuilder sb)
{
sb.AppendLine(" partial void OnSubscriberError(System.Exception ex);");