ConfigProperty.cs 24 KB

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