Wie wir die Konfiguration unserer Services von XML nach YAML übersetzt haben

Hintergrund


Unser Unternehmen hat unter anderem mehrere Services (genauer gesagt - 12) entwickelt, die als Backend unserer Systeme dienen. Jeder der Dienste ist ein Windows-Dienst und führt seine spezifischen Aufgaben aus.

Ich möchte alle diese Dienste auf * nix-OS übertragen. Verlassen Sie dazu den Wrapper in Form von Windows-Diensten und wechseln Sie von .NET Framework zu .NET Standard.

Die letzte Anforderung führt dazu, dass Legacy-Code entfernt werden muss, der in .NET Standard nicht unterstützt wird, einschließlich von der Unterstützung für die Konfiguration unserer Server über XML, implementiert mit Klassen von System.Configuration. Gleichzeitig wird das seit langem bestehende Problem behoben, das darin besteht, dass wir in XML-Konfigurationen von Zeit zu Zeit Fehler beim Ändern von Einstellungen gemacht haben (z. B. manchmal haben wir das schließende Tag an der falschen Stelle platziert oder es überhaupt vergessen), aber ein wunderbarer Leser von System.Xml-XML-Konfigurationen. XmlDocument verschluckt solche Konfigurationen stillschweigend und führt zu einem völlig unvorhersehbaren Ergebnis.

Es wurde beschlossen, über die trendige YAML auf Konfiguration umzusteigen. Welche Probleme hatten wir und wie haben wir sie gelöst? In diesem Artikel.

Was haben wir?


Wie lesen wir die Konfiguration aus XML?


Bei den meisten anderen Projekten lesen wir XML auf standardmäßige Weise.

Jeder Dienst verfügt über eine Einstellungsdatei für .NET-Projekte mit dem Namen AppSettings.cs, die alle für den Dienst erforderlichen Einstellungen enthält. Ungefähr so:

[System.Configuration.SettingsProvider(typeof(PortableSettingsProvider))] internal sealed partial class AppSettings : IServerManagerConfigStorage, IWebSettingsStorage, IServerSettingsStorage, IGraphiteAddressStorage, IDatabaseConfigStorage, IBlackListStorage, IKeyCloackConfigFilePathProvider, IPrometheusSettingsStorage, IMetricsConfig { } 


Eine ähnliche Technik zum Trennen von Einstellungen in Schnittstellen macht es bequem, sie später über einen DI-Container zu verwenden.

Die ganze Magie des Speicherns von Einstellungen ist in PortableSettingsProvider (siehe Klassenattribut) sowie in der Designer-Datei AppSettings.Designer.cs verborgen:

 [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "14.0.0.0")] internal sealed partial class AppSettings : global::System.Configuration.ApplicationSettingsBase { private static AppSettings defaultInstance = ((AppSettings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new AppSettings()))); public static AppSettings Default { get { return defaultInstance; } } [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("35016")] public int ListenPort { get { return ((int)(this["ListenPort"])); } set { this["ListenPort"] = value; } } ... 

Wie Sie sehen können, sind hinter den Kulissen alle Eigenschaften verborgen, die wir der Serverkonfiguration hinzufügen, wenn wir sie über den Einstellungsdesigner in Visual Studio bearbeiten.

Unsere oben erwähnte PortableSettingsProvider-Klasse liest die XML-Datei direkt, und das Leseergebnis wird bereits in SettingsProvider verwendet, um Einstellungen in die AppSettings-Eigenschaften zu schreiben.

Ein Beispiel für die XML-Konfiguration, die wir lesen (die meisten Einstellungen sind aus Sicherheitsgründen ausgeblendet):

 <?xml version="1.0" encoding="utf-8"?> <configuration> <configSections> <sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup"> <section name="MetricServer.Properties.Settings" type="System.Configuration.ClientSettingsSection" /> </sectionGroup> </configSections> <userSettings> <MetricServer.Properties.Settings> <setting name="MCXSettings" serializeAs="String"> <value>Inactive, ChartLen: 1000, PrintLen: 50, UseProxy: False</value> </setting> <setting name="KickUnknownAfter" serializeAs="String"> <value>00:00:10</value> </setting> ... </MetricServer.Properties.Settings> </userSettings> </configuration> 

Welche YAML-Dateien möchte ich lesen


So etwas wie das:

 VirtualFeed: MaxChartHistoryLength: 10 Port: 35016 UseThrottling: True ThrottlingIntervalMs: 50000 UseHistoryBroadcast: True CalendarName: "EmptyCalendar" UsMarketFeed: UseImbalances: True 

