Skip to content

Marten 9.32.1

Choose a tag to compare

@jeremydmiller jeremydmiller released this 06 Sep 19:20
· 170 commits to master since this release

A bug-fix release, on JasperFx 2.66.0 and Weasel 9.31.0. The headline is that Native AOT document reads worked for nobody before this; the rest is correctness work on natural keys, the outbox, EF Core projections, and flat tables.

Several of these change observable behaviour. Each one is called out below.

Native AOT: reading a document from a native binary (#5328)

An app published with PublishAot=true built and published analyzer-clean, then threw on the first document read. Writes — including schema creation — were fine. Three separate sites, all on the read path:

MissingMethodException: No parameterless constructor defined for type
'Marten.Internal.Sessions.QuerySession+StorageFinder`1[MyDocument]'
PlatformNotSupportedException: Dynamic code generation is not supported on this platform.
   at FastExpressionCompiler.ExpressionCompiler.CompileFast[R](...)
   at Marten.Linq.Parsing.LinqInternalExtensions.ReduceToConstant(Expression)

QuerySession.StorageFor(Type) and CompiledQueryPlan.sortMembers() both went from a runtime Type back to a closed generic through MakeGenericType + Activator.CreateInstance, and ILC has no native code for an instantiation nothing constructs statically. The third — expression compilation for every non-literal value in a where clause, i.e. every where x.Member == captured — is Reflection.Emit, which native AOT does not have at all.

All three now capture the closed generic where the compiler has already emitted it. LoadAsync, LINQ, raw SQL, Include, patching and source-generated compiled queries all run from a native binary.

Two shapes still need a JIT, and both have a one-line workaround (documented in the AOT publishing guide): a compiled query with an enum-typed parameter, and a where clause whose value comes from a method call — hoist the call into a local first.

Marten also ships a new runtime gate, src/Marten.AotRuntimeSmoke, which publishes natively and runs every read path against a real PostgreSQL in CI. The existing Marten.AotSmoke is a build-time analyzer gate and was structurally blind to this class of defect: the offending code analyzes clean and throws one line later. If you publish AOT, do the same in your own pipeline — a clean publish is necessary, not sufficient.

Natural keys: three fixes, two of them behavioural (#5344)

The first run of the shared NaturalKeyCompliance suite against Marten found three genuine bugs in the [NaturalKey] lookup table.

⚠️ Behavior change. A second stream claiming a natural key a live stream already holds is now refused with JasperFx.Events.DuplicateNaturalKeyException, carrying both ExistingStreamId and ClaimingStreamId. Previously the claim silently succeeded and repointed the lookup row at the newcomer. The refusal is in the SQL rather than a pre-flight read, so concurrent claimants serialize on the row lock ON CONFLICT already takes and the existing mapping is never repointed even momentarily. Re-asserting your own mapping is still idempotent, and a key held by an archived stream is still claimable.

If two live streams in your database already share a key — which older Marten permitted, because the upsert repointed — the rebuild path deliberately stays last-writer-wins so a pre-existing data condition cannot turn into an unrecoverable daemon failure.

⚠️ Behavior change. An archived stream no longer resolves by its natural key. ArchiveStream left the lookup row intact, so an archived stream stayed resolvable indefinitely. The flag is now read off the mt_streams row the lookup query already joins, which covers the explicit call, the Archived marker event, and the daemon alike. No schema change and no migration of existing lookup tables.

A [NaturalKey] of primitive type string on a Guid-identity store now works. FindFetchPlan asserted string stream storage for any string TId and refused with "This Marten event store is configured to identify streams with Guids" before any planner saw it. Both that guard and its mirror image in NaturalKeyFetchPlanner now narrow to the store's own stream identity type. The ambiguous case is unchanged by design: on a string-identity store, FetchLatest<T, string>(value) still addresses the stream.

A failed unit of work no longer commits its outbox batch (#5353)

IMessageBatch.BeforeCommitAsync was called before the UpdateBatch was built and before a transaction was even open — so it fired for units of work that had not been attempted, let alone succeeded. A SaveChangesAsync that then failed (a stream-id collision, say) left the message batch enlisted and committed, with no rollback hook able to un-enlist it.

The batch is now enlisted as an ITransactionParticipant, invoked inside the transaction after every page has executed and immediately before the COMMIT. A unit of work that fails never reaches it. Hook ordering and the inside/outside-the-transaction visibility contract are unchanged; the before hook simply runs later in the transaction. This is the placement Polecat already used.

EF Core projections stop generating a Marten document table (#5351, #5329)

Registering an EfCoreSingleStreamProjection / EfCoreMultiStreamProjection made Marten build a DocumentMapping for the aggregate type and emit schema for it, even though EF Core owns that data in a DbContext-mapped table Marten never reads or writes. Two consequences, both fixed:

  • ApplyAllConfiguredChangesToDatabaseAsync() created an empty mt_doc_<tdoc> table nothing used — and in the normal AutoCreate.None shape, where schema is built by a migration pass that does not register the projection, AssertDatabaseMatchesConfigurationAsync() then reported that table as MISSING and a correctly-migrated database failed its own startup assertion.
  • An EF entity keyed on anything not named Id threw InvalidDocumentException from every full-schema operation, including the db-apply / db-patch CLI commands. EF Core is perfectly happy with a non-Id key; Marten's document conventions are not, and the type should never have reached them.

Separately (#5329), rebuilding an EF Core projection tried to truncate that non-existent Marten document table. Teardown now clears the EF Core table the DbContext actually maps.

ℹ️ An mt_doc_<tdoc> table created by an earlier version is left in place — Marten now skips schema generation for the type rather than dropping anything. Drop it by hand if you want it gone.

Flat-table Increment(column) counts the creating event (#5342, #5341)

⚠️ Behavior change. IncrementMap.ToInsertExpression returned 0, so the event that created the row was never counted and the column read N-1 after N events. Polecat, Fisher and the lifted Weasel.Storage.Flattened DSL all insert 1, and the maintainer ruling on #5341 is that 1 is correct and Marten was wrong. Rows created after this ships read one higher; existing rows are untouched. If downstream reporting compensated for the old value, it needs to stop.

Deprecations

Marten.Exceptions.ApplyEventException is now [Obsolete] (#5337). Since the async daemon moved to JasperFx.Events, apply failures throw JasperFx.Events.Daemon.ApplyEventException and nothing in Marten throws the Marten copy any longer. It still exists so existing catch blocks compile through 9.x; catch the JasperFx.Events type instead. It will be removed in Marten 10.

Marten's own six event exception types (UnknownEventTypeException, NonExistentStreamException, ExistingStreamIdCollisionException, EventDeserializationFailureException, StreamLockedException, DefaultTenantUsageDisabledException) now have canonical versions in JasperFx.Events 2.64.0 (#5346). Marten's copies remain the ones thrown and are aliased assembly-wide, so catching them by name is unaffected; code that wants the shared type fully-qualifies it.

Housekeeping

Dependency adoptions: JasperFx 2.64.0 → 2.66.0 and Weasel 9.31.0 (#5343, #5347, #5357). Compliance enrollment continued through waves 13 and 14 (#5336, #5343). The #4821 closed-shape event-storage extraction was assessed complete and its codegen-era leftovers retired (#5339), along with local duplicates of types since lifted to shared packages (#5337). No public API was added or removed in this release.

🤖 Generated with Claude Code