我们如何将服务的配置从XML转换为YAML

背景知识


除其他事项外,我们公司已开发了几种服务(更确切地说是12种),可作为我们系统的后端。 每个服务都是Windows服务,并执行其特定任务。

我想将所有这些服务转移到* nix-OS。 为此,请放弃Windows服务形式的包装,然后从.NET Framework切换到.NET Standard。

最后一个要求导致需要摆脱一些旧版代码,.NET Standard中不支持该旧版代码,包括 支持通过XML配置我们的服务器,使用System.Configuration中的类实现。 同时,这解决了一个长期存在的问题,该问题涉及以下事实:在XML-configs中,更改设置时有时会犯错误(例如,有时我们将结束标记放在错误的位置或完全忘记了它),但却是System.Xml XML-configs的绝佳阅读者。 XmlDocument默默地吞下了此类配置,从而产生了完全无法预测的结果。

决定通过流行的YAML切换到配置。 在本文中,我们面临哪些问题以及如何解决这些问题。

我们有什么


我们如何从XML读取配置


对于大多数其他项目,我们以标准方式读取XML。

每个服务都有一个.NET项目的设置文件,称为AppSettings.cs,其中包含该服务所需的所有设置。 像这样:

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


将设置分为界面的类似技术可以方便以后通过DI容器使用它们。

实际上,存储设置的所有主要魔术都隐藏在PortableSettingsProvider中(请参阅class属性)以及设计器文件AppSettings.Designer.cs中:

 [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; } } ... 

如您所见,“幕后”隐藏了所有通过Visual Studio中的设置设计器编辑服务器配置时添加到服务器配置的属性。

上面提到的我们的PortableSettingsProvider类直接读取XML文件,并且读取结果已在SettingsProvider中用于将设置写入AppSettings属性。

我们正在阅读的XML配置示例(出于安全原因,大多数设置都被隐藏了):

 <?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> 

我想阅读哪些YAML文件


像这样:

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

过渡问题


首先, XML中配置是“扁平的”,但在YAML中则不是(支持节和小节)。 在上面的示例中可以清楚地看到这一点。 使用XML,我们通过引入自己的解析器解决了平面设置的问题,该解析器可以将某种类型的字符串转换为更复杂的类。 这样的复杂字符串的示例:

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

使用YAML时,我不希望进行此类转换。 但是同时,我们受到AppSettings类现有的“扁平”结构的限制:该类中设置的所有属性都堆积在一个堆中。

其次,我们的服务器配置不是静态的整体配置,我们会在服务器工作期间(即, 这些更改需要能够在运行时动态捕捉。 为此,在XML实现中,我们从INotifyPropertyChanged继承了AppSettings(实际上,实现AppSettings的每个接口都继承自它),并订阅设置属性事件的更新。 这种方法之所以有效,是因为现成的基类System.Configuration.ApplicationSettingsBase实现了INotifyPropertyChanged。 过渡到YAML后,必须保持类似的行为。

第三,实际上每个服务器没有一个配置文件,但是有两个配置文件:一个具有默认设置,另一个具有被覆盖的设置。 这是必需的,以便在相同类型的服务器的多个实例中的每个实例中,侦听不同的端口并且设置稍有不同,您不必完全复制整个设置。

还有一个问题 -不仅可以通过界面访问设置,还可以通过直接访问AppSettings.Default来访问设置。 让我提醒您如何在后台AppSettings.Designer.cs中声明它:

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

基于上述内容,有必要提出一种在AppSettings中存储设置的新方法。

解决方案


工具包


为了直接阅读,YAML决定使用通过NuGet提供的现成库:

  • YamlDotNet-github.com/aaubry/YamlDotNet 。 从库描述(翻译):
    YamlDotNet是YAML的.NET库。 YamlDotNet提供了低级解析器和YAML生成器,以及类似于XmlDocument的高级对象模型。 还包括一个序列化库,使您可以从YAML流中读取对象或将对象写入YAML流。

  • NetEscapades.Configuration-github.com/andrewlock/NetEscapades.Configuration 。 这是配置提供程序本身(就Microsoft.Extensions.Configuration.IConfigurationSource而言,在ASP.NET Core应用程序中活跃使用),它仅使用上述YamlDotNet读取YAML文件。

在此处阅读有关如何使用这些库的更多信息。

过渡到YAML


过渡本身分两个阶段进行:首先,我们简单地从XML切换到YAML,但是保留了配置文件的统一层次结构,然后在YAML文件中输入了部分。 从原则上讲,这些阶段可以合并为一个阶段,为了简化演示,我将这样做。 下文所述的所有操作均依次应用于每个服务。

准备一个YML文件


首先,您需要准备YAML文件本身。 我们称其为项目的名称(用于将来的集成测试,该名称应该能够与不同的服务器一起使用,并区分它们之间的配置),将文件直接放在项目的根目录中,位于AppSettings旁边:



在YML文件中,对于初学者来说,我们保存一个“扁平”结构:

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

使用设置属性填充AppSettings


我们将所有属性从AppSettings.Designer.cs转移到AppSettings.cs,同时摆脱了设计器的多余属性以及get / set-parts中的代码本身。

那是:

 [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; } } 

