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>
<Version>9.0.1</Version>
<Version>9.0.2</Version>
<LangVersion>13.0</LangVersion>
<Authors>Jeremy D. Miller;Babu Annamalai;Jaedyn Tonee</Authors>
<PackageIconUrl>https://martendb.io/logo.png</PackageIconUrl>
Expand Down
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
<PackageVersion Include="FSharp.Core" Version="9.0.100" />
<PackageVersion Include="FSharp.SystemTextJson" Version="1.3.13" />
<PackageVersion Include="JasperFx" Version="2.0.1" />
<PackageVersion Include="JasperFx.Events" Version="2.1.0" />
<PackageVersion Include="JasperFx.Events.SourceGenerator" Version="2.1.0">
<PackageVersion Include="JasperFx.Events" Version="2.1.1" />
<PackageVersion Include="JasperFx.Events.SourceGenerator" Version="2.1.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageVersion>
Expand Down
8 changes: 7 additions & 1 deletion docs/migration-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,13 @@ public partial class OrderProjection : SingleStreamProjection<Order, Guid>

The same shape applies to `EventProjection` — replace `Project<TEvent>(action)` / `ProjectAsync<TEvent>(action)` with `Project` / `ProjectAsync` method-convention overloads on a `partial` projection class.

The `partial` keyword on the class is what lets the source generator emit a sibling partial declaration with the `[GeneratedEvolver]` dispatcher. Existing convention-based projections that don't currently use `partial` keep working without it (Marten falls back to runtime evolver lookup for those); adding `partial` is what enables the AOT-clean and trim-clean dispatcher path. See [Runtime code generation removed](#runtime-code-generation-removed) for the broader context on Marten 9's source-generated dispatch model.
The `partial` keyword on a **projection subclass** (`SingleStreamProjection<T, TId>`, `MultiStreamProjection<...>`, `EventProjection`) is what lets the source generator emit a sibling partial declaration with the `[GeneratedEvolver]` dispatcher. **Self-aggregating** types registered via `Projections.Snapshot<T>(...)`, `LiveStreamAggregation<T>(...)`, `Projections.Add<SingleStreamProjection<T, TId>>(...)`, or used through `AggregateStreamAsync<T>(...)` / `FetchLatest<T>(...)` do **not** need to be `partial` — the generator emits a free-standing evolver keyed on the aggregate type.

