ConfigurationManager.cs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  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. if (Locations.HasFlag (ConfigLocations.Runtime) && !string.IsNullOrEmpty (RuntimeConfig))
  231. {
  232. Settings?.Update (RuntimeConfig, "ConfigurationManager.RuntimeConfig", ConfigLocations.Runtime);
  233. }
  234. if (Locations.HasFlag (ConfigLocations.GlobalCurrent))
  235. {
  236. Settings?.Update ($"./.tui/{_configFilename}", ConfigLocations.GlobalCurrent);
  237. }
  238. if (Locations.HasFlag (ConfigLocations.GlobalHome))
  239. {
  240. Settings?.Update ($"~/.tui/{_configFilename}", ConfigLocations.GlobalHome);
  241. }
  242. if (Locations.HasFlag (ConfigLocations.AppCurrent))
  243. {
  244. Settings?.Update ($"./.tui/{AppName}.{_configFilename}", ConfigLocations.AppCurrent);
  245. }
  246. if (Locations.HasFlag (ConfigLocations.AppHome))
  247. {
  248. Settings?.Update ($"~/.tui/{AppName}.{_configFilename}", ConfigLocations.AppHome);
  249. }
  250. ThemeManager.SelectedTheme = Settings!["Theme"].PropertyValue as string ?? "Default";
  251. }
  252. /// <summary>
  253. /// Called when an updated configuration has been applied to the application. Fires the <see cref="Applied"/>
  254. /// event.
  255. /// </summary>
  256. public static void OnApplied ()
  257. {
  258. //Logging.Trace ("");
  259. Applied?.Invoke (null, new ());
  260. // TODO: Refactor ConfigurationManager to not use an event handler for this.
  261. // Instead, have it call a method on any class appropriately attributed
  262. // to update the cached values. See Issue #2871
  263. }
  264. /// <summary>
  265. /// Called when the configuration has been updated from a configuration file or reset. Invokes the
  266. /// <see cref="Updated"/>
  267. /// event.
  268. /// </summary>
  269. public static void OnUpdated ()
  270. {
  271. //Logging.Trace (@"");
  272. Updated?.Invoke (null, new ());
  273. }
  274. /// <summary>Prints any Json deserialization errors that occurred during deserialization to the console.</summary>
  275. public static void PrintJsonErrors ()
  276. {
  277. if (_jsonErrors.Length > 0)
  278. {
  279. Console.WriteLine (
  280. @"Terminal.Gui ConfigurationManager encountered the following errors while deserializing configuration files:"
  281. );
  282. Console.WriteLine (_jsonErrors.ToString ());
  283. }
  284. }
  285. public static void LogJsonErrors ()
  286. {
  287. if (_jsonErrors.Length > 0)
  288. {
  289. Logging.Warning (
  290. @"Encountered the following errors while deserializing configuration files:"
  291. );
  292. Logging.Warning (_jsonErrors.ToString ());
  293. }
  294. }
  295. /// <summary>
  296. /// Resets the state of <see cref="ConfigurationManager"/>. Should be called whenever a new app session (e.g. in
  297. /// <see cref="Application.Init"/> starts. Called by <see cref="Load"/> if the <c>reset</c> parameter is
  298. /// <see langword="true"/>.
  299. /// </summary>
  300. /// <remarks></remarks>
  301. [RequiresUnreferencedCode ("AOT")]
  302. [RequiresDynamicCode ("AOT")]
  303. public static void Reset ()
  304. {
  305. Logging.Trace ($"_allConfigProperties = {_allConfigProperties}");
  306. if (_allConfigProperties is null)
  307. {
  308. Initialize ();
  309. }
  310. ClearJsonErrors ();
  311. Settings = new ();
  312. ThemeManager.Reset ();
  313. AppSettings = new ();
  314. // To enable some unit tests, we only load from resources if the flag is set
  315. if (Locations.HasFlag (ConfigLocations.Default))
  316. {
  317. Settings.UpdateFromResource (
  318. typeof (ConfigurationManager).Assembly,
  319. $"Terminal.Gui.Resources.{_configFilename}",
  320. ConfigLocations.Default
  321. );
  322. }
  323. OnUpdated ();
  324. Apply ();
  325. ThemeManager.Themes? [ThemeManager.SelectedTheme]?.Apply ();
  326. AppSettings?.Apply ();
  327. }
  328. /// <summary>Event fired when the configuration has been updated from a configuration source or reset.</summary>
  329. public static event EventHandler<ConfigurationManagerEventArgs>? Updated;
  330. internal static void AddJsonError (string error)
  331. {
  332. Logging.Trace ($"error = {error}");
  333. _jsonErrors.AppendLine (error);
  334. }
  335. /// <summary>
  336. /// System.Text.Json does not support copying a deserialized object to an existing instance. To work around this,
  337. /// we implement a 'deep, member-wise copy' method.
  338. /// </summary>
  339. /// <remarks>TOOD: When System.Text.Json implements `PopulateObject` revisit https://github.com/dotnet/corefx/issues/37627</remarks>
  340. /// <param name="source"></param>
  341. /// <param name="destination"></param>
  342. /// <returns><paramref name="destination"/> updated from <paramref name="source"/></returns>
  343. internal static object? DeepMemberWiseCopy (object? source, object? destination)
  344. {
  345. ArgumentNullException.ThrowIfNull (destination);
  346. if (source is null)
  347. {
  348. return null!;
  349. }
  350. if (source.GetType () == typeof (SettingsScope))
  351. {
  352. return ((SettingsScope)destination).Update ((SettingsScope)source);
  353. }
  354. if (source.GetType () == typeof (ThemeScope))
  355. {
  356. return ((ThemeScope)destination).Update ((ThemeScope)source);
  357. }
  358. if (source.GetType () == typeof (AppScope))
  359. {
  360. return ((AppScope)destination).Update ((AppScope)source);
  361. }
  362. // If value type, just use copy constructor.
  363. if (source.GetType ().IsValueType || source is string)
  364. {
  365. return source;
  366. }
  367. // HACK: Key is a class, but we want to treat it as a value type so just _keyCode gets copied.
  368. if (source.GetType () == typeof (Key))
  369. {
  370. return source;
  371. }
  372. // Dictionary
  373. if (source.GetType ().IsGenericType
  374. && source.GetType ().GetGenericTypeDefinition ().IsAssignableFrom (typeof (Dictionary<,>)))
  375. {
  376. foreach (object? srcKey in ((IDictionary)source).Keys)
  377. {
  378. if (((IDictionary)destination).Contains (srcKey))
  379. {
  380. ((IDictionary)destination) [srcKey] =
  381. DeepMemberWiseCopy (((IDictionary)source) [srcKey], ((IDictionary)destination) [srcKey]);
  382. }
  383. else
  384. {
  385. ((IDictionary)destination).Add (srcKey, ((IDictionary)source) [srcKey]);
  386. }
  387. }
  388. return destination;
  389. }
  390. // ALl other object types
  391. List<PropertyInfo>? sourceProps = source?.GetType ().GetProperties ().Where (x => x.CanRead).ToList ();
  392. List<PropertyInfo>? destProps = destination?.GetType ().GetProperties ().Where (x => x.CanWrite).ToList ()!;
  393. foreach ((PropertyInfo? sourceProp, PropertyInfo? destProp) in
  394. from sourceProp in sourceProps
  395. where destProps.Any (x => x.Name == sourceProp.Name)
  396. let destProp = destProps.First (x => x.Name == sourceProp.Name)
  397. where destProp.CanWrite
  398. select (sourceProp, destProp))
  399. {
  400. object? sourceVal = sourceProp.GetValue (source);
  401. object? destVal = destProp.GetValue (destination);
  402. if (sourceVal is { })
  403. {
  404. try
  405. {
  406. if (destVal is { })
  407. {
  408. // Recurse
  409. destProp.SetValue (destination, DeepMemberWiseCopy (sourceVal, destVal));
  410. }
  411. else
  412. {
  413. destProp.SetValue (destination, sourceVal);
  414. }
  415. }
  416. catch (ArgumentException e)
  417. {
  418. throw new JsonException ($"Error Applying Configuration Change: {e.Message}", e);
  419. }
  420. }
  421. }
  422. return destination;
  423. }
  424. /// <summary>
  425. /// Retrieves the hard coded default settings (static properites) from the Terminal.Gui library implementation. Used in
  426. /// development of
  427. /// the library to generate the default configuration file.
  428. /// </summary>
  429. /// <remarks>
  430. /// <para>
  431. /// This method is only really useful when using ConfigurationManagerTests to generate the JSON doc that is
  432. /// embedded into Terminal.Gui (during development).
  433. /// </para>
  434. /// <para>
  435. /// WARNING: The <c>Terminal.Gui.Resources.config.json</c> resource has setting definitions (Themes) that are NOT
  436. /// generated by this function. If you use this function to regenerate <c>Terminal.Gui.Resources.config.json</c>,
  437. /// make sure you copy the Theme definitions from the existing <c>Terminal.Gui.Resources.config.json</c> file.
  438. /// </para>
  439. /// </remarks>
  440. [RequiresUnreferencedCode ("AOT")]
  441. [RequiresDynamicCode ("AOT")]
  442. internal static void GetHardCodedDefaults ()
  443. {
  444. if (_allConfigProperties is null)
  445. {
  446. throw new InvalidOperationException ("Initialize must be called first.");
  447. }
  448. Settings = new ();
  449. ThemeManager.GetHardCodedDefaults ();
  450. AppSettings?.RetrieveValues ();
  451. foreach (KeyValuePair<string, ConfigProperty> p in Settings!.Where (cp => cp.Value.PropertyInfo is { }))
  452. {
  453. Settings! [p.Key].PropertyValue = p.Value.PropertyInfo?.GetValue (null);
  454. }
  455. }
  456. /// <summary>
  457. /// Initializes the internal state of ConfigurationManager. Nominally called once as part of application startup
  458. /// to initialize global state. Also called from some Unit Tests to ensure correctness (e.g. Reset()).
  459. /// </summary>
  460. [RequiresUnreferencedCode ("AOT")]
  461. internal static void Initialize ()
  462. {
  463. _allConfigProperties = new ();
  464. _settings = null;
  465. Dictionary<string, Type> classesWithConfigProps = new (StringComparer.InvariantCultureIgnoreCase);
  466. // Get Terminal.Gui.dll classes
  467. IEnumerable<Type> types = from assembly in AppDomain.CurrentDomain.GetAssemblies ()
  468. from type in assembly.GetTypes ()
  469. where type.GetProperties ()
  470. .Any (
  471. prop => prop.GetCustomAttribute (
  472. typeof (SerializableConfigurationProperty)
  473. )
  474. != null
  475. )
  476. select type;
  477. foreach (Type? classWithConfig in types)
  478. {
  479. classesWithConfigProps.Add (classWithConfig.Name, classWithConfig);
  480. }
  481. //Logging.Trace ($"ConfigManager.getConfigProperties found {classesWithConfigProps.Count} classes:");
  482. classesWithConfigProps.ToList ().ForEach (x => Logging.Trace ($" Class: {x.Key}"));
  483. foreach (PropertyInfo? p in from c in classesWithConfigProps
  484. let props = c.Value
  485. .GetProperties (
  486. BindingFlags.Instance
  487. |
  488. BindingFlags.Static
  489. |
  490. BindingFlags.NonPublic
  491. |
  492. BindingFlags.Public
  493. )
  494. .Where (
  495. prop =>
  496. prop.GetCustomAttribute (
  497. typeof (SerializableConfigurationProperty)
  498. ) is
  499. SerializableConfigurationProperty
  500. )
  501. let enumerable = props
  502. from p in enumerable
  503. select p)
  504. {
  505. if (p.GetCustomAttribute (typeof (SerializableConfigurationProperty)) is SerializableConfigurationProperty
  506. scp)
  507. {
  508. if (p.GetGetMethod (true)!.IsStatic)
  509. {
  510. // If the class name is omitted, JsonPropertyName is allowed.
  511. _allConfigProperties!.Add (
  512. scp.OmitClassName
  513. ? ConfigProperty.GetJsonPropertyName (p)
  514. : $"{p.DeclaringType?.Name}.{p.Name}",
  515. new () { PropertyInfo = p, PropertyValue = null }
  516. );
  517. }
  518. else
  519. {
  520. throw new (
  521. $"Property {p.Name} in class {p.DeclaringType?.Name} is not static. All SerializableConfigurationProperty properties must be static."
  522. );
  523. }
  524. }
  525. }
  526. _allConfigProperties = _allConfigProperties!.OrderBy (x => x.Key)
  527. .ToDictionary (
  528. x => x.Key,
  529. x => x.Value,
  530. StringComparer.InvariantCultureIgnoreCase
  531. );
  532. //Logging.Trace ($"Found {_allConfigProperties.Count} properties:");
  533. //_allConfigProperties.ToList ().ForEach (x => Logging.Trace ($" Property: {x.Key}"));
  534. AppSettings = new ();
  535. }
  536. /// <summary>Creates a JSON document with the configuration specified.</summary>
  537. /// <returns></returns>
  538. [RequiresUnreferencedCode ("AOT")]
  539. [RequiresDynamicCode ("AOT")]
  540. internal static string ToJson ()
  541. {
  542. //Logging.Trace ("ConfigurationManager.ToJson()");
  543. return JsonSerializer.Serialize (Settings!, typeof (SettingsScope), SerializerContext);
  544. }
  545. [RequiresUnreferencedCode ("AOT")]
  546. [RequiresDynamicCode ("AOT")]
  547. internal static Stream ToStream ()
  548. {
  549. string json = JsonSerializer.Serialize (Settings!, typeof (SettingsScope), SerializerContext);
  550. // turn it into a stream
  551. var stream = new MemoryStream ();
  552. var writer = new StreamWriter (stream);
  553. writer.Write (json);
  554. writer.Flush ();
  555. stream.Position = 0;
  556. return stream;
  557. }
  558. private static void ClearJsonErrors () { _jsonErrors.Clear (); }
  559. }