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.
But not every subsystem is AOT-clean. Some features rely on reflection in ways that can't yet be expressed without a source generator, and they sit behind unconditional trim suppressions. The suppressions silence the analyzer; at runtime, the affected code paths will throw under AOT if they're invoked.
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.
- 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.
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 |
|---|---|---|
| Devtools / MCP server | Assembly.GetTypes for component discovery, reflection-based hook inspection in DevtoolsStateTool, reflection-based method invocation in DevtoolsFireTool, DependencyProperty enumeration in DevtoolsPropertyTools, JsonSerializer for MCP request/response. The whole devtools surface (/--devtools, /--mcp-stdio) is reflection-driven. |
Issue #70 |
| 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 |
| Navigation state JSON | NavigationHandle serializes deep-link state via JsonSerializer without a source-generated context. Custom types that ride through navigation state will fail to serialize under AOT. |
Issue #70 |
Component discovery (ReactorApp.Run<TApp> reflection paths) |
The instantiation of TApp itself is annotated and works. The devtools-only --list-components enumeration scans Assembly.GetTypes; that path is gated to non-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 |
- 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. - 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.
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.
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.
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. |
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).
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.
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.SelfTestsWhen 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.)
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.ps1If 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.