Repository navigation
Marten 9.45.0
This one is largely the community's release. Four of the six changes below were found, diagnosed, or fixed by people outside the core team, and two of them are bugs that would have been very hard to find from the inside.
Community contributions
@Hawxy — two LINQ regressions (#5556). A captured nullable in a where clause was throwing: a Nullable<T> never survives boxing, so an empty one boxes to null and PropertyInfo.GetValue had no target, breaking the thoroughly ordinary where !captured.HasValue || x.Id == captured. HasValue and Value are now read off the boxed form. The same PR fixes literals in select statements surviving paging, stats, and version select clauses.
@Hawxy — inline-to-async on an empty database (#5561). FetchHighestEventSequenceNumber read mt_events_sequence.last_value, which PostgreSQL reports as 1 with is_called = false before anything has drawn from the sequence. SubscribeAsInlineToAsync used that as its starting position, so on an empty store the shard started at 1 against a high water mark of 0 and the agent failed with "The last committed number (1) cannot be higher than the high water mark (0)". A new store is exactly when you least want that.
@tomasherceg — ordering after a grouped projection (#5557, shipped as #5563). CompileGroupBy transferred the downstream Limit and Offset but never the OrderingExpressions — even though the comment sitting over that block had claimed OrderBy was transferred for as long as it existed. A GroupBy(...).Select(...) that was then ordered came back in whatever order PostgreSQL felt like, with no order by in the SQL and no error, so a paged one took an arbitrary page.
Tomáš found it, diagnosed it, and landed the hard part — re-expressing the ordering against the projected shape rather than the document. He also flagged his own uncertainty about edge cases, which is what sent us looking and turned up two more: a scalar projection (Select(g => g.Key), Select(g => g.Count())) has no projected members, so ordering one threw instead of ordering by its single value; and OrderBySql() reported Invalid OrderBy() expression ''. Both now work. His commit is in the tree with his authorship.
@chrisbbe — the identity probe's side effects (#5562). The best bug report we have had in a while: a minimal repro, the stack trace, the mechanism, and a correct suggested fix.
Identity resolution handed its candidate filter to a traversal that applies it to every property and field, and only afterwards picks the id by [Identity] or by the name Id. That filter is not free to ask — for any public struct shaped like a strong-typed id it registers the type on the global PostgresqlProvider singleton and closes an open generic. So an ordinary non-identity property got remapped process-wide the moment its document or event type was mapped, turning an Optional<string> used only for a nickname into varchar for everything, LINQ included. Under Native AOT the generic construction brought startup down outright with a MissingMethodException.
Both probes now split into a pure shape test and the effectful registration; the traversal asks the cheap question and only the chosen member gets the expensive one.
RegisterValueType fills StoreOptions.ValueTypes but has never registered the Postgres mapping, so a value type used as a duplicated field rather than as an id only ever worked because the probe happened to reach it on some document. If you call opts.RegisterValueType<T>() you were doing the right thing and relying on an accident. That registration now belongs to RegisterValueType.
Performance
Batch reads are one round trip again (#5550). JasperFx 2.79 added LoadManyAsync to IDocumentReadOperations and FetchManyForWriting to IEventStoreOperations, both default-implemented. The defaults are correct, and both make one round trip per id.
Marten has sixteen LoadManyAsync overloads and not one of them takes the token last, so the contract's (ids, token) call matched none of them and silently bound to the default. Nothing failed — the shared compliance suite loads 2,500 documents through it, 2,500 round trips at a time, passing.
| before | now | |
|---|---|---|
LoadManyAsync, 25 ids |
25 round trips | 1 |
LoadManyAsync, 2,500 ids |
2,500 | 1 |
FetchManyForWriting, 10 streams |
10 | 1 |
FetchManyForWriting is new public surface on Marten: one handle per id in the order asked for, a handle (not a gap) for a stream that does not exist yet, and each handle keeping its own starting version so SaveChangesAsync guards every stream it appended to and no other.
::: One lifecycle keeps the sequential shape
An aggregate projected with ProjectionLifecycle.Async is fetched inside begin transaction isolation level repeatable read read only, and PostgreSQL only accepts that as the first statement in a transaction — so two of them cannot share a batch. FetchManyForWriting detects that lifecycle and falls back to fetching sequentially: same results, same ordering, same per-stream guards, no round-trip saving. Inline, Live and Snapshot get the fast path.
:::
See FetchManyForWriting and Loading Documents by Id for the details.
Also
Dependency bumps from Dependabot: fast-uri (#5558), brace-expansion (#5559), dompurify (#5560).