diff --git a/Directory.Packages.props b/Directory.Packages.props
index 27b32f0d05..b69b2c65ce 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -101,14 +101,18 @@
failures are loud, dead consumers throw from Post), and teardown catch clauses classify via
CancellationExceptions.IsCancellationLike instead of discarding every exception type while a
shard CTS is cancelled; jasperfx#505 — source-gen evolver two-statement fix for the
- GetUninitializedObject cast (rides in the bundled JasperFx.Events.SourceGenerator analyzer). -->
-
-
-
+ GetUninitializedObject cast (rides in the bundled JasperFx.Events.SourceGenerator analyzer).
+ JasperFx 2.28.1: jasperfx#528 (marten#4966) — natural-key discovery now builds an event
+ mapping for a two-arg static evolve method (static TDoc Apply(TEvent e, TDoc current)), so a
+ natural key that CHANGES via an update event is written to mt_natural_key_X on live append and
+ rebuild; previously only the create event's key was ever recorded. -->
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/src/DaemonTests/Aggregations/rebuild_projection_with_natural_key_update.cs b/src/DaemonTests/Aggregations/rebuild_projection_with_natural_key_update.cs
new file mode 100644
index 0000000000..59da28a985
--- /dev/null
+++ b/src/DaemonTests/Aggregations/rebuild_projection_with_natural_key_update.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using DaemonTests.TestingSupport;
+using JasperFx.Events;
+using JasperFx.Events.Aggregation;
+using Marten;
+using Marten.Testing.Harness;
+using Shouldly;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace DaemonTests.Aggregations;
+
+public class Bug_4966_natural_key_update_on_projection_rebuild: DaemonContext
+{
+ private const string schemaName = "rebuild_natural_key_update";
+
+ public Bug_4966_natural_key_update_on_projection_rebuild(ITestOutputHelper output) : base(output)
+ {
+ }
+
+ public sealed record ProductCode(string Value);
+
+ public sealed record ProductRegistered(Guid ProductId, string ProductCode);
+
+ public sealed record ProductCodeChanged(Guid ProductId, string NewProductCode);
+
+ public sealed record Product
+ {
+ public Guid Id { get; set; }
+
+ [NaturalKey]
+ public ProductCode Code { get; set; }
+
+ [NaturalKeySource]
+ public static Product Create(ProductRegistered e)
+ {
+ return new Product
+ {
+ Id = e.ProductId,
+ Code = new ProductCode(e.ProductCode)
+ };
+ }
+
+ [NaturalKeySource]
+ public static Product Apply(ProductCodeChanged e, Product product)
+ {
+ return product with
+ {
+ Code = new ProductCode(e.NewProductCode)
+ };
+ }
+ }
+
+ private static void ConfigureStore(StoreOptions opts)
+ {
+ opts.Connection(ConnectionSource.ConnectionString);
+ opts.DatabaseSchemaName = schemaName;
+ opts.Events.StreamIdentity = StreamIdentity.AsGuid;
+ opts.Events.AppendMode = EventAppendMode.Quick;
+ opts.Projections.Snapshot(SnapshotLifecycle.Async);
+ }
+
+
+ [Fact]
+ public async Task bug_4966_natural_key_should_be_updated_during_rebuild()
+ {
+ StoreOptions(ConfigureStore);
+ await theStore.Storage.ApplyAllConfiguredChangesToDatabaseAsync();
+ await theStore.Advanced.Clean.DeleteAllDocumentsAsync();
+ await theStore.Advanced.Clean.DeleteAllEventDataAsync();
+
+ var streamId = Guid.NewGuid();
+ var originalCode = "PROD-001";
+ var newCode = "PROD-999";
+
+ await using (var session = theStore.LightweightSession())
+ {
+ session.Events.StartStream(streamId, new ProductRegistered(streamId, originalCode));
+ await session.SaveChangesAsync();
+ }
+
+
+ await using (var session = theStore.LightweightSession())
+ {
+ session.Events.Append(streamId, new ProductCodeChanged(streamId, newCode));
+ await session.SaveChangesAsync();
+ }
+
+ var daemon = await theStore.BuildProjectionDaemonAsync();
+ await daemon.RebuildProjectionAsync(CancellationToken.None);
+
+ await using (var session = theStore.LightweightSession())
+ {
+ var product = await session.Events.FetchLatest(new ProductCode(newCode));
+ product.ShouldNotBeNull();
+ product.Code.Value.ShouldBe(newCode);
+ }
+ }
+}