::: warning No runtime reflection fallback
Marten 9 has **no runtime reflection/codegen fallback** for conventional `Apply`/`Create`/`ShouldDelete` methods (this was the whole point of the 9.0 projections rework — see [Runtime code generation removed](#runtime-code-generation-removed)). A projection that uses convention methods **must** have a source-generated dispatcher, or override `Evolve` / `EvolveAsync` / `DetermineAction` / `DetermineActionAsync` directly. If neither is present you get an `InvalidProjectionException: No source-generated dispatcher found ...` at `DocumentStore.For(...)`.

The generator (`JasperFx.Events.SourceGenerator`) ships **inside the `Marten` NuGet package** as an analyzer ([#4557](https://github.com/JasperFx/marten/issues/4557)), so a normal `<PackageReference Include="Marten" />` is enough — you do not need to add the analyzer package yourself. It must, however, run in the assembly that **defines the aggregate type** (the runtime looks up the generated `[GeneratedEvolver]` in `typeof(TAggregate).Assembly`). If you reference Marten with `IncludeAssets`/`ExcludeAssets` that strip `analyzers`, the dispatcher won't be generated.
:::

If you need to delete the aggregate based on async work (the equivalent of the removed `DeleteEventAsync` overload), implement an `async`-returning `ShouldDelete` method that takes an `IQuerySession`:

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using System;
using System.Threading.Tasks;
using JasperFx.Events.Projections;
using Marten;
using Marten.Testing.Harness;
using Marten.Testing.OtherAssembly.Issue4557;
using Shouldly;
using Xunit;

namespace EventSourcingTests.Bugs;

// Reproduction for https://github.com/JasperFx/marten/issues/4557.
//
// A user upgrading 8 -> 9 has a self-aggregating immutable `record` snapshot with
// static Create/Apply, registered via `Projections.Snapshot<MyType>(Inline)`. It
// worked in v8 (runtime codegen) but in v9 fails at store construction with:
//
// JasperFx.Events.Projections.InvalidProjectionException:
// No source-generated dispatcher found for
// Marten.Events.Aggregation.SingleStreamProjection<MyType, System.Guid>...
//
// The aggregate type + the Snapshot<MyType> registration both live in
// Marten.Testing.OtherAssembly, which references Marten but NOT the
// JasperFx.Events.SourceGenerator analyzer. Originally the analyzer never reached a
// consumer at all: Marten referenced it with PrivateAssets=all, so a project that only
// had `<PackageReference Include="Marten" />` never ran the generator, no
// `[GeneratedEvolver]` attribute was emitted, and the runtime scan in
// JasperFxAggregationProjectionBase.tryUseAssemblyRegisteredEvolver found nothing.
//
// Resolution (#4557): Marten's NuGet package now BUNDLES the analyzer in
// analyzers/dotnet/cs, so a real consumer that references only `Marten` gets the
// generator automatically. That packaging fix is validated by packing Marten and
// running the reproduction sample — it cannot be observed through a ProjectReference,
// which never receives a NuGet-bundled analyzer. This test therefore still exercises
// the underlying runtime fail-fast for the case where the analyzer genuinely did not
// run in the aggregate's assembly (e.g. a consumer that strips the `analyzers` asset,
// or the cross-assembly record gap below).
//
// (Defining these same types directly in EventSourcingTests would NOT reproduce,
// because this test assembly references the analyzer explicitly and Pipeline 3 of
// the generator would emit the dispatcher.)
public class Bug_4557_self_aggregating_snapshot_without_source_generator
{
// Pins the runtime fail-fast: when no source-generated dispatcher is present in the
// aggregate's assembly, registration throws at DocumentStore.For(...) — before any
// database access — rather than failing later at first event dispatch.
[Fact]
public void snapshot_without_source_generator_throws_at_registration()
{
var ex = Should.Throw<JasperFx.Events.Projections.InvalidProjectionException>(() =>
{
using var store = DocumentStore.For(opts =>
{
opts.Connection(ConnectionSource.ConnectionString);
Issue4557Registration.Configure(opts);
});
});

ex.Message.ShouldContain("No source-generated dispatcher found");
}

// The end-to-end round trip a real consumer expects. This passes once the events
// source generator runs in this aggregate's assembly — which is exactly what the
// #4557 packaging fix does for package consumers (Marten now bundles the analyzer).
// It stays skipped here because Marten.Testing.OtherAssembly is a *ProjectReference*
// consumer and never receives a NuGet-bundled analyzer; the package-consumer path is
// validated by packing Marten and running the reproduction sample instead. Un-skip
// if/when OtherAssembly is given the analyzer or the cross-assembly generator change
// lands.
[Fact(Skip = "Validated via packaging (see PR); ProjectReference consumers don't receive a NuGet-bundled analyzer.")]
public async Task snapshot_without_source_generator_should_round_trip()
{
using var store = DocumentStore.For(opts =>
{
opts.Connection(ConnectionSource.ConnectionString);
opts.DatabaseSchemaName = "issue4557";
Issue4557Registration.Configure(opts);
});

await store.Advanced.Clean.CompletelyRemoveAllAsync();

var streamId = Guid.NewGuid();
await using (var session = store.LightweightSession())
{
session.Events.StartStream<MyType>(streamId,
new MyTypeCreated(streamId, "test"),
new MyTypeIncremented(1));
await session.SaveChangesAsync();
}

await using (var query = store.QuerySession())
{
var snapshot = await query.LoadAsync<MyType>(streamId);
snapshot.ShouldNotBeNull();
snapshot.Name.ShouldBe("test");
snapshot.Count.ShouldBe(1);
snapshot.LastEvent.ShouldBe(nameof(MyTypeIncremented));
}
}
}
50 changes: 50 additions & 0 deletions src/Marten.Testing.OtherAssembly/Issue4557/Issue4557Snapshot.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System;
using JasperFx.Events.Projections;
using Marten;

namespace Marten.Testing.OtherAssembly.Issue4557;

// Reproduction types for https://github.com/JasperFx/marten/issues/4557.
//
// These deliberately live in Marten.Testing.OtherAssembly because that project
// references Marten via ProjectReference but does NOT reference the
// JasperFx.Events.SourceGenerator analyzer (Marten hides it with
// PrivateAssets=all, so it never flows to a consumer). That mirrors a real
// consumer app that only does `<PackageReference Include="Marten" />`.
//
// MyType is a self-aggregating immutable record with static Create/Apply,
// registered via `Snapshot<MyType>(Inline)` — exactly the user's sample. Note
// it is intentionally NOT `partial`: self-aggregating types registered through
// Snapshot<T> do not require partial (that requirement is only for projection
// subclasses). The breakage is purely that no source-generated dispatcher is
// emitted in this analyzer-free assembly.
public static class Issue4557Registration
{
public static void Configure(StoreOptions options)
{
options.Projections.Snapshot<MyType>(SnapshotLifecycle.Inline);
}
}

public record CreateMyType(Guid Id, string Name);

public record MyTypeCreated(Guid Id, string Name);

public record MyTypeIncremented(int Amount);

public record MyType(Guid Id, string Name, int Count, string LastEvent)
{
public static MyType Create(MyTypeCreated @event)
{
return new MyType(@event.Id, @event.Name, 0, nameof(MyTypeCreated));
}

public static MyType Apply(MyTypeIncremented @event, MyType current)
{
return current with
{
Count = current.Count + @event.Amount,
LastEvent = nameof(MyTypeIncremented)
};
}
}
26 changes: 25 additions & 1 deletion src/Marten/Marten.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
<!-- FSharp.Core removed: F# support uses reflection via FSharpTypeHelper -->
<PackageReference Include="JasperFx" />
<PackageReference Include="JasperFx.Events" />
<PackageReference Include="JasperFx.Events.SourceGenerator">
<PackageReference Include="JasperFx.Events.SourceGenerator" GeneratePathProperty="true">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand All @@ -45,6 +45,30 @@
<PackageReference Include="Weasel.Postgresql" />
</ItemGroup>

<!-- #4557: Bundle the JasperFx.Events.SourceGenerator analyzer into Marten's own
NuGet package (analyzers/dotnet/cs) so it is applied to every consuming project
automatically. Marten 9 requires a source-generated evolver for conventional
Apply/Create/ShouldDelete projections — there is no runtime fallback — but the
generator ships as a DevelopmentDependency that does NOT flow transitively, so a
consumer that only references the Marten package never ran it and hit
"No source-generated dispatcher found ..." at DocumentStore.For(). Carrying the
analyzer DLL in Marten's package is the self-contained fix. -->
<PropertyGroup>
<TargetsForTfmSpecificContentInPackage>$(TargetsForTfmSpecificContentInPackage);_BundleEventsSourceGeneratorAnalyzer</TargetsForTfmSpecificContentInPackage>
</PropertyGroup>

<!-- Runs per-TFM (where the $(Pkg...) path property is resolved, unlike the outer
pack evaluation). The analyzer is TFM-agnostic and goes to analyzers/dotnet/cs,
so it must be contributed exactly once — guard on a single TFM to avoid a
duplicate-package-file conflict. -->
<Target Name="_BundleEventsSourceGeneratorAnalyzer"
Condition="'$(TargetFramework)' == 'net9.0' And '$(PkgJasperFx_Events_SourceGenerator)' != ''">
<ItemGroup>
<TfmSpecificPackageFile Include="$(PkgJasperFx_Events_SourceGenerator)\analyzers\dotnet\cs\JasperFx.Events.SourceGenerator.dll">
<PackagePath>analyzers/dotnet/cs</PackagePath>
</TfmSpecificPackageFile>
</ItemGroup>
</Target>

<Import Project="../../Analysis.Build.props" />
</Project>
Loading