它变成了:

 public int VirtualFeedPort { get; set; } 

如有必要,我们将完全删除AppSettings .Designer .cs。 现在,顺便说一下,如果项目中存在app.config文件,则可以完全摆脱它的userSettings部分-相同的默认设置存储在该位置,我们通过设置设计器指定该默认设置。
来吧

即时控制设定


由于我们需要能够在运行时捕获设置的更新,因此我们需要在AppSettings中实现INotifyPropertyChanged。 基本的System.Configuration.ApplicationSettingsBase不再存在,您不能指望任何魔术。

您可以“在额头上”实现它:通过添加抛出所需事件的方法的实现,并在每个属性的设置器中对其进行调用。 但是这些是多余的代码行,此外,还需要在所有服务之间复制这些代码行。

让我们做得更漂亮-引入辅助基类AutoNotifier,该类实际上做同样的事情,但是在后台,就像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)); } } 

在这里,[CallerMemberName]属性使您可以自动获取调用对象的属性名称,即 AppSettings

现在,我们可以从该基类AutoNotifier继承我们的AppSettings,然后对每个属性稍加修改:

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

通过这种方法,我们的AppSettings类(甚至包含很多设置)看起来很紧凑,同时完全实现了INotifyPropertyChanged。

是的,我知道可以使用例如Castle.DynamicProxy.IInterceptor引入更多的魔术,拦截必要属性中的更改并在那里引发事件。 但是在我看来,这样的决定太过分了。

从YAML文件读取设置


下一步是添加YAML配置本身的读取器。 这发生在更接近服务开始的地方。 隐藏与正在讨论的主题无关的不必要的细节,我们得到类似的结果:

 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); } 

在给出的代码中,ConfigurationBuilder可能不是特别重要-使用它的所有工作都类似于使用ASP.NET Core中的配置。 但是以下几点很有趣。 首先,“开箱即用”,我们也有机会组合来自多个文件的设置。 如上所述,这要求每个服务器至少有两个配置文件。 其次,我们将所有读取的配置传递给某个ServerConfigurationProvider。 怎么了

YAML文件中的部分


我们稍后将回答这个问题,现在回到将分层结构的设置存储在YML文件中的要求。

原则上,执行此操作非常简单。 首先,在YML文件中,我们介绍所需的结构:

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

现在,我们去AppSettings并教他如何将我们的属性划分为多个部分。 像这样:

 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"} }; ... 

如您所见,我们直接在AppSettings中添加了一个字典,其中的键是AppSettings类实现的接口的类型,而值是相应部分的标题。 现在,我们可以将YML文件中的层次结构与AppSettings中的属性层次结构进行比较(尽管不深于一层嵌套,但是在我们的例子中,这就足够了)。

为什么我们要在AppSettings中执行此操作? 因为以这种方式我们不会散布有关不同实体设置的信息,而且这是最自然的地方,因为 在每个服务中,并因此在每个AppSettings中,都有其自己的设置部分。

如果在设置中不需要层次结构?


原则上,这是一个奇怪的情况,但是我们恰好在第一阶段就拥有了它,当时我们只是简单地从XML切换到YAML,而没有利用YAML的优势。

在这种情况下,部分的整个列表无法存储,ServerConfigurationProvider将更加简单(稍后讨论)。

但是重要的一点是,如果我们决定保留平坦的层次结构,那么我们就可以满足保持通过AppSettings.Default访问设置的能力的要求。 为此,请在AppSettings中添加这样一个简单的公共构造函数:

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

现在我们可以继续通过AppSettings来访问带有设置的类。在任何地方都可以使用Default(前提是已经通过ServerConfigurationProvider中的IConfigurationRoot读取了设置,并且已经实例化了AppSettings)。

如果不能接受一个单一的层次结构,那么无论如何,您都必须摆脱AppSettings。通过代码在每个地方都设置默认值,并且只能通过接口使用设置(这在原则上是很好的)。 为什么会这样-这将变得更加清晰。

ServerConfigurationProvider


前面提到的特殊ServerConfigurationProvider类处理非常神奇的事情,它使您可以仅使用平坦的AppSettings来完全使用新的分层YAML配置。

