Repository navigation
9.41.0
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 onIIdentification<TDoc, TId>, so existing strategies are unaffected.Weasel.Core.Identity.Identifications— the sevenFor…factory methods, each building a strategy
from runtimeTypes.Weasel.Core.Identity.ReflectedValueTypeIdentification— the strong-typed id strategy with no
closed generic and no compiled expression. Handed out byForValueTypeunder Native AOT; also
constructible directly if you want the reflective path on purpose.
Verification
Weasel.Core.AotSmokenow exercisesIdentifications.ForValueTypeunderIsAotCompatible,
TrimMode=fulland the IL codes promoted to errors, so IL3050 at that call site fails CI. It was
also published withPublishAot=trueand 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.