Übergangsprobleme


Erstens sind die Konfigurationen in XML "flach", in YAML jedoch nicht (Abschnitte und Unterabschnitte werden unterstützt). Dies ist in den obigen Beispielen deutlich zu sehen. Mit XML haben wir das Problem der flachen Einstellungen gelöst, indem wir eigene Parser eingeführt haben, die Zeichenfolgen eines bestimmten Typs in unsere komplexeren Klassen konvertieren können. Ein Beispiel für eine solch komplexe Zeichenfolge:

 <setting name="MCXSettings" serializeAs="String"> <value>Inactive, ChartLen: 1000, PrintLen: 50, UseProxy: False</value> </setting> 

Ich habe keine Lust, solche Transformationen durchzuführen, wenn ich mit YAML arbeite. Gleichzeitig sind wir jedoch durch die vorhandene „flache“ Struktur der AppSettings-Klasse eingeschränkt: Alle Eigenschaften der darin enthaltenen Einstellungen sind in einem Heap zusammengefasst.

Zweitens sind die Konfigurationen unserer Server kein statischer Monolith, wir ändern sie von Zeit zu Zeit direkt im Verlauf der Serverarbeit, d. H. Diese Änderungen müssen in der Lage sein, zur Laufzeit im laufenden Betrieb zu erfassen. Zu diesem Zweck erben wir in der XML-Implementierung unsere AppSettings von INotifyPropertyChanged (tatsächlich wird jede Schnittstelle, die AppSettings implementiert, davon geerbt) und abonnieren die Aktualisierung der Ereignisse der Einstellungseigenschaften. Dieser Ansatz funktioniert, weil die Standardklasse System.Configuration.ApplicationSettingsBase standardmäßig INotifyPropertyChanged implementiert. Ein ähnliches Verhalten muss nach dem Übergang zu YAML beibehalten werden.

Drittens haben wir nicht eine Konfigurationsdatei für jeden Server, sondern zwei so viele: eine mit Standardeinstellungen, die andere mit überschriebenen. Dies ist erforderlich, damit Sie in mehreren Instanzen von Servern desselben Typs, die unterschiedliche Ports abhören und leicht unterschiedliche Einstellungen haben, nicht den gesamten Satz von Einstellungen vollständig kopieren müssen.

Und noch ein Problem : Der Zugriff auf die Einstellungen erfolgt nicht nur über Schnittstellen, sondern auch über den direkten Zugriff auf AppSettings.Default. Ich möchte Sie daran erinnern, wie es in den Backstage-AppSettings.Designer.cs deklariert ist:

 private static AppSettings defaultInstance = ((AppSettings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new AppSettings()))); public static AppSettings Default { get { return defaultInstance; } } 

Basierend auf dem Vorstehenden war es notwendig, einen neuen Ansatz zum Speichern von Einstellungen in AppSettings zu entwickeln.

Lösung


Toolkit


Für das direkte Lesen entschied sich YAML, vorgefertigte Bibliotheken zu verwenden, die über NuGet erhältlich sind:

  • YamlDotNet - github.com/aaubry/YamlDotNet . Aus der Bibliotheksbeschreibung (Übersetzung):
    YamlDotNet ist die .NET-Bibliothek für YAML. YamlDotNet bietet einen Low-Level-Parser und einen YAML-Generator sowie ein High-Level-Objektmodell ähnlich XmlDocument. Ebenfalls enthalten ist eine Serialisierungsbibliothek, mit der Sie Objekte von / zu YAML-Streams lesen und schreiben können.

  • NetEscapades.Configuration - github.com/andrewlock/NetEscapades.Configuration . Dies ist der Konfigurationsanbieter selbst (im Sinne von Microsoft.Extensions.Configuration.IConfigurationSource, der in ASP.NET Core-Anwendungen aktiv verwendet wird), der YAML-Dateien nur mit dem oben genannten YamlDotNet liest.

Lesen Sie hier mehr über die Verwendung dieser Bibliotheken.

Übergang zu YAML


Der Übergang selbst wurde in zwei Schritten durchgeführt: Zuerst haben wir einfach von XML zu YAML gewechselt, aber eine flache Hierarchie von Konfigurationsdateien beibehalten, und dann haben wir Abschnitte in YAML-Dateien eingegeben. Diese Phasen könnten im Prinzip zu einer zusammengefasst werden, und der Einfachheit halber werde ich genau das tun. Alle unten beschriebenen Aktionen wurden nacheinander auf jeden Dienst angewendet.

