AOT support matrix
June 23, 2026 · View on GitHub
Microsoft.UI.Reactor (Reactor)'s core framework targets .NET native AOT. The repo sets IsAotCompatible=true and treats the IL2*/IL3* analyzer warnings as errors on the core library, so trimming/AOT regressions can't merge silently. The tests/stress_perf/StressPerf.Reactor benchmark harness publishes Reactor with PublishAot=true on every CI build that runs perf — that's the canary for the framework's AOT viability.
Most of Reactor's runtime stays AOT-clean by default. Reflection-heavy development surfaces are now gated behind build-time feature switches so release/AOT apps can keep them trimmed unless they explicitly opt in.
This page enumerates what works, what doesn't, and what's planned. If you publish your app with PublishAot=true, you're responsible for staying inside the green column.
What works under AOT
- Core reconciler — virtual element tree, diffing, mount/update, keyed reconciliation, element pooling.
- DSL & elements — factory methods, fluent modifiers.
- Hooks —
UseState,UseReducer,UseEffect,UseMemo,UseRef,UseCallback. (See note onUseObservablebelow.) - Flex layout — Yoga port is pure C#, no reflection.
- Charting (D3) — algorithm port is pure C#.
- Markdown — md4c-backed parser and renderer.
- Commanding — command records, focus-scoped accelerators.
- Animation — compositor-layer transitions, keyframes.
- Theming —
ThemeReftokens, style caching. Brushes resolved throughXamlControlsResourceswork under AOT once the WindowsAppSDK#6394 publish workaround is in place (see Required publish-time workarounds below). - Built-in WinUI controls —
NavigationView,TabView,Pivot,TreeView,Expander,InfoBar,MenuBar,BreadcrumbBar,RefreshContainer,TeachingTip,ColorPicker,NumberBox,RatingControl,SplitButton, etc. all mount and update under AOT once WindowsAppSDK#6394 is mitigated.{TemplateBinding}against built-in DPs works; private DPs declared in a separate third-party assembly without anIXamlMetadataProviderare still broken (Issue #142). - Markdown / Localization (read path) —
IStringLoc/Loc.X.Ylookups generated byReactor.Localization.Generatorare AOT-clean. Source-generated, no runtime reflection.
What does not work under AOT (today)
These subsystems compile cleanly with IsAotCompatible=true (the warnings are suppressed), but the suppressions cover real reflection that will throw at runtime under PublishAot=true. Don't rely on them in an AOT-published app until they're rebuilt on source generators.
| Subsystem | What breaks | Tracking |
|---|---|---|
| PropertyGrid auto-discovery | TypeRegistry.Resolve, ReflectionTypeMetadataProvider walk public properties, build init-only setters, and instantiate editors via Activator.CreateInstance. Manually-registered metadata is fine; auto-discovery from a runtime type is not. | Issue #70 |
| PropertyGrid array editor | Array.CreateInstance is annotated RequiresDynamicCode. Array-typed property editors won't work AOT. | Issue #70 |
DataGrid AutoColumns<T> | Reflects over T's public properties via TypeRegistry. The T parameter is annotated [DynamicallyAccessedMembers(PublicProperties)] so the trimmer keeps the members, but AutoColumns ultimately funnels into TypeRegistry.Resolve, which is RequiresUnreferencedCode. Use explicit Column<T,V>(…) definitions instead. | Issue #70 |
UseObservable on POCOs | ObservableTreeTracker walks public properties via reflection to subscribe to INPC. Observables built explicitly (Observable<T>, IObservableCollection) are fine; the implicit-INPC path is not. | Issue #70 |
| Form validation | FormField's default editor resolution goes through TypeRegistry. Same caveat as PropertyGrid. | Issue #70 |
Component discovery (ReactorApp.Run<TApp> reflection paths) | The instantiation of TApp itself is annotated and works. Devtools component enumeration is available only when the app opts into Reactor.DevtoolsSupport at build time and launches with --devtools; leave the switch off for retail/AOT builds. | Issue #70 |
Theme resource lookup (Theme.X, ThemeRef.Resolve) | Works once the WindowsAppSDK#6394 workaround target ships the project .pri into the publish output (see Required publish-time workarounds). Reactor's library itself is AOT-clean here. | WindowsAppSDK#6394 |
| Third-party-assembly XAML metadata (Issue #142 fixtures) | Built-in WinUI controls work under AOT now (see Required publish-time workarounds). What remains is {TemplateBinding} against private DPs in a third-party assembly that ships no .xaml file: the XAML compiler only emits an IXamlMetadataProvider for projects that have at least one .xaml, and AOT trims any implicit metadata path. Affected fixtures: Issue142_CustomControlPrivateDp_Renders, Issue142_ThirdPartyControlPrivateDp_Renders. The library-author workaround is to register a hand-written IXamlMetadataProvider via RegisterControlAssembly. | Issue #142 |
Devtools support switch
Devtools are a two-layer opt-in so release and NativeAOT binaries stay trimmed by default:
-
Build-time capability — add the feature switch to an app project that needs devtools:
<ItemGroup> <RuntimeHostConfigurationOption Include="Reactor.DevtoolsSupport" Value="true" Trim="true" /> </ItemGroup>src/Reactor/build/Reactor.targetssupplies the defaultfalsevalue. Samples enable the switch conditionally in Debug builds; Release/AOT publishes leave it off. -
Runtime activation — launch with a devtools subverb, for example:
dotnet run -- --devtools runReactorApp.RunandRun<TRoot>no longer takedevtools:orpreview:parameters. If a switch-off binary receives--devtools run, it writes an actionable stderr message with theRuntimeHostConfigurationOptionsnippet above instead of silently opening a normal window.
Conventions
- The library compiles AOT-clean. Builds of
Reactor.csprojproduce zero IL2*/IL3* warnings. New code that reaches for reflection must either be source-generated, annotated withDynamicallyAccessedMembers, or — as a last resort — gated behind[RequiresUnreferencedCode]/[RequiresDynamicCode]so consumers see the warning at the call site. - Control families register into core, never the reverse. The core (
src/Reactor/Core/) and host (src/Reactor/Hosting/) must carry zero static type references into any specific control family (Charting, Docking, …). When the core needs to call into a subsystem, it defines a minimal interface (e.g.IChartingHostBridge,IScanExtension) and the subsystem registers a concrete implementation at first use — seeChartingRuntime.Activate()/D3ChartsHostBridge. This keeps the trimmer free to drop the whole subsystem from apps that never use it (issue #498 reclaimed ~7.8 KB of Charting —D3Color/ForcedColorsTheme/D3Charts— from chart-free AOT binaries). The boundary is pinned byCoreControlFamilyBoundaryTests, which scans the compiledReactor.dllIL and fails on any Core/Hosting → Charting/Docking reference (including ones hidden inside generic instantiations). - Suppressions are temporary. Every
[UnconditionalSuppressMessage("Trimming", ...)]or("AOT", ...)in this repo is a TODO. The justification field names the reflection use; tracking is folded into issue #70. - The benchmark canary.
tests/stress_perf/StressPerf.Reactor(and theStressPerf.Direct/ReactorGridsiblings) setPublishAot=true. If they stop publishing, an AOT regression has landed in the framework. - The runtime canary. CI's
AOT Selftestsjob (.github/workflows/ci.yml) publishestests/Reactor.AppTests.HostwithPublishAotInternal=trueand runs the full selftest suite against the NativeAOT binary on every PR.SelfTestRunner.DefaultAotSkipPatternsmutes the known reflection-bound fixtures (Devtools/MCP, PropertyGrid auto-discovery, Issue142 XAML metadata, plus the two framework cases under investigation); any new failure fails the job. If you intentionally need to add a skip, document the bucket in the comment aboveDefaultAotSkipPatternsand re-probe withtests/Reactor.AppTests.Host/probe-aot-skips.ps1.
Required publish-time workarounds
WindowsAppSDK#6394 — project .pri (and .xbf) missing from publish output
When publishing an unpackaged WinUI 3 app (WindowsPackageType=None) with PublishAot=true, the WindowsAppSDK build pipeline generates $(AssemblyName).pri into the intermediate output but does not copy it into the publish directory. The MakePRI step is conditioned on AppxPackage=true, so unpackaged apps fall through. Same applies to .xbf files generated by the XAML compiler.
The missing .pri has cascading runtime symptoms that look like separate bugs but all trace back to the same cause:
TabView's constructor throwsFileNotFoundExceptionfromResourceAccessor::GetLocalizedStringResource(SR_TabViewCloseButtonTooltipWithKA).NavigationViewmounts but its template-apply throws the sameFileNotFoundException(Reactor's error boundary catches it and renders an errorTextBlock).Application.Current.Resourcesloads as an empty dictionary (0merged,0themed,0keys) becauseApplication.LoadComponentcascades through MRT for type-info lookups.- Every
ThemeRef.Resolve(...)returnsnull;.Foreground(Theme.X)falls back to control defaults.
Upstream tracking: microsoft/WindowsAppSDK#6394 — still OPEN against 1.8 and 2.0.
Workaround (in tests/Reactor.AppTests.Host/Reactor.AppTests.Host.csproj):
<Target Name="_CopyWinUIResourcesForAot" AfterTargets="Publish"
Condition="'$(PublishAot)' == 'true' and '$(AppxPackage)' != 'true'">
<ItemGroup>
<_WinUIResourcesForAot Include="$(OutputPath)**\*.xbf" />
<_WinUIResourcesForAot Include="$(OutputPath)$(AssemblyName).pri"
Condition="Exists('$(OutputPath)$(AssemblyName).pri')" />
</ItemGroup>
<Copy SourceFiles="@(_WinUIResourcesForAot)"
DestinationFiles="@(_WinUIResourcesForAot->'$(PublishDir)%(RecursiveDir)%(Filename)%(Extension)')"
SkipUnchangedFiles="true" />
</Target>
Any Reactor app that publishes with PublishAot=true and WindowsPackageType=None should copy this target verbatim until WindowsAppSDK ships a fix. Remove the target once #6394 closes.
Debugging an AOT selftest hang
tests/Reactor.AppTests.Host maintains an explicit allow-list of fixtures that hang, crash, or assert-fail under NativeAOT (SelfTestRunner.DefaultAotSkipPatterns). When you remove an entry from that list and the published Host hangs, crashes, or asserts instead of producing output, use the following workflow.
Failure mode at a glance
Probing each DefaultAotSkipPatterns entry in isolation (via the tests/Reactor.AppTests.Host/probe-aot-skips.ps1 helper) reveals three buckets. Pick the matching workflow:
| Bucket | Symptom | Debug step |
|---|---|---|
| Hang (dispatcher starvation) | Fixture's RunAsync() synchronously blocks the UI thread; the in-band 15 s Task.Delay watchdog cannot fire because the dispatcher isn't pumping. | Off-dispatcher hang watchdog (60 s default, configurable via REACTOR_SELFTEST_HANG_TIMEOUT_SECONDS) writes Bail out! HANG_DETECTED: <fixture> … to stdout + stderr, flushes, then Environment.FailFast. With DOTNET_DbgEnableMiniDump=1 set, this produces a Watson minidump. |
| Native crash | Process exits with 0xC0000409 (STATUS_STACK_BUFFER_OVERRUN) — the AOT runtime's FailFast for unhandled managed exceptions. No # Total failures: line because the process terminated abruptly. | Set DOTNET_DbgEnableMiniDump=1 (and COMPlus_DbgEnableMiniDump=1 — both, matching DevtoolsStressE2ERunner) before launching. Open the resulting .dmp with dotnet-dump analyze or WinDbg; look at the dispatcher (UI) thread's stack for the throwing call. |
| Assertion failure | TAP output already shows not ok <name> - <reason>; process exits 1 cleanly. | No special tooling needed — read the TAP failure line. The fixture and check name are in the message. |
Parent attribution
The MSTest harness (Reactor.SelfTests.SelfTestBatch) parses the HANG_DETECTED signal from both stdout and stderr, and on process timeout falls back to the last # Running: line. Either way, the failure surfaces against the named fixture in the failing test's detail with a copy-pasteable repro command. It does not cascade through _initError (which would mark every unrelated fixture failed).
Capturing a dump
Set these env vars before launching the Host:
DOTNET_DbgEnableMiniDump=1
DOTNET_DbgMiniDumpType=2
DOTNET_DbgMiniDumpName=%TEMP%\reactor-selftest-%p.dmp
COMPlus_DbgEnableMiniDump=1
COMPlus_DbgMiniDumpType=2
COMPlus_DbgMiniDumpName=%TEMP%\reactor-selftest-%p.dmp
Both Environment.FailFast (hang path) and the AOT runtime's unhandled-exception fast-fail (crash path) honour these vars.
Isolated repro
Once you have the offending fixture name, repro it standalone against the AOT-published binary:
dotnet publish tests/Reactor.AppTests.Host -p:PublishAotInternal=true -p:Platform=x64 -r win-x64 -c Release
$env:DOTNET_DbgEnableMiniDump=1
$env:DOTNET_DbgMiniDumpName="$env:TEMP\reactor-hang-%p.dmp"
& "<publish-dir>\Reactor.AppTests.Host.exe" --self-test --no-aot-skip --filter <FixtureName>
--no-aot-skip bypasses the entire skip list so the targeted fixture actually runs. To drive the MSTest harness against the AOT-published binary, point it at the publish output:
$env:REACTOR_SELFTEST_HOST_EXE="<publish-dir>\Reactor.AppTests.Host.exe"
dotnet test tests/Reactor.SelfTests
Disabling the watchdog while debugging
When stepping through a fixture in a debugger, set REACTOR_SELFTEST_HANG_TIMEOUT_SECONDS=0 to suppress the hang watchdog entirely. (It also auto-disables whenever Debugger.IsAttached returns true at poll time.)
Categorising the skip list (tests/Reactor.AppTests.Host/probe-aot-skips.ps1)
The repo includes a probe script that runs every DefaultAotSkipPatterns entry in isolation under --no-aot-skip and writes a CSV summarising whether each one passes, hangs, crashes natively, or assert-fails. Use it after AOT framework changes to find stale skips that have started passing, and to triage what's still broken.
pwsh -NoProfile -File tests\Reactor.AppTests.Host\probe-aot-skips.ps1
When in doubt
If you need a feature listed in the "does not work" table and you're publishing AOT, file an issue against #70 with your scenario. The fix in most cases is a source generator pass; what gets prioritized is driven by who's hitting the wall.