ConfigurationManager.cs 26 KB

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