Vorbereiten einer YML-Datei


Zuerst müssen Sie die YAML-Datei selbst vorbereiten. Wir nennen es den Namen des Projekts (nützlich für zukünftige Integrationstests, die in der Lage sein sollten, mit verschiedenen Servern zu arbeiten und deren Konfigurationen voneinander zu unterscheiden). Legen Sie die Datei direkt im Stammverzeichnis des Projekts neben AppSettings ab:



Speichern wir zunächst in der YML-Datei eine „flache“ Struktur:

 VirtualFeed: "MaxChartHistoryLength: 10, UseThrottling: True, ThrottlingIntervalMs: 50000, UseHistoryBroadcast: True, CalendarName: EmptyCalendar" VirtualFeedPort: 35016 UsMarketFeedUseImbalances: True 

AppSettings mit Einstellungen füllen


Wir übertragen alle Eigenschaften von AppSettings.Designer.cs auf AppSettings.cs und entfernen gleichzeitig die überflüssigen Attribute des Designers und den Code selbst in get / set-parts.

Es war:

 [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("35016")] public int VirtualFeedPort{ get { return ((int)(this["VirtualFeedPort"])); } set { this["VirtualFeedPort"] = value; } } 

Es wurde:

 public int VirtualFeedPort { get; set; } 

Wir werden AppSettings .Designer .cs als unnötig entfernen. Übrigens können Sie jetzt den Abschnitt userSettings in der Datei app.config vollständig entfernen, wenn er sich im Projekt befindet. Dort werden dieselben Standardeinstellungen gespeichert, die wir über den Einstellungsdesigner festlegen.
Mach weiter.

Steuerungseinstellungen im laufenden Betrieb


Da wir in der Lage sein müssen, Aktualisierungen unserer Einstellungen zur Laufzeit abzufangen, müssen wir INotifyPropertyChanged in unseren AppSettings implementieren. Die Basis System.Configuration.ApplicationSettingsBase ist nicht mehr vorhanden. Sie können sich nicht auf Magie verlassen.

Sie können es "auf der Stirn" implementieren: indem Sie eine Implementierung einer Methode hinzufügen, die das gewünschte Ereignis auslöst, und es im Setter jeder Eigenschaft aufrufen. Dies sind jedoch zusätzliche Codezeilen, die zusätzlich über alle Dienste hinweg kopiert werden müssen.

Machen wir es schöner - stellen Sie die zusätzliche Basisklasse AutoNotifier vor, die tatsächlich dasselbe tut, jedoch hinter den Kulissen, genau wie zuvor System.Configuration.ApplicationSettingsBase:

 /// <summary> /// Implements <see cref="INotifyPropertyChanged"/> for classes with a lot of public properties (ie AppSettings). /// This implementation is: /// - fairly slow, so don't use it for classes where getting/setting of properties is often operation; /// - not for properties described in inherited classes of 2nd level (bad idea: Inherit2 -> Inherit1 -> AutoNotifier; good idea: sealed Inherit -> AutoNotifier) /// </summary> public abstract class AutoNotifier : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private readonly ConcurrentDictionary<string, object> _wrappedValues = new ConcurrentDictionary<string, object>(); //just to avoid manual writing a lot of fields protected T Get<T>([CallerMemberName] string propertyName = null) { return (T)_wrappedValues.GetValueOrDefault(propertyName, () => default(T)); } protected void Set<T>(T value, [CallerMemberName] string propertyName = null) { // ReSharper disable once AssignNullToNotNullAttribute _wrappedValues.AddOrUpdate(propertyName, value, (s, o) => value); OnPropertyChanged(propertyName); } public object this[string propertyName] { get { return Get<object>(propertyName); } set { Set(value, propertyName); } } protected void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } 

Hier können Sie mit dem Attribut [CallerMemberName] automatisch den Eigenschaftsnamen des aufrufenden Objekts abrufen, d. H. AppSettings

Jetzt können wir unsere AppSettings von dieser Basisklasse AutoNotifier erben, und dann wird jede Eigenschaft leicht geändert:

 public int VirtualFeedPort { get { return Get<int>(); } set { Set(value); } } 

Mit diesem Ansatz sehen unsere AppSettings-Klassen, die sogar viele Einstellungen enthalten, kompakt aus und implementieren gleichzeitig INotifyPropertyChanged vollständig.

