From 35ecfe482f419245ddc0d2f19fb743ecb2bf3205 Mon Sep 17 00:00:00 2001 From: "Jeremy D. Miller" Date: Fri, 27 Mar 2026 12:51:49 -0500 Subject: [PATCH 1/2] Add source generator discovery for natural key aggregates New NaturalKeyAggregateAttribute emitted by the source generator when a self-aggregating type has a [NaturalKey] property. This allows Marten to auto-register the snapshot projection and natural key infrastructure at startup without requiring explicit Projections.Snapshot(Inline) calls. - AggregateAnalyzer: detect [NaturalKey] properties on aggregate types - EvolverCodeEmitter: emit [assembly: NaturalKeyAggregate(typeof(T))] - NaturalKeyAggregateAttribute: assembly-level marker for discovery Fixes JasperFx/marten#4199 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../AggregateAnalyzer.cs | 19 +++++++++++++++++-- .../EvolverCodeEmitter.cs | 8 ++++++++ .../NaturalKeyAggregateAttribute.cs | 18 ++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 src/JasperFx.Events/Aggregation/NaturalKeyAggregateAttribute.cs diff --git a/src/JasperFx.Events.SourceGenerator/AggregateAnalyzer.cs b/src/JasperFx.Events.SourceGenerator/AggregateAnalyzer.cs index 33fe113..e8b90b5 100644 --- a/src/JasperFx.Events.SourceGenerator/AggregateAnalyzer.cs +++ b/src/JasperFx.Events.SourceGenerator/AggregateAnalyzer.cs @@ -89,6 +89,8 @@ internal sealed class CandidateInfo public List DiscoveredPublishedTypes { get; set; } = new(); // SelfAggregatingEvolve-specific public EvolveMethodInfo? EvolveMethod { get; set; } + // Natural key discovery + public bool HasNaturalKey { get; set; } } internal static class AggregateAnalyzer @@ -209,7 +211,8 @@ internal static class AggregateAnalyzer AggregateType = classSymbol, IdentityType = idType, Methods = methods, - HasDefaultConstructor = HasParameterlessConstructor(classSymbol) + HasDefaultConstructor = HasParameterlessConstructor(classSymbol), + HasNaturalKey = HasNaturalKeyProperty(classSymbol) }; } @@ -242,7 +245,8 @@ internal static class AggregateAnalyzer AggregateType = classSymbol, IdentityType = idType, HasDefaultConstructor = HasParameterlessConstructor(classSymbol), - EvolveMethod = evolveMethod + EvolveMethod = evolveMethod, + HasNaturalKey = HasNaturalKeyProperty(classSymbol) }; } @@ -1069,6 +1073,17 @@ private static bool HasParameterlessConstructor(INamedTypeSymbol type) return constructors.Any(c => c.Parameters.Length == 0 && c.DeclaredAccessibility == Accessibility.Public); } + /// + /// Checks if the type has a property marked with [NaturalKey] attribute + /// + private static bool HasNaturalKeyProperty(INamedTypeSymbol type) + { + return type.GetMembers() + .OfType() + .Any(p => p.GetAttributes() + .Any(a => a.AttributeClass?.Name is "NaturalKeyAttribute" or "NaturalKey")); + } + /// /// Checks if the type has an explicitly declared parameterless constructor (not implicit). /// Used to avoid generating a duplicate constructor in partial classes. diff --git a/src/JasperFx.Events.SourceGenerator/EvolverCodeEmitter.cs b/src/JasperFx.Events.SourceGenerator/EvolverCodeEmitter.cs index 8edc988..74f1e46 100644 --- a/src/JasperFx.Events.SourceGenerator/EvolverCodeEmitter.cs +++ b/src/JasperFx.Events.SourceGenerator/EvolverCodeEmitter.cs @@ -406,6 +406,10 @@ public static string EmitSelfAggregatingEvolver(CandidateInfo info) // Assembly attributes must precede namespace declarations sb.AppendLine($"[assembly: global::JasperFx.Events.Aggregation.GeneratedEvolver(typeof({aggregateFullName}), typeof({GetFullyQualifiedEvolverName(info, evolverName)}))]"); + if (info.HasNaturalKey) + { + sb.AppendLine($"[assembly: global::JasperFx.Events.Aggregation.NaturalKeyAggregate(typeof({aggregateFullName}))]"); + } sb.AppendLine(); var ns = info.ClassSymbol.ContainingNamespace; @@ -455,6 +459,10 @@ public static string EmitSelfAggregatingEvolveEvolver(CandidateInfo info) // Assembly attribute for discovery sb.AppendLine($"[assembly: global::JasperFx.Events.Aggregation.GeneratedEvolver(typeof({aggregateFullName}), typeof({GetFullyQualifiedEvolverName(info, evolverName)}))]"); + if (info.HasNaturalKey) + { + sb.AppendLine($"[assembly: global::JasperFx.Events.Aggregation.NaturalKeyAggregate(typeof({aggregateFullName}))]"); + } sb.AppendLine(); var ns = info.ClassSymbol.ContainingNamespace; diff --git a/src/JasperFx.Events/Aggregation/NaturalKeyAggregateAttribute.cs b/src/JasperFx.Events/Aggregation/NaturalKeyAggregateAttribute.cs new file mode 100644 index 0000000..3d7065e --- /dev/null +++ b/src/JasperFx.Events/Aggregation/NaturalKeyAggregateAttribute.cs @@ -0,0 +1,18 @@ +namespace JasperFx.Events.Aggregation; + +/// +/// Assembly-level attribute emitted by the source generator to indicate that a self-aggregating +/// type has a [NaturalKey] property. This allows event store implementations (e.g., Marten) +/// to auto-register the snapshot projection and natural key infrastructure at startup, +/// without requiring the user to explicitly call Projections.Snapshot<T>(Inline). +/// +[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] +public class NaturalKeyAggregateAttribute : Attribute +{ + public NaturalKeyAggregateAttribute(Type aggregateType) + { + AggregateType = aggregateType; + } + + public Type AggregateType { get; } +} From 4a4c740ea7af76e5aabcf4e67352ccae1ec6e8a7 Mon Sep 17 00:00:00 2001 From: "Jeremy D. Miller" Date: Fri, 27 Mar 2026 13:10:20 -0500 Subject: [PATCH 2/2] Bump versions for natural key source generator release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - JasperFx 1.21.4 → 1.21.5 - JasperFx.Events 1.24.1 → 1.24.2 (new NaturalKeyAggregateAttribute) - JasperFx.Events.SourceGenerator 1.3.0 → 1.4.0 (natural key discovery) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../JasperFx.Events.SourceGenerator.csproj | 2 +- src/JasperFx.Events/JasperFx.Events.csproj | 2 +- src/JasperFx/JasperFx.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/JasperFx.Events.SourceGenerator/JasperFx.Events.SourceGenerator.csproj b/src/JasperFx.Events.SourceGenerator/JasperFx.Events.SourceGenerator.csproj index f91ec5b..a029ca2 100644 --- a/src/JasperFx.Events.SourceGenerator/JasperFx.Events.SourceGenerator.csproj +++ b/src/JasperFx.Events.SourceGenerator/JasperFx.Events.SourceGenerator.csproj @@ -12,7 +12,7 @@ true Source generator for Fast Aggregate Projections for Marten and future JasperFx Event Stores JasperFx.Events.SourceGenerator - 1.3.0 + 1.4.0 false true diff --git a/src/JasperFx.Events/JasperFx.Events.csproj b/src/JasperFx.Events/JasperFx.Events.csproj index e7dfbc9..9be594c 100644 --- a/src/JasperFx.Events/JasperFx.Events.csproj +++ b/src/JasperFx.Events/JasperFx.Events.csproj @@ -3,7 +3,7 @@ Foundational Event Store Abstractions and Projections for the Critter Stack JasperFx.Events - 1.24.1 + 1.24.2 diff --git a/src/JasperFx/JasperFx.csproj b/src/JasperFx/JasperFx.csproj index a5cac1a..5bc3e90 100644 --- a/src/JasperFx/JasperFx.csproj +++ b/src/JasperFx/JasperFx.csproj @@ -3,7 +3,7 @@ Foundational helpers and command line support used by JasperFx and the Critter Stack projects JasperFx JasperFx - 1.21.4 + 1.21.5