Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<PropertyGroup>
<JasperFxVersion>2.42.0</JasperFxVersion>
<JasperFxVersion>2.42.1</JasperFxVersion>
<LangVersion>13</LangVersion>
<NoWarn>1570;1571;1572;1573;1574;1587;1591;1701;1702;1711;1735;0618</NoWarn>
<Authors>Jeremy D. Miller;Jaedyn Tonee</Authors>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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]
Expand All @@ -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]
Expand All @@ -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<global::System.Type> 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<Type> 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<global::System.Type> 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 <T>.EventProjection.g.cs rather than <T>.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)");
}
}
38 changes: 35 additions & 3 deletions src/JasperFx.Events.SourceGenerator/AggregateAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,14 @@ internal sealed class CandidateInfo
public List<EventConstructorInfo> EventConstructors { get; set; } = new();
public bool HasDefaultConstructor { get; set; }
public bool HasExistingParameterlessConstructor { get; set; } // On the projection class itself

/// <summary>
/// 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.
/// </summary>
public bool HasExistingPublishedTypesOverride { get; set; }

// EventProjection-specific
public INamedTypeSymbol? OperationsType { get; set; } // TOperations from JasperFxEventProjectionBase<TOperations, TQuerySession>
/// <summary>
Expand Down Expand Up @@ -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)
};
}

Expand Down Expand Up @@ -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)
};
}

Expand All @@ -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)
};
}

Expand Down Expand Up @@ -1553,6 +1564,27 @@ private static bool HasExplicitParameterlessConstructor(INamedTypeSymbol type)
!c.IsImplicitlyDeclared);
}

/// <summary>
/// Does this projection, or a user-written base class beneath ProjectionBase, already override
/// <c>ProjectionBase.PublishedTypes()</c>? The generator defers to it rather than emitting a
/// competing override. See marten#5192.
/// </summary>
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;
}

/// <summary>
/// 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
Expand Down
63 changes: 40 additions & 23 deletions src/JasperFx.Events.SourceGenerator/EvolverCodeEmitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -573,7 +573,7 @@ public static string EmitEventProjectionPartial(CandidateInfo info)
}

/// <summary>
/// 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
Expand Down Expand Up @@ -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("}");

Expand All @@ -646,31 +637,57 @@ private static List<INamedTypeSymbol> GetContainingTypes(INamedTypeSymbol type)
}

/// <summary>
/// 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
/// </summary>
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"))
.Select(m => Fqn(m.EntityReturnType!))
.Distinct()
.ToList();

EmitPublishedTypesOverride(sb, info, publishedTypes);
}

/// <summary>
/// Declares the discovered published document types by overriding
/// <c>ProjectionBase.PublishedTypes()</c> in the user's partial class.
///
/// <para>This used to be emitted as a parameterless constructor calling
/// <c>RegisterPublishedType</c>, which was wrong on two counts (marten#5192). A constructor is
/// illegal on a type that declares a primary constructor —
/// <c>partial class MyProjection(ILogger logger) : EventProjection</c> — 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
/// <c>AddProjectionWithServices&lt;T&gt;</c>, which is precisely what a projection taking an
/// <c>ILogger</c> 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.</para>
///
/// <para>An override is immune to both: it does not care how the instance was constructed, and
/// chaining through <c>base.PublishedTypes()</c> keeps hand-written <c>RegisterPublishedType</c>
/// calls and <c>Options.StorageTypes</c> flowing. Skipped when the author already wrote their
/// own override — theirs wins.</para>
/// </summary>
private static void EmitPublishedTypesOverride(StringBuilder sb, CandidateInfo info, List<string> 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<global::System.Type> PublishedTypes()");
sb.AppendLine(" {");
sb.AppendLine(" var publishedTypes = new global::System.Collections.Generic.List<global::System.Type>(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();
}
Expand Down
Loading