Skip to content

9.41.0

Choose a tag to compare

@jeremydmiller jeremydmiller released this 04 Oct 22:30
· 9 commits to master since this release
f13ed62

Weasel 9.41 makes the identity runtime reachable from a consumer that holds Types rather than type
arguments — which is what a Native AOT host has to be.

Nothing in this release is a breaking change. Two additions, both in Weasel.Core.Identity, both
additive. Nothing public was removed and no existing code path changed behaviour.

Full notes: https://weasel.jasperfx.net/release-9-41

Why

Type.MakeGenericType can close an instantiation whose type arguments are all reference types,
because those share one canonical body. It cannot close one with a value-type argument — that
needs native code ILC never generated. A consumer whose document metadata is Type-based (Marten's
ProviderGraph, Polecat's DocumentMapping) had no choice but to close Weasel's identity generics
at runtime, and an id type is routinely Guid, int or long. So a natively published store could
not be built at all.

Reflection is not a way around it, which is worth recording because it looks like one: holding the
strategy as object and invoking through MethodInfo fails differently, because GetMethods()
comes back empty — nothing statically references those members, so the trimmer took their metadata
too.

Using a strategy without naming its type arguments — #689

Weasel.Core.Identity.IIdentification, a non-generic facade that IIdentification<TDoc, TId>
satisfies:

public interface IIdentification
{
    object Identity(object document);
    object AssignIfMissing(object document, ISequenceSource sequences);
    object ToRawSqlValue(object id);
    Type RawSqlType { get; }
    object ReadIdFromReader(DbDataReader reader, int columnOrdinal);
}

If you implement IIdentification<TDoc, TId> yourself, you do not have to change anything. Every
facade member is a default implementation on the generic interface that forwards to the generic
member, so a type already compiled — including one already published and loaded against this release
by NuGet resolving Weasel up on its own — still loads.

That detail is load-bearing rather than tidy. An abstract member instead would have been a runtime
break that no amount of building or restoring reveals beforehand, which is exactly what 9.40.0
shipped and #682 caught. Verified here the only way that counts: against the published Marten
9.45.0 and Polecat 5.35.0 with this Weasel.Core resolved up, forcing the interface map the way a host
does on first load.

Every member boxes, by construction. If you can name your type arguments, keep using
IIdentification<TDoc, TId> — it allocates nothing on the path where a document already has an id.

Building a strategy without closing a generic — #690

Weasel.Core.Identity.Identifications is a non-generic entry point for all seven strategies:
ForSequentialGuid, ForRandomGuid, ForHiloInt, ForHiloLong, ForIdentityKey,
ForExternallyAssignedString and ForValueType.

Six of them close a generic over the document type alone, which is a class, so they already worked
natively. ValueTypeIdentification<TDoc, TWrapper, TInner> — strong-typed ids, Vogen /
StronglyTypedId wrappers, F# single-case DUs — did not: TInner is the wrapped primitive and
TWrapper is usually a readonly record struct, so two of three arguments are value types. The
issue predicted this rather than observing it; it is now measured, on macOS arm64 / ILC / net9.0:

IsDynamicCodeSupported = False
SequentialGuidIdentification<Doc>: constructed OK
ValueTypeIdentification<Doc,FooId,Guid>: NotSupportedException:
  'Weasel.Core.Identity.ValueTypeIdentification`3[Doc,FooId,System.Guid]'
  is missing native code or metadata.

Identifications.ForValueType chooses:

  • Where dynamic code is available — every process that is not natively published, which is most
    of them — it closes the generic exactly as a caller used to. The hot path stays an FEC-compiled
    delegate. Nothing gets slower.
  • Under Native AOT it returns the new ReflectedValueTypeIdentification, which closes no generic
    and compiles no expression.

The branch is on RuntimeFeature.IsDynamicCodeSupported, a published feature switch that ILC
substitutes to false and then trims — so an AOT build neither warns about the MakeGenericType it
will never reach nor carries it. Catching NotSupportedException instead would do neither, and would
swallow real failures.

What this deliberately does not do

There is no mapping from (documentType, idType) to a strategy. Which strategy fits an id is a
store's policy, not a fact about the id: the same Guid is a sequential id in one configuration and
a caller-assigned one in another, and an int may be Hi-Lo or externally assigned. Marten and
Polecat do not agree, so a mapping in Weasel would be a guess wearing a library's authority. Weasel
owns the construction; the store keeps the choice.

New public API

  • Weasel.Core.Identity.IIdentification — the non-generic facade. Satisfied by default
    implementations on IIdentification<TDoc, TId>, so existing strategies are unaffected.
  • Weasel.Core.Identity.Identifications — the seven For… factory methods, each building a strategy
    from runtime Types.
  • Weasel.Core.Identity.ReflectedValueTypeIdentification — the strong-typed id strategy with no
    closed generic and no compiled expression. Handed out by ForValueType under Native AOT; also
    constructible directly if you want the reflective path on purpose.

Verification

  • Weasel.Core.AotSmoke now exercises Identifications.ForValueType under IsAotCompatible,
    TrimMode=full and the IL codes promoted to errors, so IL3050 at that call site fails CI. It was
    also published with PublishAot=true and run end to end on net9.0 and net10.0 — the analyzer
    cannot prove the runtime half.
  • The two strong-typed id implementations are held against each other case for case: Guid / int /
    long / string inner types, already-assigned ids, Nullable<wrapper> properties, ToRawSqlValue,
    RawSqlType, ReadIdFromReader.

Issues: #689, #690 · Pull requests: #691, #692