如果您迫不及待,就在这里。

完整的ServerConfigurationProvider代码
 /// <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由AppSettings设置类参数化:
 public class ServerConfigurationProvider<TAppSettings> : IServerConfigurationProvider where TAppSettings : new() 

您可能会猜到,这使您可以在所有服务中立即使用它。

读取的配置本身(IConfigurationRoot)以及上面提到的部分字典(AppSettings.Sections)被传递给构造函数。 订阅了文件更新(如果YML文件发生更改,我们是否要立即将这些更改移交给我们?):

 _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); } } } 

如您所见,在这里,如果要更新YML文件,我们将遍历所有已知的部分并逐一阅读。 然后,如果该部分早已在缓存中被读取(即某个类已经在代码中的某处请求了该部分),则我们用新的值重写缓存中的旧值。

似乎-为什么要读取每个部分,为什么不只读取高速缓存中的那些部分(即要求的部分)? 因为在阅读本节中,我们已经实现了对正确配置的检查。 如果设置不正确,则会抛出相应的警报,并记录问题。 最好尽快了解配置更改中的问题,然后从中立即阅读所有部分。

用新值更新缓存中的旧值就很简单了:

 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)); } } 

但是阅读章节并不是那么简单:

 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; } 

在这里,我们首先使用标准IConfigurationRoot.GetSection阅读本节本身。然后,只需检查读取部分的正确性即可。

接下来,我们将bindim节读为设置类型: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。以任何方式默认:通过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()); } } 

在这里,首先,提取我们感兴趣的接口的所有公共属性(读取-设置部分)。对于这些属性中的每一个,在读取设置中都会找到一个匹配项:如果找不到匹配项,则会记录相应的问题,因为这意味着config文件中缺少某些配置。最后,还要检查是否有任何读取设置与接口不匹配。如果有,则再次记录该问题,因为这意味着在配置文件中找到了接口中未描述的属性,这在正常情况下也不应该出现。

出现了问题-要求从哪里来的,读文件中的所有设置都应该一对一地对应于界面中可用的设置?事实是,实际上,如上所述,此时没有读取一个文件,而是一次读取了两个文件-一个具有默认设置,另一个具有被覆盖的文件,并且两个文件都是连续的。因此,实际上,我们不是从一个文件中查看设置,而是从完整文件中查看。当然,在这种情况下,它们的设置应一对一对应于预期的设置。

在上述资源中还应注意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-使您可以访问感兴趣的设置部分。 像这样:

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

为什么需要第二种和第三种方法-我将进一步解释。

设置界面注册


在我们的项目中,温莎城堡被用作IoC容器。是由他提供的,包括服务器设置界面。因此,这些接口必须在其中注册。

为此,编写了一个简单的Extension-class,它简化了此过程,以免在每个服务器中写入整个接口集的注册:

 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); } } 

第一种方法允许您注册设置的所有部分(为此,需要IServerConfigurationProvider界面中的AllSections属性)。

第一种方法中使用了第二种方法,它使用我们的ServerConfigurationProvider自动读取指定的设置部分,从而将其立即写入ServerConfigurationProvider缓存并在Windsor中注册。
在这里,使用了IServerConfigurationProvider中的第二个非参数化FindSection方法。

剩下的就是在Windsor容器注册代码中调用我们的Extension方法:

 container.RegisterAllConfigurationSections(configProvider); 

结论


发生什么事了


以这种方式,可以很轻松地将我们服务器的所有设置从XML转移到YAML,同时对现有服务器代码进行最少的更改。

与XML不同,YAML配置不仅具有更高的简洁性,而且还支持分区,因此更具可读性。

我们并不是发明自己的自行车来解析YAML,而是使用了现成的解决方案。但是,要将它们集成到我们项目的现实中,需要本文中介绍的一些技巧。我希望它们对读者有用。

可以保留即时捕获我们服务器的网络枪口中的设置更改的能力。此外,作为奖励,还可以实时捕获YAML文件本身中的更改(以前,必须重新启动服务器以获取配置文件中的任何更改)。

我们保留了合并两个配置文件(默认设置和覆盖设置)的功能,并且我们使用了第三方解决方案来实现。

效果不佳


我不得不放弃以前可用的功能,将从服务器Web界面应用的更改保存到配置文件,因为 对此类功能的支持需要做出很大的姿态,而摆在我们面前的业务任务通常并非如此。

嗯,我还不得不拒绝通过AppSettings.Default来访问设置,但这是一个加号,而不是减号。

Source: https://habr.com/ru/post/zh-CN438362/


All Articles