Skip to main content

Native AOT and trimming

Publishing with trimming or Native AOT removes code and metadata that is only reached through reflection. aweXpect needs reflection for equivalency and for recording events, so a source generator that ships with the aweXpect package generates the registrations they need when your project is compiled.

Equivalency​

Equivalency has to know the members of the compared types. Reflection provides them under the JIT, but publishing with trimming or Native AOT enabled removes members that are only reached reflectively, so a comparison would silently verify less than it claims to.

A source generator that ships with the aweXpect package closes this gap: for every call site that passes a value to IsEquivalentTo, IsNotEquivalentTo, AreEquivalentTo or switches to Equivalent(), it registers the public fields and properties of the argument's type, of the subject's type and of every type reachable through their members. The registration runs when your assembly is loaded and needs no configuration. A type that has a registration is compared through it, every other type is reflected over under the JIT and fails with an error naming the fix where reflection is switched off, as described below. A type without any comparable member fails loudly instead of passing without verifying anything. The registration also feeds the failure message: a registered object is rendered from its registered members, so the message keeps listing them after trimming, while an unregistered object is rendered as { *unregistered* } where reflection is switched off. Either way the message lists public instance members only, without static members, indexers or properties that lack a public getter.

Types the generator cannot see, and how reflection is switched off

Some types cannot be seen by the generator, because it works from the types declared in your source:

  • a member declared as object, an interface or a base type only reveals the declared type; the instance it holds at runtime is compared through reflection,
  • a private, protected or file-local type cannot be referenced by generated code and is compared through reflection,
  • a value that reaches the comparison through your own extension method is only registered if the extension's parameter or type parameter carries [RequiresMemberMetadata],
  • an anonymous type with a member holding a collection of anonymous types, other than an array, cannot be written as an instance and is compared through reflection; the element type itself is registered.

A type the generator merely did not see, such as the runtime type behind an object member or a value passed through an unmarked extension, can be named explicitly to register it anyway:

using aweXpect.Core.Metadata;

[assembly: GenerateMetadata(typeof(Track))]

A type the generated code cannot reference stays on reflection regardless, and the generator warns with aweXpect2001 when a named type yields no registration.

The registration needs ModuleInitializerAttribute and C# 9, so nothing is generated for a project that targets .NET Framework or .NET Standard 2.0 unless it polyfills the attribute. Those targets cannot be trimmed or published with Native AOT and keep using reflection.

The walk follows every member type the comparison would visit, including framework types. A member of type Exception, for example, registers the types reachable from its properties, because reflection would compare them too. Members whose getter is marked with RequiresUnreferencedCode or RequiresDynamicCode cannot be registered, so their type stays on the reflection path.

Reflection over a type without a registration is switched off when you publish with trimming or Native AOT enabled, because the trimmer removes members that only reflection reaches, and a comparison would silently verify less than it claims to. Such a comparison fails with an error that names the type and asks you to register it. The same applies to a comparison that requests IncludeMembers.Internal, because only public members are registered. The aweXpect.ReflectionFallback.IsSupported runtime switch forces the fallback either way, and the AweXpectReflectionFallback property of your project sets that switch:

<PropertyGroup>
<AweXpectReflectionFallback>true</AweXpectReflectionFallback>
</PropertyGroup>

With the fallback forced on, a trimmed application reflects over whatever the trimmer left, which is best effort: a type whose members were all removed still fails with an error that asks you to root it, but a type that lost only some of them is compared through the rest.

Events​

A recording has to know the events of its subject and attach a handler to each of them. Reflection provides both under the JIT, but publishing with trimming or Native AOT enabled removes events that are only reached reflectively, and the handler for an event with value-type parameters cannot be bound without runtime code generation.

The source generator that ships with the aweXpect package closes this gap: for every call site of Record(), it registers the public events of the subject's static type together with a handler factory, so the recording neither looks the events up nor binds a handler reflectively. The registration runs when your assembly is loaded and needs no configuration. A subject whose runtime type has a registration is recorded through it, every other subject is reflected over as before, and a registered handler takes any number of parameters.

Types the generator cannot see, and how reflection is switched off

The generator works from the declared type, so the same limits apply as for equivalency:

  • a subject declared as an interface, an abstract class or a base type only reveals the declared type; the recording looks up the runtime type of the instance, which stays on reflection,
  • a private, protected or file-local type cannot be referenced by generated code and is reflected over,
  • a subject that reaches Record() through your own extension method is only registered if the extension's parameter or type parameter carries [RequiresEventMetadata],
  • an event whose handler returns a value or takes a parameter by reference cannot be recorded by the reflective path either, and keeps its type on reflection,
  • a struct subject is never registered, because a handler added to a boxed copy never sees the caller's value.

A type the generator did not see can be named explicitly with [assembly: GenerateMetadata(typeof(MyClass))], which registers its members and its events.

Reflection over a subject without a registration is switched off when you publish with trimming or Native AOT enabled, because the trimmer removes events that only reflection reaches and the reflective recorder needs runtime code generation. Recording such a subject fails with an error that names the type and asks you to register it. The aweXpect.ReflectionFallback.IsSupported runtime switch forces the fallback either way, as described for equivalency; with the fallback forced on, the error for an unknown event name, and for an event that a recording of all events did not find, asks you to root the type instead.

For what an extension author has to consider, see Native AOT for extensions.