Skip to content

Marten 9.49.0

Choose a tag to compare

@jeremydmiller jeremydmiller released this 11 Oct 23:34
· 22 commits to master since this release

The LINQ projection surface works under Native AOT

The theme of this release. #5624 came in as "a projection into an anonymous type cannot be
materialized under Native AOT". Publishing Marten.AotRuntimeSmoke natively and running it showed
something much wider: every Select() projection into a new object failed from a native binary,
a named DTO exactly as hard as an anonymous one
, and it failed before the serializer was ever
reached.

MissingMethodException: No parameterless constructor defined for type
'Marten.Linq.SqlGeneration.SelectDataSelectClause`1[YourDto]'

A projection is parsed out of an expression tree, so the parser only has selectExpression.Type and
closed its select clause over that Type with MakeGenericType + Activator.CreateInstance — the
defect class of #5328, one step later in the pipeline. Only a bare string scalar survived, because
its clause happens not to be generic, which is what made the surface look healthy.

Shapes that threw from a native binary before this release and run now:

  • Select(x => new Summary { ... }) — a named DTO
  • Select(x => x.Lines) — a collection member, and the element type of a SelectMany(x => x.Lines)
  • Select(x => x.Amount) for every framework scalar type, decimal? and the other nullable ones included
  • Select(x => x.Status) and MaxAsync(x => x.Status) for an enum of your own — see the one line below
  • GroupBy(...).Select(g => new Tally { ... })

The fix is SelectClauseResolvers, which does for projected types what #5328's
DocumentStorageResolvers does for document types: a registry populated from
MartenLinqQueryable<T>'s constructor, which every projected type reaches because
Queryable.Select<TSource, TResult>() asks the provider for CreateQuery<TResult>(). The reflective
path remains the fallback wherever a JIT is available, so nothing changes for JIT-mode apps.

One line for an enum or struct of your own

Marten's scalar select clause is declared where T : struct and the registration point above
constrains its own T to notnull, so Marten can root the scalar types it renders itself —
decimal, Guid, DateOnly and the rest need nothing — but not one out of your assembly:

opts.Schema.For<Order>();
opts.RegisterScalarType<OrderStatus>();   // new in 9.49.0; no-op under a JIT

Only bare scalar projections need it. Projecting the same enum as a member of a named type, and
filtering on it, never did. When the registration is missing, the failure now names that exact call
instead of reporting a missing parameterless constructor on an internal generic.

An anonymous projection is refused with the remedy

With a source-generated JsonSerializerContext assigned to
JsonSerializerOptions.TypeInfoResolver, the reflection-based resolver is replaced, and an anonymous
type then has no JsonTypeInfo anywhere — and cannot be given one, because [JsonSerializable]
needs a nameable type. This is not AOT-specific: a JIT process with the same configuration fails
identically. System.Text.Json's own error tells you to annotate a type you cannot annotate, so
Marten now replaces it:

MartenNotSupportedException: Cannot materialize the LINQ projection into the anonymous type
'<>f__AnonymousType0`2[System.String,System.Int32]' because MyApp.MyJsonContext has no
JsonTypeInfo for it, and an anonymous type cannot be named by a [JsonSerializable] attribute.
Either project into a named type that IS registered with your JsonSerializerContext, or layer
the context with SystemTextJsonSerializer.UseTypeInfoResolver(context) rather than assigning
JsonSerializerOptions.TypeInfoResolver, which keeps a reflection-based resolver behind it.

UseTypeInfoResolver — the layering API documented under
Source-Generated Metadata — is verified to materialize an
anonymous projection from a native binary, so it is a real way out and not only a JIT one.

Still JIT-only, and now documented as such

A reference-typed scalar from raw SQL (QueryAsync<byte[]>("select ...")) reaches a
where T : class clause that no registration can close. Every value type on that path is covered.

Also in this release

  • #5630 — ToJsonPageByCursorAsync / StreamPagedByCursor encode keyset cursors with the
    store's own serializer. A sort key only that serializer knows how to write (a NodaTime
    LocalDate, say) used to round-trip through JsonSerializer's defaults, come back as
    0001-01-01, and leave the seek predicate matching every row — so every page after the first
    repeated the first one. A cursor element that is a JSON object or array is now refused up front,
    so client-supplied metadata never reaches the store's serializer.

Upgrading

No behaviour changes for JIT-mode applications: every fix here either adds a compile-time
registration or replaces an exception message with a more useful one on a path that already threw.
docs/configuration/aot-publishing.md is updated throughout, including which serializer wiring to
prefer and what each choice costs.

Issues: #5624, #5635, #5630. PRs: #5634, #5636, #5630.