You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
loc: replace reflection-based args with tuple-params + IDictionary overloads
The Message(MessageKey, object?) and RichMessage(MessageKey, object?, ...)
overloads built an args dictionary by reflecting over public properties of
the passed object — anonymous types or [LocArgs]-marked records. Under
NativeAOT the trimmer keeps the ctor + backing fields of the anonymous type
(both reachable from the
ew { … } call site) but drops the property
getters, because nothing statically reads them. GetProperties() then
returned an empty array, MessageFormat threw on {name}, the catch
swallowed the exception, and the raw ICU pattern leaked through to the UI.
This change removes the reflection path entirely and gives callers two
AOT-safe shapes:
- Message(MessageKey, IDictionary<string, object>?) — explicit dict for
cases that already have one (or want to share/build one programmatically).
- Message(MessageKey, (string Name, object Value) arg1,
params (string Name, object Value)[] more) — compact tuple
syntax for the common inline case. ValueTuple is a nominal type with
concrete fields; building a Dictionary<string,object> from tuple
values needs zero reflection.
Same shape for RichMessage. (RichMessage with tags uses the dict
overload — params must come last, so the tuple variant can't carry
the optional ags argument.)
Deleted:
- ToArgsDictionary + IsAcceptableArgsContainer + _propertyCache
- The IL2026/IL2070 unconditional-suppression attributes those needed
- System.Reflection + System.Collections.Concurrent +
System.Diagnostics.CodeAnalysis usings (now unused)
Migrated callers:
- Selftest + AppTests localization fixtures (two files)
- IntlAccessorTests, RichMessageTests
- SourceRewriter CLI codegen: .Message(key, new { name = expr })
→ .Message(key, ("name", expr)). Updated SourceRewriterTests.
- Docs: _pipeline/templates/localization.md.dt narrative and
_pipeline/apps/localization/App.cs snippets, plus the
hand-edited docs/reference/localization-howto.md example.
Removed from AOT skip list / docs:
- Localization_LocaleSwitching no longer needs to be skipped under AOT
( ests/Reactor.AppTests.Host/SelfTest/SelfTestRunner.cs).
- The "Localization with anonymous-type args" row in docs/aot-support.md
is gone — there's no anonymous-type path left.
Verified:
- dotnet build src/Reactor clean (AOT warnings as errors).
- 282 localization unit tests pass.
- Full JIT selftest: 0 failures.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy file name to clipboardExpand all lines: docs/aot-support.md
-1Lines changed: 0 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -31,7 +31,6 @@ These subsystems compile cleanly with `IsAotCompatible=true` (the warnings are s
31
31
|**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|
32
32
|**`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|
33
33
|**Form validation**|`FormField`'s default editor resolution goes through `TypeRegistry`. Same caveat as PropertyGrid. | Issue #70|
34
-
|**Localization with anonymous-type args**|`t.Message(Loc.X, new { count = 3 })` reflects over the anonymous type to build the ICU args dictionary. Use `t.Message(Loc.X, new Dictionary<string, object> { ["count"] = 3 })` under AOT. | Issue #70|
35
34
|**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|
36
35
|**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|
37
36
|**Theme resource lookup (`Theme.X`, `ThemeRef.Resolve`)**|`ThemeRef.Resolve` walks `Application.Current.Resources` + its merged/theme dictionaries. The token records (`Theme.Accent`, `Theme.PrimaryText`, …) construct fine, but at runtime the `XamlControlsResources` entries that `ReactorApplication.xaml` brings in aren't fully populated under AOT — `Resolve` returns `null` for keys that exist under the JIT. Brushes applied via `.Foreground(Theme.X)` will fall back to control defaults. | Issue #70|
0 commit comments