From c45cde451a73a0191ca42110517fa4768eb778d1 Mon Sep 17 00:00:00 2001 From: "Jeremy D. Miller" Date: Thu, 6 Aug 2026 04:17:45 -0500 Subject: [PATCH 1/2] fix(generator): register published types via a PublishedTypes() override (marten#5192) An EventProjection's discovered published document types were registered by emitting `public MyProjection() { RegisterPublishedType(...); }` into the user's partial class. A constructor is the wrong extension point, and it failed two ways. It is illegal on a type that declares a primary constructor -- `partial class MyProjection(ILogger logger) : EventProjection` -- because C# requires every other constructor to chain through the primary one, so the generated file broke the build outright with CS8862. That is what marten#5192 reported. Worse, and silent: a projection that takes dependencies has to be registered through Marten's AddProjectionWithServices, so the container calls the dependency-taking constructor and the generated parameterless one never ran. Published types went unregistered, which also left jasperfx#626's teardown registration -- it reads PublishedTypes() -- with nothing to register. Emit an override of the virtual ProjectionBase.PublishedTypes() instead. It does not care how the instance was constructed, and chaining through base.PublishedTypes() keeps hand-written RegisterPublishedType calls and Options.StorageTypes flowing. The generator yields when the author already wrote their own override. Both emission sites are covered: EmitEventProjectionTypeRegistrationPartial (an ApplyAsync override) and the conventional-method path. Neither defect was reachable before 2.38.0: discovery was syntactic, so only an explicit `Store(x)` produced a registration and the far more common `Store(doc)` produced none. jasperfx#611 made discovery semantic and both spellings started emitting the constructor. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VpDCvJcBDZerieJB4JEHde --- .../AggregateEvolverGeneratorTests.cs | 117 +++++++++++++++++- .../AggregateAnalyzer.cs | 38 +++++- .../EvolverCodeEmitter.cs | 63 ++++++---- 3 files changed, 188 insertions(+), 30 deletions(-) diff --git a/src/JasperFx.Events.SourceGenerator.Tests/AggregateEvolverGeneratorTests.cs b/src/JasperFx.Events.SourceGenerator.Tests/AggregateEvolverGeneratorTests.cs index 1c7ece7a..bd04cabc 100644 --- a/src/JasperFx.Events.SourceGenerator.Tests/AggregateEvolverGeneratorTests.cs +++ b/src/JasperFx.Events.SourceGenerator.Tests/AggregateEvolverGeneratorTests.cs @@ -1456,7 +1456,7 @@ public override ValueTask ApplyAsync(ITestOperations operations, IEvent e, Cance "); string.Join("\n", generatedSources) - .ShouldContain("RegisterPublishedType(typeof(global::Test.AuditRecord));"); + .ShouldContain("typeof(global::Test.AuditRecord)"); } [Fact] @@ -1476,7 +1476,7 @@ public override ValueTask ApplyAsync(ITestOperations operations, IEvent e, Cance "); string.Join("\n", generatedSources) - .ShouldContain("RegisterPublishedType(typeof(global::Test.AuditRecord));"); + .ShouldContain("typeof(global::Test.AuditRecord)"); } [Fact] @@ -1494,7 +1494,7 @@ public override ValueTask ApplyAsync(ITestOperations operations, IEvent e, Cance "); diagnostics.ShouldContain(d => d.Id == "JFXEVT005"); - string.Join("\n", generatedSources).ShouldNotContain("RegisterPublishedType"); + string.Join("\n", generatedSources).ShouldNotContain("typeof(global::Test.AuditRecord)"); } [Fact] @@ -1518,6 +1518,115 @@ public override ValueTask ApplyAsync(ITestOperations operations, IEvent e, Cance "); diagnostics.ShouldNotContain(d => d.Id == "JFXEVT005"); - string.Join("\n", generatedSources).ShouldNotContain("RegisterPublishedType"); + string.Join("\n", generatedSources).ShouldNotContain("typeof(global::Test.AuditRecord)"); + } + + [Fact] + public void registers_published_types_on_a_projection_with_a_primary_constructor() + { + // marten#5192: registration used to be emitted as a parameterless constructor, which is + // illegal on a type declaring a primary constructor -- C# requires every other constructor + // to chain through it -- so the generated file failed the whole build with CS8862. + var source = EventProjectionPreamble + @" +public class Recorder { } + +public partial class PrimaryCtorProjection(Recorder recorder) : TestEventProjection +{ + public override ValueTask ApplyAsync(ITestOperations operations, IEvent e, CancellationToken cancellation) + { + operations.Store(new AuditRecord()); + return default; + } +} +"; + + CompileWithGenerator(source) + .Where(d => d.Severity == DiagnosticSeverity.Error) + .Select(d => d.ToString()) + .ShouldBeEmpty(); + + var (_, generatedSources) = RunGenerator(source); + string.Join("\n", generatedSources).ShouldContain("typeof(global::Test.AuditRecord)"); + } + + [Fact] + public void registers_published_types_on_a_projection_built_by_a_container() + { + // marten#5192: a projection taking dependencies has to be registered through Marten's + // AddProjectionWithServices, so the container calls the dependency-taking constructor. The + // old generated parameterless constructor compiled here but never ran, and the published + // types went silently unregistered. An override does not care how the instance was built. + var (_, generatedSources) = RunGenerator(EventProjectionPreamble + @" +public class Recorder { } + +public partial class InjectedProjection : TestEventProjection +{ + private readonly Recorder _recorder; + + public InjectedProjection(Recorder recorder) => _recorder = recorder; + + public override ValueTask ApplyAsync(ITestOperations operations, IEvent e, CancellationToken cancellation) + { + operations.Store(new AuditRecord()); + return default; + } +} +"); + + var generated = string.Join("\n", generatedSources); + generated.ShouldContain( + "public override global::System.Collections.Generic.IEnumerable PublishedTypes()"); + generated.ShouldContain("typeof(global::Test.AuditRecord)"); + generated.ShouldNotContain("public InjectedProjection()"); + } + + [Fact] + public void defers_to_a_hand_written_published_types_override() + { + // The author took control of PublishedTypes(); a generated second override would be CS0111. + var source = EventProjectionPreamble + @" +public partial class HandWrittenProjection : TestEventProjection +{ + public override System.Collections.Generic.IEnumerable PublishedTypes() => new[] { typeof(AuditRecord) }; + + public override ValueTask ApplyAsync(ITestOperations operations, IEvent e, CancellationToken cancellation) + { + operations.Store(new AuditRecord()); + return default; + } +} +"; + + CompileWithGenerator(source) + .Where(d => d.Severity == DiagnosticSeverity.Error) + .Select(d => d.ToString()) + .ShouldBeEmpty(); + + string.Join("\n", RunGenerator(source).generatedSources) + .ShouldNotContain( + "public override global::System.Collections.Generic.IEnumerable PublishedTypes()"); + } + + [Fact] + public void registers_published_types_from_conventional_create_methods_with_a_primary_constructor() + { + // Same marten#5192 break on the other emission site: a conventional-method EventProjection + // gets its registration in .EventProjection.g.cs rather than .TypeRegistration.g.cs. + var source = EventProjectionPreamble + @" +public class Recorder { } + +public partial class ConventionalPrimaryCtorProjection(Recorder recorder) : TestEventProjection +{ + public AuditRecord Create(AuditableEvent e) => new AuditRecord(); +} +"; + + CompileWithGenerator(source) + .Where(d => d.Severity == DiagnosticSeverity.Error) + .Select(d => d.ToString()) + .ShouldBeEmpty(); + + string.Join("\n", RunGenerator(source).generatedSources) + .ShouldContain("typeof(global::Test.AuditRecord)"); } } diff --git a/src/JasperFx.Events.SourceGenerator/AggregateAnalyzer.cs b/src/JasperFx.Events.SourceGenerator/AggregateAnalyzer.cs index e60189d5..d0962174 100644 --- a/src/JasperFx.Events.SourceGenerator/AggregateAnalyzer.cs +++ b/src/JasperFx.Events.SourceGenerator/AggregateAnalyzer.cs @@ -112,6 +112,14 @@ internal sealed class CandidateInfo public List EventConstructors { get; set; } = new(); public bool HasDefaultConstructor { get; set; } public bool HasExistingParameterlessConstructor { get; set; } // On the projection class itself + + /// + /// True when the projection — or a user-written base class between it and ProjectionBase — + /// already overrides PublishedTypes(). The generator yields to a hand-written override rather + /// than emitting a second one and tripping CS0111. See marten#5192. + /// + public bool HasExistingPublishedTypesOverride { get; set; } + // EventProjection-specific public INamedTypeSymbol? OperationsType { get; set; } // TOperations from JasperFxEventProjectionBase /// @@ -240,7 +248,8 @@ internal static class AggregateAnalyzer QuerySessionType = baseInfo.querySessionType, Methods = methods, HasDefaultConstructor = HasParameterlessConstructor(baseInfo.docType), - HasExistingParameterlessConstructor = HasExplicitParameterlessConstructor(classSymbol) + HasExistingParameterlessConstructor = HasExplicitParameterlessConstructor(classSymbol), + HasExistingPublishedTypesOverride = HasPublishedTypesOverride(classSymbol) }; } @@ -951,7 +960,8 @@ public static (INamedTypeSymbol operationsType, INamedTypeSymbol querySessionTyp QuerySessionType = baseInfo.querySessionType, DiscoveredPublishedTypes = discoveredTypes, UnresolvedDocumentOperations = unresolved, - HasExistingParameterlessConstructor = HasExplicitParameterlessConstructor(classSymbol) + HasExistingParameterlessConstructor = HasExplicitParameterlessConstructor(classSymbol), + HasExistingPublishedTypesOverride = HasPublishedTypesOverride(classSymbol) }; } @@ -972,7 +982,8 @@ public static (INamedTypeSymbol operationsType, INamedTypeSymbol querySessionTyp OperationsType = baseInfo.operationsType, QuerySessionType = baseInfo.querySessionType, Methods = methods, - HasExistingParameterlessConstructor = HasExplicitParameterlessConstructor(classSymbol) + HasExistingParameterlessConstructor = HasExplicitParameterlessConstructor(classSymbol), + HasExistingPublishedTypesOverride = HasPublishedTypesOverride(classSymbol) }; } @@ -1553,6 +1564,27 @@ private static bool HasExplicitParameterlessConstructor(INamedTypeSymbol type) !c.IsImplicitlyDeclared); } + /// + /// Does this projection, or a user-written base class beneath ProjectionBase, already override + /// ProjectionBase.PublishedTypes()? The generator defers to it rather than emitting a + /// competing override. See marten#5192. + /// + private static bool HasPublishedTypesOverride(INamedTypeSymbol type) + { + var current = type; + while (current != null && current.Name != "ProjectionBase") + { + foreach (var member in current.GetMembers("PublishedTypes")) + { + if (member is IMethodSymbol { IsOverride: true, Parameters.Length: 0 }) return true; + } + + current = current.BaseType; + } + + return false; + } + /// /// Checks if the type has a constructor that takes exactly one parameter that isn't /// a well-known framework type (i.e., it's likely an event parameter constructor diff --git a/src/JasperFx.Events.SourceGenerator/EvolverCodeEmitter.cs b/src/JasperFx.Events.SourceGenerator/EvolverCodeEmitter.cs index 734d7a13..bf9cbfea 100644 --- a/src/JasperFx.Events.SourceGenerator/EvolverCodeEmitter.cs +++ b/src/JasperFx.Events.SourceGenerator/EvolverCodeEmitter.cs @@ -556,8 +556,8 @@ public static string EmitEventProjectionPartial(CandidateInfo info) sb.AppendLine($"partial class {className}{typeParams}"); sb.AppendLine("{"); - // Emit constructor that registers published document types from Create/Transform methods - EmitEventProjectionConstructor(sb, info, className); + // Register the document types published by Create/Transform methods + EmitConventionalPublishedTypes(sb, info); EmitEventProjectionApplyAsync(sb, info); @@ -573,7 +573,7 @@ public static string EmitEventProjectionPartial(CandidateInfo info) } /// - /// Emits a partial class with only a constructor that registers published document types. + /// Emits a partial class that registers published document types and nothing else. /// Used for EventProjection subclasses that have an explicit ApplyAsync override and /// call Store/Insert/Delete with document types that need to be registered. /// See https://github.com/JasperFx/marten/issues/4166 @@ -609,17 +609,8 @@ public static string EmitEventProjectionTypeRegistrationPartial(CandidateInfo in sb.AppendLine($"partial class {className}{typeParams}"); sb.AppendLine("{"); - if (!info.HasExistingParameterlessConstructor && info.DiscoveredPublishedTypes.Count > 0) - { - sb.AppendLine($" [global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"JasperFx.Events.SourceGenerator\", \"1.0\")]"); - sb.AppendLine($" public {className}()"); - sb.AppendLine(" {"); - foreach (var docType in info.DiscoveredPublishedTypes.Select(t => Fqn(t)).Distinct()) - { - sb.AppendLine($" RegisterPublishedType(typeof({docType}));"); - } - sb.AppendLine(" }"); - } + EmitPublishedTypesOverride(sb, info, + info.DiscoveredPublishedTypes.Select(Fqn).Distinct().ToList()); sb.AppendLine("}"); @@ -646,15 +637,12 @@ private static List GetContainingTypes(INamedTypeSymbol type) } /// - /// Emits a constructor that registers document types published by Create/Transform methods - /// as published types, so that the document storage is generated for code-gen scenarios. + /// Registers the document types published by Create/Transform methods, so that the document + /// storage is generated for code-gen scenarios. /// See https://github.com/JasperFx/marten/issues/4166 /// - private static void EmitEventProjectionConstructor(StringBuilder sb, CandidateInfo info, string className) + private static void EmitConventionalPublishedTypes(StringBuilder sb, CandidateInfo info) { - // Skip if the class already has an explicit parameterless constructor - if (info.HasExistingParameterlessConstructor) return; - // Collect distinct entity return types from Create/Transform methods var publishedTypes = info.Methods .Where(m => m.EntityReturnType != null && (m.MethodName == "Create" || m.MethodName == "Transform")) @@ -662,15 +650,44 @@ private static void EmitEventProjectionConstructor(StringBuilder sb, CandidateIn .Distinct() .ToList(); + EmitPublishedTypesOverride(sb, info, publishedTypes); + } + + /// + /// Declares the discovered published document types by overriding + /// ProjectionBase.PublishedTypes() in the user's partial class. + /// + /// This used to be emitted as a parameterless constructor calling + /// RegisterPublishedType, which was wrong on two counts (marten#5192). A constructor is + /// illegal on a type that declares a primary constructor — + /// partial class MyProjection(ILogger logger) : EventProjection — because C# requires + /// every other constructor to chain through it, so the generated file broke the build with + /// CS8862. And when the projection is container-built (Marten's + /// AddProjectionWithServices<T>, which is precisely what a projection taking an + /// ILogger has to use), the container calls the dependency-taking constructor, so the + /// generated parameterless one never ran and the published types were silently never + /// registered at all. + /// + /// An override is immune to both: it does not care how the instance was constructed, and + /// chaining through base.PublishedTypes() keeps hand-written RegisterPublishedType + /// calls and Options.StorageTypes flowing. Skipped when the author already wrote their + /// own override — theirs wins. + /// + private static void EmitPublishedTypesOverride(StringBuilder sb, CandidateInfo info, List publishedTypes) + { if (publishedTypes.Count == 0) return; + if (info.HasExistingPublishedTypesOverride) return; - sb.AppendLine($" [global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"JasperFx.Events.SourceGenerator\", \"1.0\")]"); - sb.AppendLine($" public {className}()"); + sb.AppendLine(" [global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"JasperFx.Events.SourceGenerator\", \"1.0\")]"); + sb.AppendLine(" public override global::System.Collections.Generic.IEnumerable PublishedTypes()"); sb.AppendLine(" {"); + sb.AppendLine(" var publishedTypes = new global::System.Collections.Generic.List(base.PublishedTypes());"); foreach (var typeName in publishedTypes) { - sb.AppendLine($" RegisterPublishedType(typeof({typeName}));"); + sb.AppendLine($" if (!publishedTypes.Contains(typeof({typeName}))) publishedTypes.Add(typeof({typeName}));"); } + + sb.AppendLine(" return publishedTypes;"); sb.AppendLine(" }"); sb.AppendLine(); } From 0feba4fb698c2a9878f396e047a2ff1db29cb73d Mon Sep 17 00:00:00 2001 From: "Jeremy D. Miller" Date: Thu, 6 Aug 2026 04:29:58 -0500 Subject: [PATCH 2/2] 2.42.1 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index a4f32bfb..92f4715a 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - 2.42.0 + 2.42.1 13 1570;1571;1572;1573;1574;1587;1591;1701;1702;1711;1735;0618 Jeremy D. Miller;Jaedyn Tonee