CLR & Concurrency · Lesson 10

AOT metadata needs an explicit reachability plan

Compare direct registration with annotated reflection without claiming an unpublished native artifact.

External .NET 10 SDK / desktop runtime. OS-thread experiments run only here; the .NET 9 browser companion has the narrower scope stated in the guide.

Read or download the complete SDK project, then follow its setup on your computer. The separate C# companion practice below runs in SharpRune’s .NET 9 console environment.

The runtime contract

NativeAOT and trimming require a deliberate plan for which code and metadata remain reachable. An explicit registry exposes known constructors directly, while bounded reflection can communicate requirements with annotations such as DynamicallyAccessedMembers. The portable companion uses only direct registration; the external desktop project also executes an annotated Activator path for a statically known type. An ordinary build and run of that project is JIT-based and is not a NativeAOT publish. The sample therefore teaches the reachability boundary without claiming a native artifact was generated, warnings were eliminated, or arbitrary reflection is safe in every deployment.

Trace the portable program

The registry maps rune to a factory for RuneMessage. Calling it produces text rune, and looking up other fails, so stdout is message:rune followed by missing:False. The external program compares that result with actual annotated reflection activation and fails if their text disagrees. Its annotation requests a public parameterless constructor for the supplied Type; it does not preserve every member of every possible future plugin. A common wrong answer claims that seeing identical desktop results proves NativeAOT compatibility. That conclusion still requires publishing the intended target and checking the actual native artifact and relevant diagnostics.

Execute the actual desktop lab

The external Create method accepts a Type annotated with a public-parameterless-constructor requirement and actually calls Activator.CreateInstance. Its statically known RuneMessage input satisfies that requirement and is compared with a direct factory result. The annotation communicates metadata reachability to analysis; it does not turn every Type into an IMessage or guarantee arbitrary generic code can be generated at runtime. The supplied project explicitly runs normally with PublishAot false. A separate native publication, supported toolchain, warning review, and execution of the resulting artifact are necessary before calling this a tested NativeAOT deployment.

Pitfalls and boundary limits

Do not suppress trimming or dynamic-code warnings merely because a development JIT run happened to succeed. An unconstrained string-based type search or runtime MakeGenericType call can introduce requirements the analyzer cannot predict. Conversely, reflection is not universally forbidden: bounded patterns with appropriate metadata and known code can be supported. Direct registration often makes a small allowed set easier to inspect and test. Keep constructors, methods, attributes, and generic instantiations as distinct preservation needs; annotating one constructor does not preserve unrelated members or authorize loading a new assembly in an execution model that cannot support that operation.

Practice one: isolate behavior

First add a second explicitly registered message factory and verify its known text and the unchanged missing-key behavior. Hint: the registry key is an application contract, not a simple Type.Name guessed from arbitrary input. Then pass an incompatible known type to a separate reflection experiment and distinguish construction failure from a failed interface cast. Keep that invalid experiment outside the baseline. The direct and reflective paths can share a behavior contract while having different validation, diagnostics, and deployment requirements that deserve separate expected outcomes.

Practice two: design and verify

For another exercise, design a plugin-like feature with a fixed allowed set and compare explicit factories against unrestricted runtime discovery. Specify exactly which members the reflective approach needs and where annotations would belong. Hint: do not broaden preservation to everything simply to hide uncertainty. If native deployment is required, perform an actual supported publish and run before making a compatibility claim, including the failure and missing-registration cases. Until that experiment exists, describe the project as annotated JIT execution with a clear publication boundary rather than a native artifact that was never built.

Review the evidence

Review AOT readiness by naming the supported execution model, reachable code, required metadata, dynamic-code operations, and actual publication evidence. The companion's two lines establish a direct registration contract, and the external run establishes one annotated reflection activation on desktop JIT. Neither substitutes for NativeAOT execution. A precise deployment boundary makes the example useful without fabricating capabilities. Prefer a bounded design when it fits the product, and let any genuinely dynamic extension requirement drive explicit testing of the runtime and deployment model that must support it. A runtime capability guard can select a supported fallback, but it cannot make missing native code or unavailable metadata appear after publication.

External .NET 10 SDK / desktop runtime. OS-thread experiments run only here; the .NET 9 browser companion has the narrower scope stated in the guide.

AOT metadata needs an explicit reachability plan — desktop runtime lab

Set up the SDK project

  1. Save both files at their exact paths in a fresh owned directory.
  2. Use a .NET 10 SDK, then run dotnet run --project RuntimeLab.csproj. This project intentionally uses C# 13 syntax.
  3. Workers and waits are bounded; a timeout or a violated invariant fails the process. No benchmark or NativeAOT publication is implied by an ordinary run.
  4. This run is JIT-based. A separate NativeAOT publish with supported platform prerequisites and inspection of warnings is required before claiming the native artifact works; no NativeAOT execution is claimed here.

Download complete SDK project ↗

Project files

Program.csDownload file ↗
using System;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Reflection;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;

var registry = new Dictionary<string, Func<IMessage>>(StringComparer.Ordinal) { ["rune"] = () => new RuneMessage() };
IMessage direct = registry["rune"]();
IMessage reflected = Create(typeof(RuneMessage));
if (reflected.Text != direct.Text) throw new InvalidOperationException("Activation disagreed");
Console.WriteLine($"message:{direct.Text}");
Console.WriteLine($"missing:{registry.TryGetValue("other", out _)}");
static IMessage Create([System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type type)
    => (IMessage)Activator.CreateInstance(type)!;
public interface IMessage { string Text { get; } }
public sealed class RuneMessage : IMessage { public RuneMessage() { } public string Text => "rune"; }
RuntimeLab.csprojDownload file ↗
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <LangVersion>13.0</LangVersion>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <PublishAot>false</PublishAot>
  </PropertyGroup>
</Project>

Expected result

Exact behavioral stdout on the stated desktop lab: message:rune missing:False Variable diagnostics, where emitted, go to stderr and must not be treated as portable timing or allocation guarantees.

Use the listed SDK and dependencies for this project. Open C# practice runs the separate portable exercise.

C# companion practice

using System;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Reflection;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;

var registry = new Dictionary<string, Func<IMessage>>(StringComparer.Ordinal) { ["rune"] = () => new RuneMessage() };
Console.WriteLine($"message:{registry["rune"]().Text}");
Console.WriteLine($"missing:{registry.TryGetValue("other", out _)}");
public interface IMessage { string Text { get; } }
public sealed class RuneMessage : IMessage { public RuneMessage() { } public string Text => "rune"; }
Open C# practice

C# practice output

message:rune
missing:False
Try this
Direct registration exposes reachable constructors. The annotated external reflection path is executed on desktop JIT; NativeAOT still needs a real publish and warning review.

Further reading

Interview questions for this lesson