diff --git a/Examples/NativeAot/NativeAot.csproj b/Examples/NativeAot/NativeAot.csproj
index 8ae39a7f22..18f5bce920 100644
--- a/Examples/NativeAot/NativeAot.csproj
+++ b/Examples/NativeAot/NativeAot.csproj
@@ -9,7 +9,6 @@
-
diff --git a/Terminal.Gui/Configuration/ConfigProperty.cs b/Terminal.Gui/Configuration/ConfigProperty.cs
index 0a5d7dc8f9..1eb114887f 100644
--- a/Terminal.Gui/Configuration/ConfigProperty.cs
+++ b/Terminal.Gui/Configuration/ConfigProperty.cs
@@ -3,6 +3,7 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
+using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -457,14 +458,27 @@ private static void UpdateSchemeDictionary (
private static ImmutableSortedDictionary? _classesWithConfigProps;
///
- /// INTERNAL: Called from the method to initialize the
- /// _classesWithConfigProps dictionary.
+ /// INTERNAL: Called from the method to initialize the
+ /// dictionary.
///
- [RequiresDynamicCode ("Uses reflection to scan assemblies for configuration properties. " +
- "Only called during initialization and not needed during normal operation. " +
- "In AOT environments, ensure all types with ConfigurationPropertyAttribute are preserved.")]
- [RequiresUnreferencedCode ("Reflection requires all types with ConfigurationPropertyAttribute to be preserved in AOT. " +
- "Use the SourceGenerationContext to register all configuration property types.")]
+ ///
+ ///
+ /// The set of host types that ship in Terminal.Gui is declared statically in
+ /// and registered unconditionally. Declaring the set statically keeps initialization
+ /// trim- and AOT-safe without requiring consumer apps to carry <TrimmerRootAssembly Include="Terminal.Gui" />.
+ /// See .
+ ///
+ ///
+ /// Additional host types defined outside Terminal.Gui (test suites, plugins, embedding apps) are picked up via a
+ /// runtime assembly scan when dynamic code is supported. The scan is a no-op under AOT where dynamic code is disabled,
+ /// and any types it would find would have been trimmed anyway unless the consumer roots them.
+ ///
+ ///
+ [RequiresDynamicCode ("Uses reflection to scan assemblies for configuration properties. "
+ + "Only called during initialization and not needed during normal operation. "
+ + "In AOT environments, ensure all types with ConfigurationPropertyAttribute are preserved.")]
+ [RequiresUnreferencedCode ("Reflection requires all types with ConfigurationPropertyAttribute to be preserved in AOT. "
+ + "Use the SourceGenerationContext to register all configuration property types.")]
internal static void Initialize ()
{
if (_classesWithConfigProps is { })
@@ -474,8 +488,28 @@ internal static void Initialize ()
Dictionary dict = new (StringComparer.InvariantCultureIgnoreCase);
- // Process assemblies directly to avoid LINQ overhead
+ // Trim/AOT-safe: statically-rooted host types that ship with Terminal.Gui.
+ foreach (Type type in ConfigPropertyHostTypes.GetTypes ())
+ {
+ dict [type.Name] = type;
+ }
+
+ // JIT-only supplement: discover host types defined outside Terminal.Gui (test suites, plugins).
+ // Under AOT, RuntimeFeature.IsDynamicCodeSupported is false and we skip the scan entirely.
+ if (RuntimeFeature.IsDynamicCodeSupported)
+ {
+ ScanLoadedAssembliesForConfigPropertyHosts (dict);
+ }
+
+ _classesWithConfigProps = dict.ToImmutableSortedDictionary ();
+ }
+
+ [RequiresDynamicCode ("Uses reflection to scan assemblies for configuration properties.")]
+ [RequiresUnreferencedCode ("Scanning for types via reflection is not trim-safe.")]
+ private static void ScanLoadedAssembliesForConfigPropertyHosts (Dictionary dict)
+ {
Assembly [] assemblies = AppDomain.CurrentDomain.GetAssemblies ();
+
foreach (Assembly assembly in assemblies)
{
try
@@ -488,24 +522,29 @@ internal static void Initialize ()
foreach (Type type in assembly.GetTypes ())
{
PropertyInfo [] properties = type.GetProperties ();
-
- // Check if any property has the ConfigurationPropertyAttribute
var hasConfigProp = false;
+
foreach (PropertyInfo prop in properties)
{
if (HasConfigurationPropertyAttribute (prop))
{
hasConfigProp = true;
+
break;
}
}
- if (hasConfigProp)
+ if (!hasConfigProp)
{
- dict [type.Name] = type;
+ continue;
}
+
+ // TryAdd — never overwrite a statically-registered Terminal.Gui host. An external assembly
+ // with a same-named type (e.g. a consumer's own `Window`) must not displace the built-in entry.
+ dict.TryAdd (type.Name, type);
}
}
+
// Skip problematic assemblies that can't be loaded or analyzed
catch (ReflectionTypeLoadException)
{
@@ -516,8 +555,6 @@ internal static void Initialize ()
continue;
}
}
-
- _classesWithConfigProps = dict.ToImmutableSortedDictionary ();
}
///
diff --git a/Terminal.Gui/Configuration/ConfigPropertyHostTypes.cs b/Terminal.Gui/Configuration/ConfigPropertyHostTypes.cs
new file mode 100644
index 0000000000..4b6ce6e712
--- /dev/null
+++ b/Terminal.Gui/Configuration/ConfigPropertyHostTypes.cs
@@ -0,0 +1,103 @@
+using System.Diagnostics.CodeAnalysis;
+using Terminal.Gui.App;
+using Terminal.Gui.Drawing;
+using Terminal.Gui.Drivers;
+using Terminal.Gui.Input;
+using Terminal.Gui.Text;
+using Terminal.Gui.Tracing;
+using Terminal.Gui.ViewBase;
+using Terminal.Gui.Views;
+
+namespace Terminal.Gui.Configuration;
+
+///
+/// INTERNAL: The statically-known set of types that own
+/// -decorated properties.
+///
+///
+///
+/// used to discover these types by reflecting over every loaded
+/// assembly at module-init time. That scan was not trim-safe: with PublishTrimmed=true, types
+/// not otherwise referenced by the consuming application were stripped and
+/// Scope<T>.GetUninitializedProperty would throw at startup.
+/// See .
+///
+///
+/// The per-type entries on root
+/// each host and preserve its so the
+/// trimmer does not remove the properties the scan depends on.
+///
+///
+/// A unit test verifies this list stays in sync with the types that actually carry
+/// so drift is caught at build time, not in AOT
+/// consumer apps.
+///
+///
+internal static class ConfigPropertyHostTypes
+{
+ private const DynamicallyAccessedMemberTypes PreservedMembers = DynamicallyAccessedMemberTypes.PublicProperties;
+
+ private static readonly Type [] _types =
+ [
+ typeof (Application),
+ typeof (ConfigurationManager),
+ typeof (SchemeManager),
+ typeof (ThemeManager),
+ typeof (Color),
+ typeof (Glyphs),
+ typeof (Driver),
+ typeof (Key),
+ typeof (NerdFonts),
+ typeof (Trace),
+ typeof (View),
+ typeof (Button),
+ typeof (CharMap),
+ typeof (CheckBox),
+ typeof (Dialog),
+ typeof (FileDialog),
+ typeof (FileDialogStyle),
+ typeof (FrameView),
+ typeof (HexView),
+ typeof (LinearRange),
+ typeof (Menu),
+ typeof (MenuBar),
+ typeof (MessageBox),
+ typeof (PopoverMenu),
+ typeof (SelectorBase),
+ typeof (StatusBar),
+ typeof (TextField),
+ typeof (TextView),
+ typeof (Window)
+ ];
+
+ [DynamicDependency (PreservedMembers, typeof (Application))]
+ [DynamicDependency (PreservedMembers, typeof (ConfigurationManager))]
+ [DynamicDependency (PreservedMembers, typeof (SchemeManager))]
+ [DynamicDependency (PreservedMembers, typeof (ThemeManager))]
+ [DynamicDependency (PreservedMembers, typeof (Color))]
+ [DynamicDependency (PreservedMembers, typeof (Glyphs))]
+ [DynamicDependency (PreservedMembers, typeof (Driver))]
+ [DynamicDependency (PreservedMembers, typeof (Key))]
+ [DynamicDependency (PreservedMembers, typeof (NerdFonts))]
+ [DynamicDependency (PreservedMembers, typeof (Trace))]
+ [DynamicDependency (PreservedMembers, typeof (View))]
+ [DynamicDependency (PreservedMembers, typeof (Button))]
+ [DynamicDependency (PreservedMembers, typeof (CharMap))]
+ [DynamicDependency (PreservedMembers, typeof (CheckBox))]
+ [DynamicDependency (PreservedMembers, typeof (Dialog))]
+ [DynamicDependency (PreservedMembers, typeof (FileDialog))]
+ [DynamicDependency (PreservedMembers, typeof (FileDialogStyle))]
+ [DynamicDependency (PreservedMembers, typeof (FrameView))]
+ [DynamicDependency (PreservedMembers, typeof (HexView))]
+ [DynamicDependency (PreservedMembers, typeof (LinearRange))]
+ [DynamicDependency (PreservedMembers, typeof (Menu))]
+ [DynamicDependency (PreservedMembers, typeof (MenuBar))]
+ [DynamicDependency (PreservedMembers, typeof (MessageBox))]
+ [DynamicDependency (PreservedMembers, typeof (PopoverMenu))]
+ [DynamicDependency (PreservedMembers, typeof (SelectorBase))]
+ [DynamicDependency (PreservedMembers, typeof (StatusBar))]
+ [DynamicDependency (PreservedMembers, typeof (TextField))]
+ [DynamicDependency (PreservedMembers, typeof (TextView))]
+ [DynamicDependency (PreservedMembers, typeof (Window))]
+ internal static Type [] GetTypes () => _types;
+}
diff --git a/Tests/UnitTestsParallelizable/Configuration/ConfigPropertyHostTypesTests.cs b/Tests/UnitTestsParallelizable/Configuration/ConfigPropertyHostTypesTests.cs
new file mode 100644
index 0000000000..a68abb4b3e
--- /dev/null
+++ b/Tests/UnitTestsParallelizable/Configuration/ConfigPropertyHostTypesTests.cs
@@ -0,0 +1,87 @@
+// Claude - Opus 4.7
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+using Terminal.Gui.Configuration;
+
+namespace ConfigurationTests;
+
+public class ConfigPropertyHostTypesTests
+{
+ ///
+ /// Guard-rail test: the hard-coded list in must exhaustively cover every type in
+ /// the Terminal.Gui assembly that declares a property. Drift between
+ /// the list and the attribute usage would silently reintroduce the trim/AOT failure fixed by
+ /// .
+ ///
+ [Fact]
+ public void ConfigPropertyHostTypes_GetTypes_Matches_Reflected_Hosts_In_TerminalGui_Assembly ()
+ {
+ // Arrange
+ Assembly terminalGuiAssembly = typeof (ConfigurationManager).Assembly;
+
+ HashSet reflected = terminalGuiAssembly
+ .GetTypes ()
+ .Where (HasConfigurationProperty)
+ .ToHashSet ();
+
+ // Act
+ HashSet registered = ConfigPropertyHostTypes.GetTypes ().ToHashSet ();
+
+ // Assert
+ Type [] missing = reflected.Except (registered).ToArray ();
+ Type [] extra = registered.Except (reflected).ToArray ();
+
+ Assert.True (
+ missing.Length == 0 && extra.Length == 0,
+ $"ConfigPropertyHostTypes drift detected.\nMissing (add to list): {string.Join (", ", missing.Select (t => t.FullName))}\nExtra (remove from list): {string.Join (", ", extra.Select (t => t.FullName))}");
+ }
+
+ ///
+ /// Guard-rail test: the set on GetTypes must exactly match the
+ /// _types array it returns. The attributes are what the trimmer actually reads to preserve members; if
+ /// someone adds a type to the array but forgets the matching attribute (or vice-versa), AOT builds would silently
+ /// lose rooting again without the reflected-hosts test above catching it.
+ ///
+ [Fact]
+ public void ConfigPropertyHostTypes_DynamicDependencies_Match_Returned_Types ()
+ {
+ // Arrange
+ MethodInfo getTypes = typeof (ConfigPropertyHostTypes).GetMethod (
+ nameof (ConfigPropertyHostTypes.GetTypes),
+ BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)!;
+
+ HashSet rooted = getTypes
+ .GetCustomAttributes ()
+ .Select (a => a.Type!)
+ .ToHashSet ();
+
+ HashSet returned = ConfigPropertyHostTypes.GetTypes ().ToHashSet ();
+
+ // Assert
+ Type [] missingRoots = returned.Except (rooted).ToArray ();
+ Type [] extraRoots = rooted.Except (returned).ToArray ();
+
+ Assert.True (
+ missingRoots.Length == 0 && extraRoots.Length == 0,
+ $"ConfigPropertyHostTypes [DynamicDependency] drift detected.\nTypes in array but not rooted by attribute: {string.Join (", ", missingRoots.Select (t => t.FullName))}\nTypes rooted by attribute but not in array: {string.Join (", ", extraRoots.Select (t => t.FullName))}");
+ }
+
+ private static bool HasConfigurationProperty (Type type)
+ {
+ // Mirror the production scan in ConfigProperty.Initialize (), which calls type.GetProperties () — i.e.,
+ // public properties only (includes inherited public members; excludes non-public and, by default, statics
+ // not declared on `type` itself). Widening the binding flags here would flag types the production scan
+ // could never discover and the trimmer wouldn't preserve via PublicProperties.
+ PropertyInfo [] properties = type.GetProperties ();
+
+ foreach (PropertyInfo property in properties)
+ {
+ if (property.GetCustomAttribute () is { })
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}