Ja, ich weiß, dass es möglich wäre, etwas mehr Magie einzuführen, indem Sie beispielsweise Castle.DynamicProxy.IInterceptor verwenden, Änderungen an den erforderlichen Eigenschaften abfangen und dort Ereignisse auslösen. Aber eine solche Entscheidung schien mir zu überladen.

Lesen von Einstellungen aus einer YAML-Datei


Der nächste Schritt besteht darin, den Leser der YAML-Konfiguration selbst hinzuzufügen. Dies geschieht irgendwo näher am Start des Dienstes. Wenn wir unnötige Details verbergen, die nicht mit dem diskutierten Thema zusammenhängen, erhalten wir etwas Ähnliches:

 public static IServerConfigurationProvider LoadServerConfiguration(IReadOnlyDictionary<Type, string> allSections) { IConfigurationBuilder builder = new ConfigurationBuilder().SetBasePath(ConfigFiles.BasePath); foreach (string configFile in configFiles) { string directory = Path.GetDirectoryName(configFile); if (!string.IsNullOrEmpty(directory)) //can be empty if relative path is used { Directory.CreateDirectory(directory); } builder = builder.AddYamlFile(configFile, optional: true, reloadOnChange: true); } IConfigurationRoot config = builder.Build(); // load prepared files and merge them return new ServerConfigurationProvider<TAppSettings>(config, allSections); } 

In dem vorgestellten Code ist ConfigurationBuilder wahrscheinlich nicht von besonderem Interesse - alle Arbeiten damit ähneln der Arbeit mit Konfigurationen in ASP.NET Core. Die folgenden Punkte sind jedoch von Interesse. Erstens hatten wir "out of the box" auch die Möglichkeit, Einstellungen aus mehreren Dateien zu kombinieren. Dies setzt voraus, dass mindestens zwei Konfigurationsdateien pro Server vorhanden sind, wie oben erwähnt. Zweitens übergeben wir die gesamte Lesekonfiguration an einen bestimmten ServerConfigurationProvider. Warum?

Abschnitte in der YAML-Datei


Wir werden diese Frage später beantworten und nun zu der Anforderung zurückkehren, hierarchisch strukturierte Einstellungen in einer YML-Datei zu speichern.

Die Implementierung ist im Prinzip recht einfach. Zunächst stellen wir in der YML-Datei die Struktur vor, die wir benötigen:

 VirtualFeed: MaxChartHistoryLength: 10 Port: 35016 UseThrottling: True ThrottlingIntervalMs: 50000 UseHistoryBroadcast: True CalendarName: "EmptyCalendar" UsMarketFeed: UseImbalances: True 

Gehen wir jetzt zu AppSettings und bringen ihm bei, wie wir unsere Eigenschaften in Abschnitte unterteilen können. Irgendwie so:

 public sealed class AppSettings : AutoNotifier, IWebSettingsStorage, IServerSettingsStorage, IServerManagerAddressStorage, IGlobalCredentialsStorage, IGraphiteAddressStorage, IDatabaseConfigStorage, IBlackListStorage, IKeyCloackConfigFilePathProvider, IPrometheusSettingsStorage, IHeartBeatConfig, IConcurrentAcceptorProperties, IMetricsConfig { public static IReadOnlyDictionary<Type, string> Sections { get; } = new Dictionary<Type, string> { {typeof(IDatabaseConfigStorage), "Database"}, {typeof(IWebSettingsStorage), "Web"}, {typeof(IServerSettingsStorage), "Server"}, {typeof(IConcurrentAcceptorProperties), "ConcurrentAcceptor"}, {typeof(IGraphiteAddressStorage), "Graphite"}, {typeof(IKeyCloackConfigFilePathProvider), "Keycloak"}, {typeof(IPrometheusSettingsStorage), "Prometheus"}, {typeof(IHeartBeatConfig), "Heartbeat"}, {typeof(IServerManagerAddressStorage), "ServerManager"}, {typeof(IGlobalCredentialsStorage), "GlobalCredentials"}, {typeof(IBlackListStorage), "Blacklist"}, {typeof(IMetricsConfig), "Metrics"} }; ... 

Wie Sie sehen können, haben wir AppSettings direkt ein Wörterbuch hinzugefügt, wobei die Schlüssel die Arten von Schnittstellen sind, die von der AppSettings-Klasse implementiert werden, und die Werte die Überschriften der entsprechenden Abschnitte sind. Jetzt können wir die Hierarchie in der YML-Datei mit der Hierarchie der Eigenschaften in AppSettings vergleichen (obwohl nicht tiefer als eine Verschachtelungsebene, aber in unserem Fall war dies ausreichend).

Warum machen wir das hier - in AppSettings? Weil wir auf diese Weise die Informationen über die Einstellungen für verschiedene Entitäten nicht verbreiten und dies außerdem der natürlichste Ort ist, weil in jedem Dienst und dementsprechend in jedem AppSettings einen eigenen Einstellungsbereich.

Wenn Sie keine Hierarchie in den Einstellungen benötigen?


Im Prinzip ist es ein seltsamer Fall, aber wir hatten ihn genau in der ersten Phase, als wir einfach von XML zu YAML wechselten, ohne die Vorteile von YAML zu nutzen.

In diesem Fall kann nicht die gesamte Liste der Abschnitte gespeichert werden, und ServerConfigurationProvider ist viel einfacher (wird später erläutert).

Der wichtige Punkt ist jedoch, dass wir, wenn wir uns entscheiden, eine flache Hierarchie zu verlassen, nur die Anforderung erfüllen können, den Zugriff auf Einstellungen über AppSettings.Default aufrechtzuerhalten. Fügen Sie dazu hier einen so einfachen öffentlichen Konstruktor in AppSettings hinzu:

 public static AppSettings Default { get; } public AppSettings() { Default = this; } 

Jetzt können wir weiterhin überall über AppSettings.Default auf die Einstellungsklasse zugreifen (vorausgesetzt, die Einstellungen wurden bereits über IConfigurationRoot in ServerConfigurationProvider gelesen und AppSettings wurde entsprechend instanziiert).

Wenn eine flache Hierarchie nicht akzeptabel ist, müssen Sie AppSettings ohnehin loswerden. Standardmäßig überall per Code und mit Einstellungen nur über Schnittstellen arbeiten (was im Prinzip gut ist). Warum so - es wird weiter klar.

ServerConfigurationProvider


Die zuvor erwähnte spezielle ServerConfigurationProvider-Klasse befasst sich mit der Magie, die es Ihnen ermöglicht, mit der neuen hierarchischen YAML-Konfiguration mit nur flachen AppSettings vollständig zu arbeiten.

Wenn Sie nicht warten können, ist es hier.

Vollständiger ServerConfigurationProvider-Code
 /// <summary> /// Provides different configurations for current server /// </summary> public class ServerConfigurationProvider<TAppSettings> : IServerConfigurationProvider where TAppSettings : new() { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private readonly IConfigurationRoot _configuration; private readonly IReadOnlyDictionary<Type, string> _sectionsByInterface; private readonly IReadOnlyDictionary<string, Type> _interfacesBySections; /// <summary> /// Section name -> config /// </summary> private readonly ConcurrentDictionary<string, TAppSettings> _cachedSections; public ServerConfigurationProvider(IConfigurationRoot configuration, IReadOnlyDictionary<Type, string> allSections) { _configuration = configuration; _cachedSections = new ConcurrentDictionary<string, TAppSettings>(); _sectionsByInterface = allSections; var interfacesBySections = new Dictionary<string, Type>(); foreach (KeyValuePair<Type, string> interfaceAndSection in _sectionsByInterface) { //section names must be unique interfacesBySections.Add(interfaceAndSection.Value, interfaceAndSection.Key); } _interfacesBySections = interfacesBySections; _configuration.GetReloadToken()?.RegisterChangeCallback(OnConfigurationFileChanged, null); } private void OnConfigurationFileChanged(object _) { UpdateCache(); } private void UpdateCache() { foreach (string sectionName in _cachedSections.Keys) { Type sectionInterface = _interfacesBySections[sectionName]; TAppSettings newSection = ReadSection(sectionName, sectionInterface); TAppSettings oldSection; if (_cachedSections.TryGetValue(sectionName, out oldSection)) { UpdateSection(oldSection, newSection); } } } private void UpdateSection(TAppSettings oldConfig, TAppSettings newConfig) { foreach (PropertyInfo propertyInfo in typeof(TAppSettings).GetProperties().Where(p => p.GetMethod != null && p.SetMethod != null)) { propertyInfo.SetValue(newConfig, propertyInfo.GetValue(oldConfig)); } } public IEnumerable<Type> AllSections => _sectionsByInterface.Keys; public TSettingsSectionInterface FindSection<TSettingsSectionInterface>() where TSettingsSectionInterface : class { return (TSettingsSectionInterface)FindSection(typeof(TSettingsSectionInterface)); } [CanBeNull] public object FindSection(Type sectionInterface) { string sectionName = FindSectionName(sectionInterface); if (sectionName == null) { return null; } //we must return same instance of settings for same requested section (otherwise changing of settings will lead to inconsistent state) return _cachedSections.GetOrAdd(sectionName, typeName => ReadSection(sectionName, sectionInterface)); } private string FindSectionName(Type sectionInterface) { string sectionName; if (!_sectionsByInterface.TryGetValue(sectionInterface, out sectionName)) { Logger.Debug("This server doesn't contain settings for {0}", sectionInterface.FullName); return null; } return sectionName; } private TAppSettings ReadSection(string sectionName, Type sectionInterface) { TAppSettings parsed; try { IConfigurationSection section = _configuration.GetSection(sectionName); CheckSection(section, sectionName, sectionInterface); parsed = section.Get<TAppSettings>(); if (parsed == null) { //means that this section is empty or all its properties are empty return new TAppSettings(); } ReadArrays(parsed, section); } catch (Exception ex) { Logger.Fatal(ex, "Something wrong during reading section {0} in config", sectionName.SafeSurround()); throw; } return parsed; } /// <summary> /// Manual reading of array properties in config /// </summary> private void ReadArrays(TAppSettings settings, IConfigurationSection section) { foreach (PropertyInfo propertyInfo in GetPublicProperties(typeof(TAppSettings), needSetters: true).Where(p => typeof(IEnumerable<string>).IsAssignableFrom(p.PropertyType))) { ClearDefaultArrayIfOverridenExists(section.Key, propertyInfo.Name); IConfigurationSection enumerableProperty = section.GetSection(propertyInfo.Name); propertyInfo.SetValue(settings, enumerableProperty.Get<IEnumerable<string>>()); } } /// <summary> /// Clears array property from default config to use overriden one. /// Standard implementation merges default and overriden array by indexes - this is not what we need /// </summary> private void ClearDefaultArrayIfOverridenExists(string sectionName, string propertyName) { List<IConfigurationProvider> providers = _configuration.Providers.ToList(); if (providers.Count == 0) { return; } string propertyTemplate = $"{sectionName}:{propertyName}:"; if (!providers[providers.Count - 1].TryGet($"{propertyTemplate}{0}", out _)) { //we should use array from default config, because overriden config has no overriden array return; } foreach (IConfigurationProvider provider in providers.Take(providers.Count - 1)) { for (int i = 0; ; i++) { string propertyInnerName = $"{propertyTemplate}{i}"; if (!provider.TryGet(propertyInnerName, out _)) { break; } provider.Set(propertyInnerName, null); } } } private void CheckSection(IConfigurationSection section, string sectionName, Type sectionInterface) { ICollection<PropertyInfo> properties = GetPublicProperties(sectionInterface, needSetters: false); var configProperties = new HashSet<string>(section.GetChildren().Select(c => c.Key)); foreach (PropertyInfo propertyInfo in properties) { if (!configProperties.Remove(propertyInfo.Name)) { if (propertyInfo.PropertyType != typeof(string) && typeof(IEnumerable).IsAssignableFrom(propertyInfo.PropertyType)) { //no way to distinguish absent array and empty array :( Logger.Debug("Property {0} has no valuable items in configs section {1}", propertyInfo.Name, sectionName.SafeSurround()); } else { Logger.Fatal("Property {0} not found in configs section {1}", propertyInfo.Name, sectionName.SafeSurround()); } } } if (configProperties.Any()) { Logger.Fatal("Unexpected config properties {0} in configs section {1}", configProperties.SafeSurroundAndJoin(), sectionName.SafeSurround()); } } private static ICollection<PropertyInfo> GetPublicProperties(Type type, bool needSetters) { if (!type.IsInterface) { return type.GetProperties().Where(x => x.GetMethod != null && (!needSetters || x.SetMethod != null)).ToArray(); } var propertyInfos = new List<PropertyInfo>(); var considered = new List<Type>(); var queue = new Queue<Type>(); considered.Add(type); queue.Enqueue(type); while (queue.Count > 0) { Type subType = queue.Dequeue(); foreach (Type subInterface in subType.GetInterfaces()) { if (considered.Contains(subInterface)) { continue; } considered.Add(subInterface); queue.Enqueue(subInterface); } PropertyInfo[] typeProperties = subType.GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance); IEnumerable<PropertyInfo> newPropertyInfos = typeProperties.Where(x => x.GetMethod != null && (!needSetters || x.SetMethod != null) && !propertyInfos.Contains(x)); propertyInfos.InsertRange(0, newPropertyInfos); } return propertyInfos; } } 


ServerConfigurationProvider wird von der AppSettings-Einstellungsklasse parametrisiert:
 public class ServerConfigurationProvider<TAppSettings> : IServerConfigurationProvider where TAppSettings : new() 

Auf diese Weise können Sie es, wie Sie vielleicht vermuten, sofort in allen Diensten verwenden.

Die Lesekonfiguration selbst (IConfigurationRoot) sowie das oben erwähnte Abschnittswörterbuch (AppSettings.Sections) werden an den Konstruktor übergeben. Es gibt ein Abonnement für Dateiaktualisierungen (möchten wir diese Änderungen im Falle einer Änderung der YML-Datei sofort auf uns übertragen?):

 _configuration.GetReloadToken()?.RegisterChangeCallback(OnConfigurationFileChanged, null); ... private void OnConfigurationFileChanged(object _) { foreach (string sectionName in _cachedSections.Keys) { Type sectionInterface = _interfacesBySections[sectionName]; TAppSettings newSection = ReadSection(sectionName, sectionInterface); TAppSettings oldSection; if (_cachedSections.TryGetValue(sectionName, out oldSection)) { UpdateSection(oldSection, newSection); } } } 

Wie Sie hier sehen können, gehen wir beim Aktualisieren der YML-Datei alle uns bekannten Abschnitte durch und lesen sie jeweils. Wenn der Abschnitt dann bereits früher im Cache gelesen wurde (d. H. Von einer Klasse bereits irgendwo im Code angefordert wurde), schreiben wir die alten Werte im Cache mit neuen um.

Es scheint - warum jeden Abschnitt lesen, warum nicht nur diejenigen lesen, die sich im Cache befinden (d. H. Gefordert werden)? Weil wir beim Lesen des Abschnitts eine Überprüfung auf die richtige Konfiguration implementiert haben. Und bei falschen Einstellungen werden die entsprechenden Warnungen ausgegeben und Probleme protokolliert. Es ist besser, sich so schnell wie möglich über Probleme bei den Konfigurationsänderungen zu informieren, aus denen wir sofort alle Abschnitte lesen.

Das Aktualisieren alter Werte im Cache mit neuen Werten ist trivial genug:

 private void UpdateSection(TAppSettings oldConfig, TAppSettings newConfig) { foreach (PropertyInfo propertyInfo in typeof(TAppSettings).GetProperties().Where(p => p.GetMethod != null && p.SetMethod != null)) { propertyInfo.SetValue(newConfig, propertyInfo.GetValue(oldConfig)); } } 

Das Lesen von Abschnitten ist jedoch nicht so einfach:

 private TAppSettings ReadSection(string sectionName, Type sectionInterface) { TAppSettings parsed; try { IConfigurationSection section = _configuration.GetSection(sectionName); CheckSection(section, sectionName, sectionInterface); parsed = section.Get<TAppSettings>(); if (parsed == null) { //means that this section is empty or all its properties are empty return new TAppSettings(); } ReadArrays(parsed, section); } catch (Exception ex) { Logger.Fatal(ex, "Something wrong during reading section {0} in config", sectionName.SafeSurround()); throw; } return parsed; } 

Hier lesen wir zunächst den Abschnitt selbst mit dem Standard IConfigurationRoot.GetSection. - .

: section.Get YAML- — ( , .. ) , .

:

 VirtualFeed: Names: [] 

VirtualFeed Names , YAML-, , , VirtualFeed . .

IEnumerable- . « » .

 ReadArrays(parsed, section); ... /// <summary> /// Manual reading of array properties in config /// </summary> private void ReadArrays(TAppSettings settings, IConfigurationSection section) { foreach (PropertyInfo propertyInfo in GetPublicProperties(typeof(TAppSettings), needSetters: true).Where(p => typeof(IEnumerable<string>).IsAssignableFrom(p.PropertyType))) { ClearDefaultArrayIfOverridenExists(section.Key, propertyInfo.Name); IConfigurationSection enumerableProperty = section.GetSection(propertyInfo.Name); propertyInfo.SetValue(settings, enumerableProperty.Get<IEnumerable<string>>()); } } /// <summary> /// Clears array property from default config to use overriden one. /// Standard implementation merges default and overriden array by indexes - this is not what we need /// </summary> private void ClearDefaultArrayIfOverridenExists(string sectionName, string propertyName) { List<IConfigurationProvider> providers = _configuration.Providers.ToList(); if (providers.Count == 0) { return; } string propertyTemplate = $"{sectionName}:{propertyName}:"; if (!providers[providers.Count - 1].TryGet($"{propertyTemplate}{0}", out _)) { //we should use array from default config, because overriden config has no overriden array return; } foreach (IConfigurationProvider provider in providers.Take(providers.Count - 1)) { for (int i = 0; ; i++) { string propertyInnerName = $"{propertyTemplate}{i}"; if (!provider.TryGet(propertyInnerName, out _)) { break; } provider.Set(propertyInnerName, null); } } } 

, , IEnumerable «», . : -? — , , -, . , ( ) IConfigurationSection, . - .

ReadSection : FindSection.

 [CanBeNull] public object FindSection(Type sectionInterface) { string sectionName = FindSectionName(sectionInterface); if (sectionName == null) { return null; } //we must return same instance of settings for same requested section (otherwise changing of settings will lead to inconsistent state) return _cachedSections.GetOrAdd(sectionName, typeName => ReadSection(sectionName, sectionInterface)); } 

, , AppSettings.Default: ( ) FindSection AppSettings, , , , AppSettings.Default, , ( — NULL 0).

:

 private void CheckSection(IConfigurationSection section, string sectionName, Type sectionInterface) { ICollection<PropertyInfo> properties = GetPublicProperties(sectionInterface, needSetters: false); var configProperties = new HashSet<string>(section.GetChildren().Select(c => c.Key)); foreach (PropertyInfo propertyInfo in properties) { if (!configProperties.Remove(propertyInfo.Name)) { if (propertyInfo.PropertyType != typeof(string) && typeof(IEnumerable).IsAssignableFrom(propertyInfo.PropertyType)) { //no way to distinguish absent array and empty array :( Logger.Debug("Property {0} has no valuable items in configs section {1}", propertyInfo.Name, sectionName.SafeSurround()); } else { Logger.Fatal("Property {0} not found in configs section {1}", propertyInfo.Name, sectionName.SafeSurround()); } } } if (configProperties.Any()) { Logger.Fatal("Unexpected config properties {0} in configs section {1}", configProperties.SafeSurroundAndJoin(), sectionName.SafeSurround()); } } 

( — ). : , , , - . , - . , , .. , , , .

— , «--»? , , , , — , , . , , . , , .

GetPublicProperties, , , . , , , , , , , , , .


:

 /// <summary> /// Provides different configurations for current server /// </summary> public interface IServerConfigurationProvider { TSettingsSectionInterface FindSection<TSettingsSectionInterface>() where TSettingsSectionInterface : class; object FindSection(Type sectionInterface); IEnumerable<Type> AllSections { get; } } 

— FindSection — . Irgendwie so:

 IThreadPoolProperties threadPoolProperties = ConfigurationProvider.FindSection<IThreadPoolProperties>(); 

— .


IoC- Castle Windsor. . , .

Extension-, , :

 public static class ServerConfigurationProviderExtensions { public static void RegisterAllConfigurationSections(this IWindsorContainer container, IServerConfigurationProvider configurationProvider) { Register(container, configurationProvider, configurationProvider.AllSections.ToArray()); } public static void Register(this IWindsorContainer container, IServerConfigurationProvider configurationProvider, params Type[] configSections) { var registrations = new IRegistration[configSections.Length]; for (int i = 0; i < registrations.Length; i++) { Type configSection = configSections[i]; object section = configurationProvider.FindSection(configSection); registrations[i] = Component.For(configSection).Instance(section).Named(configSection.FullName); } container.Register(registrations); } } 

( AllSections IServerConfigurationProvider).

, ServerConfigurationProvider, ServerConfigurationProvider Windsor.
, , FindSection IServerConfigurationProvider.

Windsor Extension-:

 container.RegisterAllConfigurationSections(configProvider); 

Fazit



XML YAML, .

YAML-, XML, , .

YAML, . , , . , .

- « ». , YAML- ( -).

— , « ».


, - , -, .. , - - .

AppSettings.Default, , .

Source: https://habr.com/ru/post/de438362/


All Articles