ConfigProperty.cs 24 KB

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