diff --git a/Directory.Packages.props b/Directory.Packages.props index ffe62e0d8b..e133e56a66 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -102,13 +102,13 @@ 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). --> - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/docs/events/projections/event-projections.md b/docs/events/projections/event-projections.md index e9cfbf3ee7..4a7f5ad918 100644 --- a/docs/events/projections/event-projections.md +++ b/docs/events/projections/event-projections.md @@ -137,6 +137,30 @@ The `Project()` methods can accept these arguments: The return value must be either `void` or `Task` depending on whether or not the method needs to be asynchronous +## Rebuilds and Document Teardown + +::: warning +Because an `EventProjection` writes documents through *ad-hoc* `IDocumentOperations` calls (`ops.Insert`, `ops.Store`, etc.) inside its `Project()` / `Create()` methods, Marten **cannot infer which document types the projection produces**. Unlike a single-stream or multi-stream aggregation (whose view type is known), an `EventProjection` therefore does **not** automatically wipe its documents when the projection is rebuilt. + +If you rebuild an `EventProjection` and want the previously-written documents cleared first (the normal rebuild semantics), **declare each written document type explicitly** so it participates in the rebuild teardown: + +```cs +public class MyProjection: EventProjection +{ + public MyProjection() + { + // Tell Marten this projection writes MyDoc, so a rebuild TRUNCATEs it first + Options.DeleteViewTypeOnTeardown(); + } + + public void Project(SomethingHappened e, IDocumentOperations ops) + => ops.Insert(new MyDoc { Id = e.Id /* ... */ }); +} +``` + +Without this declaration the old documents survive the rebuild. The per-row rebuild path tolerates that (re-inserting an existing id upserts), but the INSERT-only [binary `COPY` rebuild](/events/projections/rebuilding) (`Projections.RebuildWithBulkCopy`) requires an empty target table and will fail with a duplicate-key error. This is a long-standing `EventProjection` limitation, not specific to bulk-copy rebuilds. +::: + ## Identifying the Event Parameter In both the `Create()` and `Project()` conventions above, the event parameter can be named anything — Marten identifies it **by type**, not by name. Given `Project(StopEvent1 e, IDocumentOperations ops)`, `StopEvent1` is the event because it's the only concrete event type in the signature (`IDocumentOperations` is an interface and is never treated as the event). The same applies to `IEvent`, which is always recognized as the event regardless of the parameter name. diff --git a/src/DaemonTests/rebuild_with_bulk_copy_inserts.cs b/src/DaemonTests/rebuild_with_bulk_copy_inserts.cs index b4271d4243..2dd3288c79 100644 --- a/src/DaemonTests/rebuild_with_bulk_copy_inserts.cs +++ b/src/DaemonTests/rebuild_with_bulk_copy_inserts.cs @@ -463,6 +463,12 @@ public partial class BulkCopyInsertProjection: EventProjection public BulkCopyInsertProjection() { Name = ProjectionName; + + // EventProjection cannot infer the document types it writes through ad-hoc ops.Insert/ops.Store, + // so it does not know to wipe them on rebuild. Declare the written type explicitly so the rebuild + // teardown TRUNCATEs it first — otherwise the INSERT-only binary COPY rebuild collides with docs a + // prior run left behind (duplicate key). This is a long-standing EventProjection limitation. + Options.DeleteViewTypeOnTeardown(); } public void Project(ItemAdded e, IDocumentOperations ops) @@ -480,6 +486,10 @@ public partial class InsertAndDeleteProjection: EventProjection public InsertAndDeleteProjection() { Name = ProjectionName; + + // See BulkCopyInsertProjection: an EventProjection must declare the doc types it writes ad-hoc so the + // rebuild teardown clears them. + Options.DeleteViewTypeOnTeardown(); } public void Project(ItemAdded e, IDocumentOperations ops)