Marten 9.24.0
Fixes
Cross-tenant event rewrites under UseTenantPartitionedEvents (#5234)
If you use Events.UseTenantPartitionedEvents together with event masking or stream compaction, read this one.
Under per-tenant event partitioning each tenant draws from its own sequence, so seq_id is not unique across tenants — seq_id = 1 exists in every tenant's partition. Three operations that rewrite mt_events keyed their WHERE on seq_id alone, so while the read side was correctly scoped by ForTenant(...), the write escaped it:
- Masking destroyed an uninvolved tenant's event payload and replaced it with the calling tenant's masked JSON — a cross-tenant write and disclosure, in the right-to-erasure feature.
- Compaction permanently deleted an uninvolved tenant's events and left the calling tenant's whole aggregate state, as
Compacted<T>, sitting in their stream.
Nothing threw in either case. All three operations now carry the tenant predicate.
Damage already written by an earlier version cannot be reversed by this fix — restore the affected tenants from a backup or archival storage. Stores that never enabled UseTenantPartitionedEvents are unaffected, because a single global sequence makes seq_id store-unique there.
Reported with executed failing tests by @arnelirobles.
EF Core inline projection connection leak (#5228)
The EF Core integration's placeholder connection was released only on the success path of BeforeCommitAsync. A projection that threw, an optimistic concurrency failure, or a throw inside the commit hook each stranded a pooled connection — and because an inline multi-stream projection builds its storage for events it will not even process, a workload that merely had one registered leaked on every failed save. Measured at 20 saves: 41 stranded backends before, 2 after. Reported by @markotny.
Compiled query parameters inside Select() (#5233)
Count(predicate) inside a projection is now re-bound per invocation of an ICompiledQuery instead of being frozen at plan time. A projection Marten cannot translate to SQL, which is applied by a delegate compiled once per plan, now throws InvalidCompiledQueryException at plan time when it reads a value off the compiled query instance, rather than silently returning the first invocation's results forever.
Dead letter persistence failures are no longer swallowed (#5229)
IEventDatabase.StoreDeadLetterEventAsync wrapped its whole body in an empty catch. A failed write dropped the only record that a projection skipped an event, with no exception and no log line; a wrong storage argument made the method a silent no-op. Failures are now logged at Error with the projection, shard, sequence and tenant, and a wrong argument throws.
Features
Count(predicate) translated inside Select() projections (#5223)
x.Lines.Count(line => line.IsActive) in a projection is now computed by PostgreSQL rather than by deserializing the whole document on the client. Same translation Where() has used since 9.14.1. This also fixes bare boolean predicates and && compounds in the Where() form — Where(x => x.Lines.Count(l => l.IsActive) == 3) previously threw.
Custom fetch planners (#5226)
StoreOptions.Projections.FetchPlanners is now public, so an application can supply its own IFetchPlanner to take over FetchForWriting() / FetchLatest() for the aggregate types it recognizes.
Opt-in FetchForWriting metrics (#5227)
OpenTelemetryOptions.TrackFetchForWritingMetrics() enables a marten.fetch_for_writing.events_replayed histogram, tagged by aggregate type and fetch plan, so the cost of Live / Async / Inline is directly comparable.
Both contributed by @erdtsieck.