Repository navigation
Marten 9.49.0
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 DTOSelect(x => x.Lines)— a collection member, and the element type of aSelectMany(x => x.Lines)Select(x => x.Amount)for every framework scalar type,decimal?and the other nullable ones includedSelect(x => x.Status)andMaxAsync(x => x.Status)for an enum of your own — see the one line belowGroupBy(...).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 JITOnly 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/StreamPagedByCursorencode 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 throughJsonSerializer'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.