ConfigurationManager.cs 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650
  1. global using static Terminal.Gui.ConfigurationManager;
  2. global using CM = Terminal.Gui.ConfigurationManager;
  3. using System.Collections;
  4. using System.Diagnostics;
  5. using System.Diagnostics.CodeAnalysis;
  6. using System.Reflection;
  7. using System.Runtime.Versioning;
  8. using System.Text.Encodings.Web;
  9. using System.Text.Json;
  10. using System.Text.Json.Serialization;
  11. using Microsoft.Extensions.Logging;
  12. #nullable enable
  13. namespace Terminal.Gui;
  14. /// <summary>
  15. /// Provides settings and configuration management for Terminal.Gui applications.
  16. /// <para>
  17. /// Users can set Terminal.Gui settings on a global or per-application basis by providing JSON formatted
  18. /// configuration files. The configuration files can be placed in at <c>.tui</c> folder in the user's home
  19. /// directory (e.g. <c>C:/Users/username/.tui</c>, or <c>/usr/username/.tui</c>), the folder where the Terminal.Gui
  20. /// application was launched from (e.g. <c>./.tui</c> ), or as a resource within the Terminal.Gui application's
  21. /// main assembly.
  22. /// </para>
  23. /// <para>
  24. /// Settings are defined in JSON format, according to this schema:
  25. /// https://gui-cs.github.io/Terminal.GuiV2Docs/schemas/tui-config-schema.json
  26. /// </para>
  27. /// <para>
  28. /// Settings that will apply to all applications (global settings) reside in files named <c>config.json</c>.
  29. /// Settings that will apply to a specific Terminal.Gui application reside in files named
  30. /// <c>appname.config.json</c>, where <c>appname</c> is the assembly name of the application (e.g.
  31. /// <c>UICatalog.config.json</c>).
  32. /// </para>
  33. /// Settings are applied using the following precedence (higher precedence settings overwrite lower precedence
  34. /// settings):
  35. /// <para>
  36. /// 1. Application configuration found in the users' home directory (<c>~/.tui/appname.config.json</c>) --
  37. /// Highest precedence
  38. /// </para>
  39. /// <para>
  40. /// 2. Application configuration found in the directory the app was launched from (
  41. /// <c>./.tui/appname.config.json</c>).
  42. /// </para>
  43. /// <para>3. Application configuration found in the applications' resources (<c>Resources/config.json</c>).</para>
  44. /// <para>4. Global configuration found in the user's home directory (<c>~/.tui/config.json</c>).</para>
  45. /// <para>5. Global configuration found in the directory the app was launched from (<c>./.tui/config.json</c>).</para>
  46. /// <para>
  47. /// 6. Global configuration in <c>Terminal.Gui.dll</c>'s resources (<c>Terminal.Gui.Resources.config.json</c>) --
  48. /// Lowest Precedence.
  49. /// </para>
  50. /// </summary>
  51. [ComponentGuarantees (ComponentGuaranteesOptions.None)]
  52. public static class ConfigurationManager
  53. {
  54. /// <summary>
  55. /// A dictionary of all properties in the Terminal.Gui project that are decorated with the
  56. /// <see cref="SerializableConfigurationProperty"/> attribute. The keys are the property names pre-pended with the
  57. /// class that implements the property (e.g. <c>Application.UseSystemConsole</c>). The values are instances of
  58. /// <see cref="ConfigProperty"/> which hold the property's value and the <see cref="PropertyInfo"/> that allows
  59. /// <see cref="ConfigurationManager"/> to get and set the property's value.
  60. /// </summary>
  61. /// <remarks>Is <see langword="null"/> until <see cref="Initialize"/> is called.</remarks>
  62. [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "<Pending>")]
  63. internal static Dictionary<string, ConfigProperty>? _allConfigProperties;
  64. [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "<Pending>")]
  65. internal static readonly JsonSerializerOptions SerializerOptions = new ()
  66. {
  67. ReadCommentHandling = JsonCommentHandling.Skip,
  68. PropertyNameCaseInsensitive = true,
  69. DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
  70. WriteIndented = true,
  71. Converters =
  72. {
  73. // We override the standard Rune converter to support specifying Glyphs in
  74. // a flexible way
  75. new RuneJsonConverter (),
  76. // Override Key to support "Ctrl+Q" format.
  77. new KeyJsonConverter ()
  78. },
  79. // Enables Key to be "Ctrl+Q" vs "Ctrl\u002BQ"
  80. Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
  81. TypeInfoResolver = SourceGenerationContext.Default
  82. };
  83. [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "<Pending>")]
  84. internal static readonly SourceGenerationContext SerializerContext = new (SerializerOptions);
  85. [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "<Pending>")]
  86. internal static StringBuilder _jsonErrors = new ();
  87. [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "<Pending>")]
  88. private static readonly string _configFilename = "config.json";
  89. /// <summary>The backing property for <see cref="Settings"/>.</summary>
  90. /// <remarks>
  91. /// Is <see langword="null"/> until <see cref="Reset"/> is called. Gets set to a new instance by deserialization
  92. /// (see <see cref="Load"/>).
  93. /// </remarks>
  94. private static SettingsScope? _settings;
  95. /// <summary>Name of the running application. By default, this property is set to the application's assembly name.</summary>
  96. public static string AppName { get; set; } = Assembly.GetEntryAssembly ()?.FullName?.Split (',') [0]?.Trim ()!;
  97. /// <summary>Application-specific configuration settings scope.</summary>
  98. [SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)]
  99. [JsonPropertyName ("AppSettings")]
  100. public static AppScope? AppSettings { get; set; }
  101. /// <summary>
  102. /// Gets and sets the locations where <see cref="ConfigurationManager"/> will look for config files. The value is
  103. /// <see cref="ConfigLocations.All"/>.
  104. /// </summary>
  105. public static ConfigLocations Locations { get; set; } = ConfigLocations.All;
  106. /// <summary>
  107. /// The root object of Terminal.Gui configuration settings / JSON schema. Contains only properties with the
  108. /// <see cref="SettingsScope"/> attribute value.
  109. /// </summary>
  110. public static SettingsScope? Settings
  111. {
  112. [RequiresUnreferencedCode ("AOT")]
  113. [RequiresDynamicCode ("AOT")]
  114. get
  115. {
  116. if (_settings is null)
  117. {
  118. // If Settings is null, we need to initialize it.
  119. Reset ();
  120. }
  121. return _settings;
  122. }
  123. set => _settings = value!;
  124. }
  125. /// <summary>
  126. /// The root object of Terminal.Gui themes manager. Contains only properties with the <see cref="ThemeScope"/>
  127. /// attribute value.
  128. /// </summary>
  129. public static ThemeManager? Themes => ThemeManager.Instance;
  130. /// <summary>
  131. /// Gets or sets whether the <see cref="ConfigurationManager"/> should throw an exception if it encounters an
  132. /// error on deserialization. If <see langword="false"/> (the default), the error is logged and printed to the console
  133. /// when <see cref="Application.Shutdown"/> is called.
  134. /// </summary>
  135. [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
  136. public static bool? ThrowOnJsonErrors { get; set; } = false;
  137. /// <summary>Event fired when an updated configuration has been applied to the application.</summary>
  138. public static event EventHandler<ConfigurationManagerEventArgs>? Applied;
  139. /// <summary>Applies the configuration settings to the running <see cref="Application"/> instance.</summary>
  140. [RequiresUnreferencedCode ("AOT")]
  141. [RequiresDynamicCode ("AOT")]
  142. public static void Apply ()
  143. {
  144. var settings = false;
  145. var themes = false;
  146. var appSettings = false;
  147. try
  148. {
  149. if (string.IsNullOrEmpty (ThemeManager.SelectedTheme))
  150. {
  151. // First start. Apply settings first. This ensures if a config sets Theme to something other than "Default", it gets used
  152. settings = Settings?.Apply () ?? false;
  153. themes = !string.IsNullOrEmpty (ThemeManager.SelectedTheme)
  154. && (ThemeManager.Themes? [ThemeManager.SelectedTheme]?.Apply () ?? false);
  155. }
  156. else
  157. {
  158. // Subsequently. Apply Themes first using whatever the SelectedTheme is
  159. themes = ThemeManager.Themes? [ThemeManager.SelectedTheme]?.Apply () ?? false;
  160. settings = Settings?.Apply () ?? false;
  161. }
  162. appSettings = AppSettings?.Apply () ?? false;
  163. }
  164. catch (JsonException e)
  165. {
  166. if (ThrowOnJsonErrors ?? false)
  167. {
  168. throw;
  169. }
  170. else
  171. {
  172. AddJsonError ($"Error applying Configuration Change: {e.Message}");
  173. }
  174. }
  175. finally
  176. {
  177. if (settings || themes || appSettings)
  178. {
  179. OnApplied ();
  180. }
  181. }
  182. }
  183. /// <summary>Returns an empty Json document with just the $schema tag.</summary>
  184. /// <returns></returns>
  185. public static string GetEmptyJson ()
  186. {
  187. var emptyScope = new SettingsScope ();
  188. emptyScope.Clear ();
  189. return JsonSerializer.Serialize (emptyScope, typeof (SettingsScope), SerializerContext);
  190. }
  191. /// <summary>
  192. /// Gets or sets the in-memory config.json. See <see cref="ConfigLocations.Runtime"/>.
  193. /// </summary>
  194. public static string? RuntimeConfig { get; set; } = """{ }""";
  195. /// <summary>
  196. /// Loads all settings found in the configuration storage locations (<see cref="ConfigLocations"/>). Optionally, resets
  197. /// all settings attributed with
  198. /// <see cref="SerializableConfigurationProperty"/> to the defaults.
  199. /// </summary>
  200. /// <remarks>
  201. /// <para>
  202. /// Use <see cref="Apply"/> to cause the loaded settings to be applied to the running application.
  203. /// </para>
  204. /// </remarks>
  205. /// <param name="reset">
  206. /// If <see langword="true"/> the state of <see cref="ConfigurationManager"/> will be reset to the
  207. /// defaults (<see cref="ConfigLocations.Default"/>).
  208. /// </param>
  209. [RequiresUnreferencedCode ("AOT")]
  210. [RequiresDynamicCode ("AOT")]
  211. public static void Load (bool reset = false)
  212. {
  213. Logging.Trace ($"reset = {reset}");
  214. if (reset)
  215. {
  216. Reset ();
  217. }
  218. if (Locations.HasFlag (ConfigLocations.AppResources))
  219. {
  220. string? embeddedStylesResourceName = Assembly.GetEntryAssembly ()
  221. ?
  222. .GetManifestResourceNames ()
  223. .FirstOrDefault (x => x.EndsWith (_configFilename));
  224. if (string.IsNullOrEmpty (embeddedStylesResourceName))
  225. {
  226. embeddedStylesResourceName = _configFilename;
  227. }
  228. Settings?.UpdateFromResource (Assembly.GetEntryAssembly ()!, embeddedStylesResourceName!, ConfigLocations.AppResources);
  229. }
  230. // TODO: Determine if Runtime should be applied last.
  231. if (Locations.HasFlag (ConfigLocations.Runtime) && !string.IsNullOrEmpty (RuntimeConfig))
  232. {
  233. Settings?.Update (RuntimeConfig, "ConfigurationManager.RuntimeConfig", ConfigLocations.Runtime);
  234. }
  235. if (Locations.HasFlag (ConfigLocations.GlobalCurrent))
  236. {
  237. Settings?.Update ($"./.tui/{_configFilename}", ConfigLocations.GlobalCurrent);
  238. }
  239. if (Locations.HasFlag (ConfigLocations.GlobalHome))
  240. {
  241. Settings?.Update ($"~/.tui/{_configFilename}", ConfigLocations.GlobalHome);
  242. }
  243. if (Locations.HasFlag (ConfigLocations.AppCurrent))
  244. {
  245. Settings?.Update ($"./.tui/{AppName}.{_configFilename}", ConfigLocations.AppCurrent);
  246. }
  247. if (Locations.HasFlag (ConfigLocations.AppHome))
  248. {
  249. Settings?.Update ($"~/.tui/{AppName}.{_configFilename}", ConfigLocations.AppHome);
  250. }
  251. ThemeManager.SelectedTheme = Settings!["Theme"].PropertyValue as string ?? "Default";
  252. }
  253. /// <summary>
  254. /// Called when an updated configuration has been applied to the application. Fires the <see cref="Applied"/>
  255. /// event.
  256. /// </summary>
  257. public static void OnApplied ()
  258. {
  259. //Logging.Trace ("");
  260. Applied?.Invoke (null, new ());
  261. // TODO: Refactor ConfigurationManager to not use an event handler for this.
  262. // Instead, have it call a method on any class appropriately attributed
  263. // to update the cached values. See Issue #2871
  264. }
  265. /// <summary>
  266. /// Called when the configuration has been updated from a configuration file or reset. Invokes the
  267. /// <see cref="Updated"/>
  268. /// event.
  269. /// </summary>
  270. public static void OnUpdated ()
  271. {
  272. //Logging.Trace (@"");
  273. Updated?.Invoke (null, new ());
  274. }
  275. /// <summary>Prints any Json deserialization errors that occurred during deserialization to the console.</summary>
  276. public static void PrintJsonErrors ()
  277. {
  278. if (_jsonErrors.Length > 0)
  279. {
  280. Console.WriteLine (
  281. @"Terminal.Gui ConfigurationManager encountered the following errors while deserializing configuration files:"
  282. );
  283. Console.WriteLine (_jsonErrors.ToString ());
  284. }
  285. }
  286. /// <summary>
  287. /// Logs Json deserialization errors that occurred during deserialization.
  288. /// </summary>
  289. public static void LogJsonErrors ()
  290. {
  291. if (_jsonErrors.Length > 0)
  292. {
  293. Logging.Error (
  294. @"Encountered the following errors while deserializing configuration files:"
  295. );
  296. Logging.Error (_jsonErrors.ToString ());
  297. }
  298. }
  299. /// <summary>
  300. /// Resets the state of <see cref="ConfigurationManager"/>. Should be called whenever a new app session (e.g. in
  301. /// <see cref="Application.Init"/> starts. Called by <see cref="Load"/> if the <c>reset</c> parameter is
  302. /// <see langword="true"/>.
  303. /// </summary>
  304. /// <remarks></remarks>
  305. [RequiresUnreferencedCode ("AOT")]
  306. [RequiresDynamicCode ("AOT")]
  307. public static void Reset ()
  308. {
  309. Logging.Trace ($"_allConfigProperties = {_allConfigProperties}");
  310. if (_allConfigProperties is null)
  311. {
  312. Initialize ();
  313. }
  314. ClearJsonErrors ();
  315. Settings = new ();
  316. ThemeManager.Reset ();
  317. AppSettings = new ();
  318. // To enable some unit tests, we only load from resources if the flag is set
  319. if (Locations.HasFlag (ConfigLocations.Default))
  320. {
  321. Settings.UpdateFromResource (
  322. typeof (ConfigurationManager).Assembly,
  323. $"Terminal.Gui.Resources.{_configFilename}",
  324. ConfigLocations.Default
  325. );
  326. }
  327. OnUpdated ();
  328. Apply ();
  329. ThemeManager.Themes? [ThemeManager.SelectedTheme]?.Apply ();
  330. AppSettings?.Apply ();
  331. }
  332. /// <summary>Event fired when the configuration has been updated from a configuration source or reset.</summary>
  333. public static event EventHandler<ConfigurationManagerEventArgs>? Updated;
  334. internal static void AddJsonError (string error)
  335. {
  336. Logging.Trace ($"error = {error}");
  337. _jsonErrors.AppendLine (error);
  338. }
  339. /// <summary>
  340. /// System.Text.Json does not support copying a deserialized object to an existing instance. To work around this,
  341. /// we implement a 'deep, member-wise copy' method.
  342. /// </summary>
  343. /// <remarks>TOOD: When System.Text.Json implements `PopulateObject` revisit https://github.com/dotnet/corefx/issues/37627</remarks>
  344. /// <param name="source"></param>
  345. /// <param name="destination"></param>
  346. /// <returns><paramref name="destination"/> updated from <paramref name="source"/></returns>
  347. internal static object? DeepMemberWiseCopy (object? source, object? destination)
  348. {
  349. ArgumentNullException.ThrowIfNull (destination);
  350. if (source is null)
  351. {
  352. return null!;
  353. }
  354. if (source.GetType () == typeof (SettingsScope))
  355. {
  356. return ((SettingsScope)destination).Update ((SettingsScope)source);
  357. }
  358. if (source.GetType () == typeof (ThemeScope))
  359. {
  360. return ((ThemeScope)destination).Update ((ThemeScope)source);
  361. }
  362. if (source.GetType () == typeof (AppScope))
  363. {
  364. return ((AppScope)destination).Update ((AppScope)source);
  365. }
  366. // If value type, just use copy constructor.
  367. if (source.GetType ().IsValueType || source is string)
  368. {
  369. return source;
  370. }
  371. // HACK: Key is a class, but we want to treat it as a value type so just _keyCode gets copied.
  372. if (source.GetType () == typeof (Key))
  373. {
  374. return source;
  375. }
  376. // Dictionary
  377. if (source.GetType ().IsGenericType
  378. && source.GetType ().GetGenericTypeDefinition ().IsAssignableFrom (typeof (Dictionary<,>)))
  379. {
  380. foreach (object? srcKey in ((IDictionary)source).Keys)
  381. {
  382. if (((IDictionary)destination).Contains (srcKey))
  383. {
  384. ((IDictionary)destination) [srcKey] =
  385. DeepMemberWiseCopy (((IDictionary)source) [srcKey], ((IDictionary)destination) [srcKey]);
  386. }
  387. else
  388. {
  389. ((IDictionary)destination).Add (srcKey, ((IDictionary)source) [srcKey]);
  390. }
  391. }
  392. return destination;
  393. }
  394. // ALl other object types
  395. List<PropertyInfo>? sourceProps = source?.GetType ().GetProperties ().Where (x => x.CanRead).ToList ();
  396. List<PropertyInfo>? destProps = destination?.GetType ().GetProperties ().Where (x => x.CanWrite).ToList ()!;
  397. foreach ((PropertyInfo? sourceProp, PropertyInfo? destProp) in
  398. from sourceProp in sourceProps
  399. where destProps.Any (x => x.Name == sourceProp.Name)
  400. let destProp = destProps.First (x => x.Name == sourceProp.Name)
  401. where destProp.CanWrite
  402. select (sourceProp, destProp))
  403. {
  404. object? sourceVal = sourceProp.GetValue (source);
  405. object? destVal = destProp.GetValue (destination);
  406. if (sourceVal is { })
  407. {
  408. try
  409. {
  410. if (destVal is { })
  411. {
  412. // Recurse
  413. destProp.SetValue (destination, DeepMemberWiseCopy (sourceVal, destVal));
  414. }
  415. else
  416. {
  417. destProp.SetValue (destination, sourceVal);
  418. }
  419. }
  420. catch (ArgumentException e)
  421. {
  422. throw new JsonException ($"Error Applying Configuration Change: {e.Message}", e);
  423. }
  424. }
  425. }
  426. return destination;
  427. }
  428. /// <summary>
  429. /// Retrieves the hard coded default settings (static properites) from the Terminal.Gui library implementation. Used in
  430. /// development of
  431. /// the library to generate the default configuration file.
  432. /// </summary>
  433. /// <remarks>
  434. /// <para>
  435. /// This method is only really useful when using ConfigurationManagerTests to generate the JSON doc that is
  436. /// embedded into Terminal.Gui (during development).
  437. /// </para>
  438. /// <para>
  439. /// WARNING: The <c>Terminal.Gui.Resources.config.json</c> resource has setting definitions (Themes) that are NOT
  440. /// generated by this function. If you use this function to regenerate <c>Terminal.Gui.Resources.config.json</c>,
  441. /// make sure you copy the Theme definitions from the existing <c>Terminal.Gui.Resources.config.json</c> file.
  442. /// </para>
  443. /// </remarks>
  444. [RequiresUnreferencedCode ("AOT")]
  445. [RequiresDynamicCode ("AOT")]
  446. internal static void GetHardCodedDefaults ()
  447. {
  448. if (_allConfigProperties is null)
  449. {
  450. throw new InvalidOperationException ("Initialize must be called first.");
  451. }
  452. Settings = new ();
  453. ThemeManager.GetHardCodedDefaults ();
  454. AppSettings?.RetrieveValues ();
  455. foreach (KeyValuePair<string, ConfigProperty> p in Settings!.Where (cp => cp.Value.PropertyInfo is { }))
  456. {
  457. Settings! [p.Key].PropertyValue = p.Value.PropertyInfo?.GetValue (null);
  458. }
  459. }
  460. /// <summary>
  461. /// Initializes the internal state of ConfigurationManager. Nominally called once as part of application startup
  462. /// to initialize global state. Also called from some Unit Tests to ensure correctness (e.g. Reset()).
  463. /// </summary>
  464. [RequiresUnreferencedCode ("AOT")]
  465. internal static void Initialize ()
  466. {
  467. _allConfigProperties = new ();
  468. _settings = null;
  469. Dictionary<string, Type> classesWithConfigProps = new (StringComparer.InvariantCultureIgnoreCase);
  470. // Get Terminal.Gui.dll classes
  471. IEnumerable<Type> types = from assembly in AppDomain.CurrentDomain.GetAssemblies ()
  472. from type in assembly.GetTypes ()
  473. where type.GetProperties ()
  474. .Any (
  475. prop => prop.GetCustomAttribute (
  476. typeof (SerializableConfigurationProperty)
  477. )
  478. != null
  479. )
  480. select type;
  481. foreach (Type? classWithConfig in types)
  482. {
  483. classesWithConfigProps.Add (classWithConfig.Name, classWithConfig);
  484. }
  485. //Logging.Trace ($"ConfigManager.getConfigProperties found {classesWithConfigProps.Count} classes:");
  486. classesWithConfigProps.ToList ().ForEach (x => Logging.Trace ($" Class: {x.Key}"));
  487. foreach (PropertyInfo? p in from c in classesWithConfigProps
  488. let props = c.Value
  489. .GetProperties (
  490. BindingFlags.Instance
  491. |
  492. BindingFlags.Static
  493. |
  494. BindingFlags.NonPublic
  495. |
  496. BindingFlags.Public
  497. )
  498. .Where (
  499. prop =>
  500. prop.GetCustomAttribute (
  501. typeof (SerializableConfigurationProperty)
  502. ) is
  503. SerializableConfigurationProperty
  504. )
  505. let enumerable = props
  506. from p in enumerable
  507. select p)
  508. {
  509. if (p.GetCustomAttribute (typeof (SerializableConfigurationProperty)) is SerializableConfigurationProperty
  510. scp)
  511. {
  512. if (p.GetGetMethod (true)!.IsStatic)
  513. {
  514. // If the class name is omitted, JsonPropertyName is allowed.
  515. _allConfigProperties!.Add (
  516. scp.OmitClassName
  517. ? ConfigProperty.GetJsonPropertyName (p)
  518. : $"{p.DeclaringType?.Name}.{p.Name}",
  519. new () { PropertyInfo = p, PropertyValue = null }
  520. );
  521. }
  522. else
  523. {
  524. throw new (
  525. $"Property {p.Name} in class {p.DeclaringType?.Name} is not static. All SerializableConfigurationProperty properties must be static."
  526. );
  527. }
  528. }
  529. }
  530. _allConfigProperties = _allConfigProperties!.OrderBy (x => x.Key)
  531. .ToDictionary (
  532. x => x.Key,
  533. x => x.Value,
  534. StringComparer.InvariantCultureIgnoreCase
  535. );
  536. //Logging.Trace ($"Found {_allConfigProperties.Count} properties:");
  537. //_allConfigProperties.ToList ().ForEach (x => Logging.Trace ($" Property: {x.Key}"));
  538. AppSettings = new ();
  539. }
  540. /// <summary>Creates a JSON document with the configuration specified.</summary>
  541. /// <returns></returns>
  542. [RequiresUnreferencedCode ("AOT")]
  543. [RequiresDynamicCode ("AOT")]
  544. internal static string ToJson ()
  545. {
  546. //Logging.Trace ("ConfigurationManager.ToJson()");
  547. return JsonSerializer.Serialize (Settings!, typeof (SettingsScope), SerializerContext);
  548. }
  549. [RequiresUnreferencedCode ("AOT")]
  550. [RequiresDynamicCode ("AOT")]
  551. internal static Stream ToStream ()
  552. {
  553. string json = JsonSerializer.Serialize (Settings!, typeof (SettingsScope), SerializerContext);
  554. // turn it into a stream
  555. var stream = new MemoryStream ();
  556. var writer = new StreamWriter (stream);
  557. writer.Write (json);
  558. writer.Flush ();
  559. stream.Position = 0;
  560. return stream;
  561. }
  562. private static void ClearJsonErrors () { _jsonErrors.Clear (); }
  563. }