ConfigProperty.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  1. using System.Collections.Concurrent;
  2. using System.Collections.Immutable;
  3. using System.Diagnostics;
  4. using System.Diagnostics.CodeAnalysis;
  5. using System.Reflection;
  6. using System.Text.Json;
  7. using System.Text.Json.Serialization;
  8. namespace Terminal.Gui.Configuration;
  9. /// <summary>
  10. /// Holds a property's value and the <see cref="PropertyInfo"/> that allows <see cref="ConfigurationManager"/> to
  11. /// retrieve and apply the property's value.
  12. /// </summary>
  13. /// <remarks>
  14. /// Configuration properties must be <see langword="public"/>/<see langword="internal"/> and <see langword="static"/> and have the
  15. /// <see cref="ConfigurationPropertyAttribute"/> attribute. If the type of the property requires specialized JSON
  16. /// serialization, a <see cref="JsonConverter"/> must be provided using the <see cref="JsonConverterAttribute"/>
  17. /// attribute.
  18. /// </remarks>
  19. public class ConfigProperty
  20. {
  21. /// <summary>Describes the property.</summary>
  22. public PropertyInfo? PropertyInfo { get; set; }
  23. /// <summary>INTERNAL: Cached value of ConfigurationPropertyAttribute.OmitClassName; makes more AOT friendly.</summary>
  24. internal bool OmitClassName { get; set; }
  25. /// <summary>INTERNAL: Cached value of ConfigurationPropertyAttribute.Scope; makes more AOT friendly.</summary>
  26. internal string? ScopeType { get; set; }
  27. private object? _propertyValue;
  28. /// <summary>
  29. /// Holds the property's value as it was either read from the class's implementation or from a config file. If the
  30. /// property has not been set (e.g. because no configuration file specified a value), <see cref="HasValue"/> will be <see langword="false"/>.
  31. /// </summary>
  32. public object? PropertyValue
  33. {
  34. get => _propertyValue;
  35. set
  36. {
  37. if (Immutable)
  38. {
  39. throw new InvalidOperationException ($"Property {PropertyInfo?.Name} is immutable and cannot be set.");
  40. }
  41. // TODO: Verify value is correct type?
  42. _propertyValue = value;
  43. HasValue = true;
  44. }
  45. }
  46. /// <summary>
  47. /// Gets or sets whether this config property has a value. This is set to <see langword="true"/> when <see cref="PropertyValue"/> is set.
  48. /// </summary>
  49. public bool HasValue { get; set; }
  50. /// <summary>
  51. /// INTERNAL: Gets or sets whether this property is immutable. If <see langword="true"/>, the property cannot be changed.
  52. /// </summary>
  53. internal bool Immutable { get; set; }
  54. /// <summary>Applies the <see cref="PropertyValue"/> to the static property described by <see cref="PropertyInfo"/>.</summary>
  55. /// <returns></returns>
  56. [RequiresDynamicCode ("Uses reflection to get and set property values")]
  57. [RequiresUnreferencedCode ("Uses DeepCloner which requires types to be registered in SourceGenerationContext")]
  58. public bool Apply ()
  59. {
  60. try
  61. {
  62. if (PropertyInfo?.GetValue (null) is { })
  63. {
  64. // Use DeepCloner to create a deep copy of PropertyValue
  65. object? val = DeepCloner.DeepClone (PropertyValue);
  66. Debug.Assert (!Immutable);
  67. PropertyInfo.SetValue (null, val);
  68. }
  69. }
  70. catch (TargetInvocationException tie)
  71. {
  72. if (tie.InnerException is { })
  73. {
  74. throw new JsonException (
  75. $"Error Applying Configuration Change: {tie.InnerException.Message}",
  76. tie.InnerException
  77. );
  78. }
  79. throw new JsonException ($"Error Applying Configuration Change: {tie.Message}", tie);
  80. }
  81. catch (ArgumentException ae)
  82. {
  83. throw new JsonException (
  84. $"Error Applying Configuration Change ({PropertyInfo?.Name}): {ae.Message}",
  85. ae
  86. );
  87. }
  88. return PropertyValue != null;
  89. }
  90. /// <summary>
  91. /// INTERNAL: Creates a copy of a ConfigProperty with the same metadata but no value.
  92. /// </summary>
  93. /// <param name="source">The source ConfigProperty.</param>
  94. /// <returns>A new ConfigProperty instance.</returns>
  95. internal static ConfigProperty CreateCopy (ConfigProperty source)
  96. {
  97. return new ConfigProperty
  98. {
  99. Immutable = false,
  100. PropertyInfo = source.PropertyInfo,
  101. OmitClassName = source.OmitClassName,
  102. ScopeType = source.ScopeType,
  103. HasValue = false
  104. };
  105. }
  106. /// <summary>
  107. /// INTERNAL: Create an immutable ConfigProperty with cached attribute information
  108. /// </summary>
  109. /// <param name="propertyInfo">The PropertyInfo to create from</param>
  110. /// <returns>A new ConfigProperty with attribute data cached</returns>
  111. [RequiresDynamicCode ("Uses reflection to access custom attributes")]
  112. internal static ConfigProperty CreateImmutableWithAttributeInfo (PropertyInfo propertyInfo)
  113. {
  114. var attr = propertyInfo.GetCustomAttribute (typeof (ConfigurationPropertyAttribute)) as ConfigurationPropertyAttribute;
  115. return new ConfigProperty
  116. {
  117. PropertyInfo = propertyInfo,
  118. OmitClassName = attr?.OmitClassName ?? false,
  119. ScopeType = attr?.Scope!.Name,
  120. // By default, properties are immutable
  121. Immutable = true
  122. };
  123. }
  124. /// <summary>
  125. /// INTERNAL: Helper method to get the ConfigurationPropertyAttribute for a PropertyInfo
  126. /// </summary>
  127. /// <param name="propertyInfo">The PropertyInfo to get the attribute from</param>
  128. /// <returns>The ConfigurationPropertyAttribute if found; otherwise, null</returns>
  129. [RequiresDynamicCode ("Uses reflection to access custom attributes")]
  130. internal static ConfigurationPropertyAttribute? GetConfigurationPropertyAttribute (PropertyInfo propertyInfo)
  131. {
  132. return propertyInfo.GetCustomAttribute (typeof (ConfigurationPropertyAttribute)) as ConfigurationPropertyAttribute;
  133. }
  134. /// <summary>
  135. /// INTERNAL: Helper method to check if a PropertyInfo has a ConfigurationPropertyAttribute
  136. /// </summary>
  137. /// <param name="propertyInfo">The PropertyInfo to check</param>
  138. /// <returns>True if the PropertyInfo has a ConfigurationPropertyAttribute; otherwise, false</returns>
  139. [RequiresDynamicCode ("Uses reflection to access custom attributes")]
  140. internal static bool HasConfigurationPropertyAttribute (PropertyInfo propertyInfo)
  141. {
  142. return propertyInfo.GetCustomAttribute (typeof (ConfigurationPropertyAttribute)) != null;
  143. }
  144. /// <summary>
  145. /// INTERNAL: Helper to get either the Json property named (specified by [JsonPropertyName(name)] or the actual property
  146. /// name.
  147. /// </summary>
  148. /// <param name="pi"></param>
  149. /// <returns></returns>
  150. [RequiresDynamicCode ("Uses reflection to access custom attributes")]
  151. internal static string GetJsonPropertyName (PropertyInfo pi)
  152. {
  153. var attr = pi.GetCustomAttribute (typeof (JsonPropertyNameAttribute)) as JsonPropertyNameAttribute;
  154. return attr?.Name ?? pi.Name;
  155. }
  156. /// <summary>
  157. /// Updates (using reflection) the <see cref="PropertyValue"/> from the static <see cref="ConfigurationPropertyAttribute"/>
  158. /// property described in <see cref="PropertyInfo"/>.
  159. /// </summary>
  160. /// <returns></returns>
  161. [RequiresDynamicCode ("Uses reflection to retrieve property values")]
  162. public object? UpdateToCurrentValue ()
  163. {
  164. return PropertyValue = PropertyInfo!.GetValue (null);
  165. }
  166. /// <summary>
  167. /// INTERNAL: Updates <see cref="PropertyValue"/> with the value in <paramref name="source"/> using a deep memberwise copy that
  168. /// copies only the values that <see cref="HasValue"/>.
  169. /// </summary>
  170. /// <param name="source">The source object to copy values from.</param>
  171. /// <returns>The updated property value.</returns>
  172. /// <exception cref="ArgumentException">Thrown when the source type doesn't match the property type.</exception>
  173. [RequiresUnreferencedCode ("Uses DeepCloner which requires types to be registered in SourceGenerationContext")]
  174. [RequiresDynamicCode ("Calls Terminal.Gui.DeepCloner.DeepClone<T>(T)")]
  175. internal object? UpdateFrom (object? source)
  176. {
  177. // If the source (higher-priority layer) doesn't provide a value, keep the existing value
  178. // In the context of layering, a null source means the higher-priority layer doesn't specify a value,
  179. // so we should retain the value from the lower-priority layer.
  180. if (source is null)
  181. {
  182. return PropertyValue;
  183. }
  184. // Process the source based on its type
  185. if (source is ConcurrentDictionary<string, ThemeScope> themeDictSource &&
  186. PropertyValue is ConcurrentDictionary<string, ThemeScope> themeDictDest)
  187. {
  188. UpdateThemeScopeDictionary (themeDictSource, themeDictDest);
  189. }
  190. else if (source is ConcurrentDictionary<string, ConfigProperty> concurrentDictSource &&
  191. PropertyValue is ConcurrentDictionary<string, ConfigProperty> concurrentDictDest)
  192. {
  193. UpdateConfigPropertyConcurrentDictionary (concurrentDictSource, concurrentDictDest);
  194. }
  195. else if (source is Dictionary<string, ConfigProperty> dictSource &&
  196. PropertyValue is Dictionary<string, ConfigProperty> dictDest)
  197. {
  198. UpdateConfigPropertyDictionary (dictSource, dictDest);
  199. }
  200. else if (source is ConfigProperty configProperty)
  201. {
  202. if (configProperty.HasValue)
  203. {
  204. PropertyValue = DeepCloner.DeepClone (configProperty.PropertyValue);
  205. }
  206. }
  207. else if (source is Dictionary<string, Scheme> dictSchemeSource &&
  208. PropertyValue is Dictionary<string, Scheme> dictSchemesDest)
  209. {
  210. UpdateSchemeDictionary (dictSchemeSource, dictSchemesDest);
  211. }
  212. else if (source is Scheme scheme)
  213. {
  214. PropertyValue = new Scheme (scheme); // use copy constructor
  215. }
  216. else
  217. {
  218. // Validate type compatibility for non-dictionary types
  219. ValidateTypeCompatibility (source);
  220. // For non-scope types, perform a deep copy of the source value to ensure immutability
  221. PropertyValue = DeepCloner.DeepClone (source);
  222. }
  223. return PropertyValue;
  224. }
  225. /// <summary>
  226. /// Validates that the source type is compatible with the property type.
  227. /// </summary>
  228. /// <param name="source">The source object to validate.</param>
  229. /// <exception cref="ArgumentException">Thrown when the source type doesn't match the property type.</exception>
  230. private void ValidateTypeCompatibility (object source)
  231. {
  232. Type? underlyingType = Nullable.GetUnderlyingType (PropertyInfo!.PropertyType);
  233. bool isCompatibleType = source.GetType () == PropertyInfo.PropertyType ||
  234. (underlyingType is { } && source.GetType () == underlyingType);
  235. if (!isCompatibleType)
  236. {
  237. throw new ArgumentException (
  238. $"The source object ({PropertyInfo.DeclaringType}.{PropertyInfo.Name}) is not of type {PropertyInfo.PropertyType}."
  239. );
  240. }
  241. }
  242. /// <summary>
  243. /// Updates a Scheme object by selectively applying explicitly set attributes from the source.
  244. /// </summary>
  245. /// <param name="sourceScheme">The source Scheme.</param>
  246. /// <param name="destScheme">The destination Scheme to update.</param>
  247. private void UpdateScheme (Scheme sourceScheme, Scheme destScheme)
  248. {
  249. // We can't modify properties of a record directly, so we need to create a new one
  250. // First, create a clone of the destination to preserve any values
  251. var updatedScheme = new Scheme (destScheme);
  252. //// Use with expressions to update only explicitly set attributes
  253. //// For each role, check if the source has an explicitly set attribute
  254. //if (sourceScheme.Normal.IsExplicitlySet)
  255. //{
  256. // updatedScheme = updatedScheme with { Normal = sourceScheme.Normal };
  257. //}
  258. //if (sourceScheme.HotNormal.IsExplicitlySet)
  259. //{
  260. // updatedScheme = updatedScheme with { HotNormal = sourceScheme.HotNormal };
  261. //}
  262. //if (sourceScheme.Focus.IsExplicitlySet)
  263. //{
  264. // updatedScheme = updatedScheme with { Focus = sourceScheme.Focus };
  265. //}
  266. //if (sourceScheme.HotFocus.IsExplicitlySet)
  267. //{
  268. // updatedScheme = updatedScheme with { HotFocus = sourceScheme.HotFocus };
  269. //}
  270. //if (sourceScheme.Active.IsExplicitlySet)
  271. //{
  272. // updatedScheme = updatedScheme with { Active = sourceScheme.Active };
  273. //}
  274. //if (sourceScheme.HotActive.IsExplicitlySet)
  275. //{
  276. // updatedScheme = updatedScheme with { HotActive = sourceScheme.HotActive };
  277. //}
  278. //if (sourceScheme.Highlight.IsExplicitlySet)
  279. //{
  280. // updatedScheme = updatedScheme with { Highlight = sourceScheme.Highlight };
  281. //}
  282. //if (sourceScheme.Editable.IsExplicitlySet)
  283. //{
  284. // updatedScheme = updatedScheme with { Editable = sourceScheme.Editable };
  285. //}
  286. //if (sourceScheme.ReadOnly.IsExplicitlySet)
  287. //{
  288. // updatedScheme = updatedScheme with { ReadOnly = sourceScheme.ReadOnly };
  289. //}
  290. //if (sourceScheme.Disabled.IsExplicitlySet)
  291. //{
  292. // updatedScheme = updatedScheme with { Disabled = sourceScheme.Disabled };
  293. //}
  294. // Update the PropertyValue with the merged scheme
  295. PropertyValue = updatedScheme;
  296. }
  297. /// <summary>
  298. /// Updates a ThemeScope dictionary with values from a source dictionary.
  299. /// </summary>
  300. /// <param name="source">The source ThemeScope dictionary.</param>
  301. /// <param name="destination">The destination ThemeScope dictionary.</param>
  302. [RequiresUnreferencedCode ("Calls Terminal.Gui.Scope<T>.UpdateFrom(Scope<T>)")]
  303. [RequiresDynamicCode ("Calls Terminal.Gui.Scope<T>.UpdateFrom(Scope<T>)")]
  304. private static void UpdateThemeScopeDictionary (
  305. ConcurrentDictionary<string, ThemeScope> source,
  306. ConcurrentDictionary<string, ThemeScope> destination)
  307. {
  308. foreach (KeyValuePair<string, ThemeScope> scope in source)
  309. {
  310. if (!destination.ContainsKey (scope.Key))
  311. {
  312. destination.TryAdd (scope.Key, scope.Value);
  313. continue;
  314. }
  315. destination [scope.Key].UpdateFrom (scope.Value);
  316. }
  317. }
  318. /// <summary>
  319. /// Updates a ConfigProperty dictionary with values from a source dictionary.
  320. /// </summary>
  321. /// <param name="source">The source ConfigProperty dictionary.</param>
  322. /// <param name="destination">The destination ConfigProperty dictionary.</param>
  323. [RequiresUnreferencedCode ("Calls Terminal.Gui.ConfigProperty.UpdateFrom(Object)")]
  324. [RequiresDynamicCode ("Calls Terminal.Gui.ConfigProperty.UpdateFrom(Object)")]
  325. private static void UpdateConfigPropertyConcurrentDictionary (
  326. ConcurrentDictionary<string, ConfigProperty> source,
  327. ConcurrentDictionary<string, ConfigProperty> destination)
  328. {
  329. foreach (KeyValuePair<string, ConfigProperty> sourceProp in source)
  330. {
  331. // Skip properties without values
  332. if (!sourceProp.Value.HasValue)
  333. {
  334. continue;
  335. }
  336. if (!destination.ContainsKey (sourceProp.Key))
  337. {
  338. // Add the property to the destination
  339. var copy = CreateCopy (sourceProp.Value);
  340. destination.TryAdd (sourceProp.Key, copy);
  341. }
  342. // Update the value in the destination
  343. destination [sourceProp.Key].UpdateFrom (sourceProp.Value);
  344. }
  345. }
  346. /// <summary>
  347. /// Updates a ConfigProperty dictionary with values from a source dictionary.
  348. /// </summary>
  349. /// <param name="source">The source ConfigProperty dictionary.</param>
  350. /// <param name="destination">The destination ConfigProperty dictionary.</param>
  351. [RequiresUnreferencedCode ("Calls Terminal.Gui.ConfigProperty.UpdateFrom(Object)")]
  352. [RequiresDynamicCode ("Calls Terminal.Gui.ConfigProperty.UpdateFrom(Object)")]
  353. private static void UpdateConfigPropertyDictionary (
  354. Dictionary<string, ConfigProperty> source,
  355. Dictionary<string, ConfigProperty> destination)
  356. {
  357. foreach (KeyValuePair<string, ConfigProperty> sourceProp in source)
  358. {
  359. // Skip properties without values
  360. if (!sourceProp.Value.HasValue)
  361. {
  362. continue;
  363. }
  364. if (!destination.ContainsKey (sourceProp.Key))
  365. {
  366. // Add the property to the destination
  367. var copy = CreateCopy (sourceProp.Value);
  368. destination.Add (sourceProp.Key, copy);
  369. }
  370. // Update the value in the destination
  371. destination [sourceProp.Key].UpdateFrom (sourceProp.Value);
  372. }
  373. }
  374. /// <summary>
  375. /// Updates a ConfigProperty dictionary with values from a source dictionary.
  376. /// </summary>
  377. /// <param name="source">The source ConfigProperty dictionary.</param>
  378. /// <param name="destination">The destination ConfigProperty dictionary.</param>
  379. private static void UpdateSchemeDictionary (
  380. Dictionary<string, Scheme> source,
  381. Dictionary<string, Scheme> destination)
  382. {
  383. foreach (KeyValuePair<string, Scheme> sourceProp in source)
  384. {
  385. if (!destination.ContainsKey (sourceProp.Key))
  386. {
  387. // Add the property to the destination
  388. // Schemes are structs are passed by val
  389. destination.Add (sourceProp.Key, sourceProp.Value);
  390. }
  391. // Update the value in the destination
  392. // Schemes are structs are passed by val
  393. destination [sourceProp.Key] = sourceProp.Value;
  394. }
  395. }
  396. #region Initialization
  397. /// <summary>
  398. /// INTERNAL: A cache of all classes that have properties decorated with the <see cref="ConfigurationPropertyAttribute"/>.
  399. /// </summary>
  400. /// <remarks>Is <see langword="null"/> until <see cref="Initialize"/> is called.</remarks>
  401. private static ImmutableSortedDictionary<string, Type>? _classesWithConfigProps;
  402. /// <summary>
  403. /// INTERNAL: Called from the <see cref="ModuleInitializers.InitializeConfigurationManager"/> method to initialize the
  404. /// _classesWithConfigProps dictionary.
  405. /// </summary>
  406. [RequiresDynamicCode ("Uses reflection to scan assemblies for configuration properties. " +
  407. "Only called during initialization and not needed during normal operation. " +
  408. "In AOT environments, ensure all types with ConfigurationPropertyAttribute are preserved.")]
  409. [RequiresUnreferencedCode ("Reflection requires all types with ConfigurationPropertyAttribute to be preserved in AOT. " +
  410. "Use the SourceGenerationContext to register all configuration property types.")]
  411. internal static void Initialize ()
  412. {
  413. if (_classesWithConfigProps is { })
  414. {
  415. return;
  416. }
  417. Dictionary<string, Type> dict = new (StringComparer.InvariantCultureIgnoreCase);
  418. // Process assemblies directly to avoid LINQ overhead
  419. Assembly [] assemblies = AppDomain.CurrentDomain.GetAssemblies ();
  420. foreach (Assembly assembly in assemblies)
  421. {
  422. try
  423. {
  424. if (assembly.IsDynamic)
  425. {
  426. continue;
  427. }
  428. foreach (Type type in assembly.GetTypes ())
  429. {
  430. PropertyInfo [] properties = type.GetProperties ();
  431. // Check if any property has the ConfigurationPropertyAttribute
  432. var hasConfigProp = false;
  433. foreach (PropertyInfo prop in properties)
  434. {
  435. if (HasConfigurationPropertyAttribute (prop))
  436. {
  437. hasConfigProp = true;
  438. break;
  439. }
  440. }
  441. if (hasConfigProp)
  442. {
  443. dict [type.Name] = type;
  444. }
  445. }
  446. }
  447. // Skip problematic assemblies that can't be loaded or analyzed
  448. catch (ReflectionTypeLoadException)
  449. {
  450. continue;
  451. }
  452. catch (BadImageFormatException)
  453. {
  454. continue;
  455. }
  456. }
  457. _classesWithConfigProps = dict.ToImmutableSortedDictionary ();
  458. }
  459. /// <summary>
  460. /// INTERNAL: Retrieves a dictionary of all properties annotated with <see cref="ConfigurationPropertyAttribute"/> from the classes in the module.
  461. /// The dictionary case-insensitive and sorted.
  462. /// The <see cref="ConfigProperty"/> items have <see cref="PropertyInfo"/> set, but not <see cref="PropertyValue"/>.
  463. /// <see cref="Immutable"/> is set to <see langword="true"/>.
  464. /// </summary>
  465. [RequiresDynamicCode ("Uses reflection to scan assemblies for configuration properties. " +
  466. "Only called during initialization and not needed during normal operation. " +
  467. "In AOT environments, ensure all types with ConfigurationPropertyAttribute are preserved.")]
  468. [RequiresUnreferencedCode ("Reflection requires all types with ConfigurationPropertyAttribute to be preserved in AOT. " +
  469. "Use the SourceGenerationContext to register all configuration property types.")]
  470. internal static ImmutableSortedDictionary<string, ConfigProperty> GetAllConfigProperties ()
  471. {
  472. if (_classesWithConfigProps is null)
  473. {
  474. throw new InvalidOperationException ("Initialize has not been called.");
  475. }
  476. // Estimate capacity to reduce resizing operations
  477. int estimatedCapacity = _classesWithConfigProps.Count * 5; // Assume ~5 properties per class
  478. Dictionary<string, ConfigProperty> allConfigProperties = new (estimatedCapacity, StringComparer.InvariantCultureIgnoreCase);
  479. // Process each class with direct iteration instead of LINQ
  480. foreach (KeyValuePair<string, Type> classEntry in _classesWithConfigProps)
  481. {
  482. Type type = classEntry.Value;
  483. // Get all public static/instance properties
  484. BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance;
  485. PropertyInfo [] properties = type.GetProperties (bindingFlags);
  486. foreach (PropertyInfo propertyInfo in properties)
  487. {
  488. // Skip properties without our attribute
  489. if (!HasConfigurationPropertyAttribute (propertyInfo))
  490. {
  491. continue;
  492. }
  493. // Verify the property is static
  494. if (!propertyInfo.GetGetMethod (true)!.IsStatic)
  495. {
  496. throw new InvalidOperationException (
  497. $"Property {propertyInfo.Name} in class {propertyInfo.DeclaringType?.Name} is not static. " +
  498. "[ConfigurationProperty] properties must be static.");
  499. }
  500. // Create config property with cached attribute data
  501. ConfigProperty configProperty = CreateImmutableWithAttributeInfo (propertyInfo);
  502. // Use cached attribute data to determine the key
  503. string key = configProperty.OmitClassName
  504. ? GetJsonPropertyName (propertyInfo)
  505. : $"{propertyInfo.DeclaringType?.Name}.{propertyInfo.Name}";
  506. allConfigProperties.Add (key, configProperty);
  507. }
  508. }
  509. return allConfigProperties.ToImmutableSortedDictionary (StringComparer.InvariantCultureIgnoreCase);
  510. }
  511. #endregion Initialization
  512. }