Как мы перевели конфигурирование наших сервисов с XML на YAML

    Предыстория вопроса


    Нашей компанией, среди прочего, разработаны несколько сервисов (точнее — 12), работающих бэкендом наших систем. Каждый из сервисов представляет собой Windows-службу и выполняет свои специфические задачи.

    Хочется все эти сервисы перенести под *nix-ОС. Для этого надо отказываться от обёртки в виде Windows-служб и переходить с .NET Framework на .NET Standard.

    Последнее требование приводит к необходимости избавиться от некоторого Legacy-кода, который не поддерживается в .NET Standard, в т.ч. от поддержки конфигурирования наших серверов через XML, реализованного с использованием классов из System.Configuration. Заодно таким образом решается и давняя проблема, связанная с тем, что в XML-конфигах мы время от времени ошибались при изменении настроек (например, иногда не туда ставили закрывающий тэг или забывали его вовсе), а замечательная читалка XML-конфигов System.Xml.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 (см. атрибут класса), а также в файле дизайнера 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-реализации мы наследуем наш AppSettings от INotifyPropertyChanged (на самом деле от него унаследован каждый интерфейс, который реализует 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 — это .NET библиотека для YAML. YamlDotNet предоставляет низкоуровневые парсер и генератор YAML, а также высокоуровневую объектную модель, схожую с XmlDocument. Также сюда включена библиотека сериализации, которая позволяет читать и записывать объекты из/в YAML-потоков.

    • NetEscapades.Configuration — github.com/andrewlock/NetEscapades.Configuration. Это непосредственно провайдер конфигураций (в смысле Microsoft.Extensions.Configuration.IConfigurationSource, активно используемого в ASP.NET Core приложениях), который читает YAML-файлы, используя как раз, упомянутый выше YamlDotNet.

    Подробнее о том, как использовать указанные библиотеки можно почитать вот тут.

    Переход к 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-частях.

    Было:

    [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 за ненадобностью. Теперь, кстати говоря, можно полностью избавиться от секции userSettings в файле app.config, если он есть в проекте — там хранятся те самые дефолтные настройки, которые мы прописываем через дизайнер настроек.
    Идем дальше.

    Контроль изменения настроек «на лету»


    Так как нам надо уметь ловить обновления наших настроек в рантайме, то требуется реализовать INotifyPropertyChanged в нашем AppSettings. Базового System.Configuration.ApplicationSettingsBase больше нет, соответственно, рассчитывать на какую-то магию не приходится.

    Можно реализовать «в лоб»: добавив имплементацию метода, выкидывающего нужное событие, и вызывая его в сеттере каждого свойства. Но это лишние строки кода, которые к тому же надо будет копировать по всем сервисам.

    Поступим красивее — введем вспомогательный базовый класс AutoNotifier, который фактически делает то же самое, но «за кулисами», прямо как делал ранее System.Configuration.ApplicationSettingsBase:

    /// <summary>
    /// Implements <see cref="INotifyPropertyChanged"/> for classes with a lot of public properties (i.e. 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.

    Теперь мы можем занаследовать наш AppSettings от этого базового класса AutoNotifier, а далее каждое свойство несколько видоизменить:

    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 (при условии, что настройки уже были ранее прочитаны через IConfigurationRoot в ServerConfigurationProvider и, соответственно, AppSettings был проинстанциирован).

    Если же плоская иерархия недопустима, то, как ни крути, придется избавляться от AppSettings.Default везде по коду и работать с настройками только через интерфейсы (что в принципе хорошо). Почему так — станет ясно дальше.

    ServerConfigurationProvider


    Специальный класс ServerConfigurationProvider, упомянутый ранее, занимается той самой магией, которая позволяет полноценно работать с новым иерархическим YAML-конфигом при наличии лишь плоского AppSettings.

    Если не терпится — вот он.

    Полный код 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. Затем как раз-таки проверяем корректность прочитанной секции.

    Далее прочитанную секцию биндим к типу наших сеттингов: 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 — позволяет обращаться к интересующей секции настроек. Как-то так:

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

    Вывод


    Что получилось


    Представленным способом удалось достаточно безболезненно перевести все настройки наших серверов с XML на YAML, при этом произведя минимум изменений по существующему коду серверов.

    YAML-конфигурации, в отличие от XML, получились более читаемыми за счет не только большей лаконичности, но и поддержки разбиения на секции.

    Мы не изобретали собственных велосипедов для парсинга YAML, а использовали готовые решения. Тем не менее, для интеграции их в реалии нашего проекта потребовались некоторые ухищрения, описанные в этой статье. Надеюсь, они будут полезны и читателям.

    Удалось сохранить возможность отлавливания изменений настроек в веб-мордах наших серверов «на лету». Более того, бонусом появилась возможность также налету отлавливать изменения в самом YAML-файле (ранее приходилось перезагружать сервер при любых изменений в конфиг-файлах).

    Мы сохранили возможность мержа двух файлов конфигов — дефолтных и переопределенных настроек, причем сделали это с использованием сторонних решений «из коробки».

    Что не очень получилось


    Пришлось отказаться от имевшейся ранее возможности сохранения изменений, примененных из веб-морд наших серверов, в конфиг-файлы, т.к. поддержка такой функциональности потребовала бы больших телодвижений, а бизнес-задачи перед нами такой в общем-то не стояло.

    Ну и также пришлось отказаться от обращений к настройкам через AppSettings.Default, но это скорее плюс, чем минус.
    United Traders
    Финтех
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 23

      0
      Но ведь System.Configuration есть в виде пакета с поддержкой .NETStandard — использование xml конфигов — не стопер перехода на .NETStandard.

      И если у вас 12 сервисов и есть необходимость динамического изменения конфигурации, то почему просто не использовать Consul или что-то подобное?
        0
        XML — далеко не единственное, что держит нас пока что от полного перехода на .NET Core всех серверов, хотя мы движемся в этом направлении. Сам по себе XML — да, не стопер.
        При этом у читалки XML из стандартного пакета есть проблемы, которые в принципе усложняют работу с ним, связанные прежде всего с плохой реакцией на опечатки в XML (которые в силу многословности XML не так уж редки), которые как раз дополнительно простимулировали нас уходить от него.
        Читалку из .NETStandard для XML мы, честно скажу, не проверяли. У нее с обработкой опечаток дела обстоят лучше?

        Consul и т.п. сервисы — да, была такая идея, и она, собственно говоря, никуда не делась, в будущем вполне возможно перейдем на какое-то единое решение, которое, кроме прочего, позволит конфигурировать не только указанные 12 серверов, но и все остальные сервисы, разрабатываемые у нас в компании. Но прямо сейчас это потребовало бы, по ощущениям, сильно больше телодвижений, чем просто выкинуть один способ работы с файликами конфигурации и заменить его на другой, всё остальное трогать нам сейчас не пришлось.
          0
          Мы пользуемся «голым» XmlDocument, а обёрткой, которая маппит секции из app.config в объект конфигурации. Если в этом объекте пометить какое-то свойство, как обязательно, но на этапе чтения настроек всё развалится.
            0
            У нас чтение производилось с использованием XmlDocument, он в половине случаев опечатки молча проглатывал (если честно — не помню хотя бы раза, когда он не проглатил бы). Какие конкретно примеры — сходу не подскажу, посмотрю потом при случае.
              +1
              Да с XmlDocument-то всё понятно. Он просто позволяет загрузить в память xml документ. А раз xml не накладывает особых ограничений на названия нод, то при чтении конфига типа
              <config>
                <param name="a" value="b"/>
                <parameter name="a" value="b"/>
              </config>
              

              Просто будет документ в котором есть две ноды с разным названием.
              Не то, чтобы я говорю, что вам не надо было переезжать на yaml, просто кажется что вы поменяли шило на мыло.
              С другой стороны, если произошла унификация кода для работы с конфигами среди всех сервисов и проектов, то профит неоспорим.
        +3
        я работал с конфигурациями, где размер конфигов в мегабайтах.
        так вот XML, от корого вы отказались, спокойно можно прочитать.
        json/YAML конфиги даже на 500 стр уже читать и редактировать нереально.
        если вы не знаете, что XML расширяется своими типами как только угодно, и вам было просто лень открыть гугл, прочитать, и дописать свой тип, который легко настраивается потом, для парсинга в тип при чтении, то это другой вопрос.
        <setting name="MCXSettings" serializeAs="String">
           <value>Inactive, ChartLen: 1000, PrintLen: 50, UseProxy: False</value>
        </setting>
        
        кто мешал сделать типа такого
        <setting name="MCXSettings" serializeAs="String">
           <CustomType>
           <Inactive>
        True
        </Inactive>
           <ChartLen>
         1000 
        </ChartLen>
           <PrintLen>
         50
        </PrintLen>
           <UseProxy>
         False
        </UseProxy>
        </CustomType>
        </setting>
        

        ?
        это занялоб несколько часов, а не недели гугления и боли. я б на месте заказчика за такое самоволосьтво не платил.
          0
          В других проектах у нас конфиги были и есть на YAML, по нему отзывы были положительные: достаточно удобно читать и редактировать. Размеры файлов там разные, по несколько сотен строк тоже есть, но чаще — несколько десятков. Поэтому решили опробовать его и здесь.
          В этом проекте после перехода на YAML лично у меня тоже скорее положительные ощущения. Конфиги стали сильно меньше, опечатки в них исчезли, все проблемы стали видны сразу на старте приложения. Единственный сайд-эффект, который я пока наблюдал, то, что по началу табы, которые иногда получались вместо пробелов, портили чтение файла, но это не большая проблема, т.к. такие ошибки, как я уже сказал, сразу же всплывали на старте сервиса, плюс сама проблема достаточно легко решилась настройкой в редакторе.
            +2
            В варианте, который привели выше, опечатки, можно сказать, исключены, если пользоваться стандартным XmlSerializer — он если не найдёт нужный тип в объекте, то просто при десериализации развалится.
            +2
            YAML позволит ужать по весу большие файлы, поскольку XML не самый экономный язык. Но на этом все преимущества и заканчиваются. Если раньше обрабатывался нетипизированный документ, и парсер проглатывал все синтаксически корректное (про список параметров внури значений я даже говорить не хочу), то и сейчас будет не лучше. YAML парсер так же будет глотать все опечатки, в лучшем случае ругаясь на совпадающие отступы.
              0
              Раньше парсер проглатывал несуществующие (неправильно написанные) тэги. Сейчас этого не происходит в силу предложенного решения сопоставлений ожидаемого конфига и файла-конфига «один-к-одному».
            +2
            Я тут как раз на дня хотел объяснить людям почему JSON плох для конфигов/данных погуглил и выяснил что и YAML это мягко говоря странная штука, нашёл только один проект похожий на адекватный StrictYAML, как раз у них есть и аргументация против остальных форматов.
              +2
              Как у вас обстоят дела с валидацией конфигов? Судя по тому, что жалуетесь на опечатки — никак. А зря. И вот по этой причине я бы посоветовал вам оставаться на XML, и сделать XSD схемы для всех конфигов (и прочих документов, если есть).

              Я может быть отстал от жизни, но для JSON/YAML общепризнанных стандартных форматов схем документов пока нет вроде бы.

              Т.е. оставайтесь на XML, но сделайте конвертеры XML to YAML и обратно. Вручную правьте YAML. Храните и используйте — XML. Ну может быть за редким исключением, когда размер имеет архиважное значение, ужимайте в YAML. Хотя по-моему если использовать ZIP архивацию — вообще без разницы практически, будет ли предварительная стадия XML to YAML.

              Такой подход облегчит вам экспорт-импорт и визуализацию на любые случаи жизни. XML всё-таки более распространён пока. Больше возможностей по трансформации в любые форматы — см. XSLT. Опять же — а есть ли уже что-то подобное для JSON/YAML?
                0
                Т.е. вместо XML-ки с конфигами поддерживать еще и XSD-схемы для каждого вида серверов? Это усложнение работы, а не ее упрощение.
                В решении с YAML в том виде, как оно реализовано, получилось реальное упрощение чтения конфига глазами + более прозрачная работа парсера, вываливающая все встречающиеся ошибки явным образом.
                  +1
                  Простите, я может чего-то недопонимаю, но как ваш парсер «вывалит ВСЕ встречающиеся ошибки явным образом», не имея описания формата? Я как-то привык считать, что конфиг — это часть API. API должно быть явным образом описано, стандартизировано, иметь версию, описание должно иметь версию и должно быть сохранено в git (или типа того).

                  Наверняка вы имеет ввиду только ошибки YAML стандарта. Но опечатка в имени параметра этот фильтр пройдёт. И пропущенный обязательный параметр, и неправильное значение (вне диапазона, допустим) — это всё тоже пройдёт незамеченным.

                  Самое лучшее описание API — то, которое умеет читать какой-нибудь парсер. Который может проверить документ на соответствие описанному стандарту. Так вот я и спрашиваю — есть ли подобное для JSON/YAML?

                  А то, что описание API так или иначе надо поддерживать… Ну что тут поделаешь — да, можно смотреть на это как на усложнение работы. И не делать эту часть работы. Какое-то время. Но когда вы сами начнёте путаться в своих конфигах и их версиях — вы об этой невыполненной части работы вспомните. Вздохнёте глубоко… И начнёте её делать. Имхо.
                    0

                    В статье как раз описывается как реализована проверка полноты и корректности конфига. Ошибки yaml синтаксиса вываливает yaml-читалка, а ошибки вида "не хватает такого-то параметра" или "этот параметр я не знаю" вываливает наш код. Описание формата у нашего кода конечно же есть, иначе зачем нам вообще этот конфиг, если мы не знаем, что из него брать. См. класс AppSettings в статье.

                      0

                      Описание "ожидаемого содержимого конфига", а не формата, имел в виду

                      0

                      Насчёт схем формата, вынесенных в отдельный файл, наподобие xsd, я не задавался пока что таким вопросом, т.к. это было бы усложнение задачи поддержки конфигов. Нам в любом случае надо было бы и в коде поддерживать соответствие "такое-то поле мы используем вот здесь". Т.е. для добавления нового поля, например, нам потребовалось бы сделать это в xml, в xsd и в коде. Сейчас мы обходимся только добавлением в yml и в коде. Не вижу смысла усложнять работу

                      0
                      Т.е. вместо XML-ки с конфигами поддерживать еще и XSD-схемы для каждого вида серверов?

                      Желательно их автогенерировать, благо стандартный XmlSerializer это умеет. Впрочем, и в ручной поддержке актуальности схем я не вижу ничего страшного если новые настройки появляются не часто.

                    0
                    Прочитав статью я не совсем понял вашу аргументацию при отказе от XML в пользу YAML.
                    Да, XML многословен но не «плоский» как вы сказали а довольно легко поддерживающий иерархии.
                    У меня на проекте мы подумываем полностью отказаться от локальных конфигов в пользу централизованного сервиса. Да тот же Redis будет более удобен с минимальными настройками что бы хранить конфиги, а сервису /приложению нужно только знать как оттуда вычитать и какой десериализатор использовать (это может быть единственной настройкой которую как то нужно передать сервису). Локальные конфиг файлы будь-то XML/YAML/JSON/ini файл это большое зло.
                      0
                      Централизованный сервис мы тоже рассматриваем.
                      XML не «плоский», плоская та структура, которую мы ранее для него предусматривали. Реализовать в нем вложенную структуру, конечно же, вполне можно было, но монструозность конфигов сильно увеличилась бы.
                      Получившиеся у нас сейчас конфиги хорошо читаются глазами.
                        0
                        Аналогичные ощущения после прочтения статьи.
                        Схему автор не использовал.
                        Тоже думаю хранить конфиги централизовано. (Руки еще не дошли до прода)
                        Вот только вопросы с выбором формата и инструмента ( read\write) остаются.
                          0
                          Я смотрю в сторону простейшего UI который поможет быстро сделать конфиг и хранением этого в JSON виде в Redis или в чем то подобном.
                          В идеале хочется следующего — приложение знает один единственный URL или иную точку входа и знает свой собственный тенант идентификатор (у нас малтитенатси) и этого хватает что бы вычитать свой конфиг и запуститься.
                          Раз в X минут приложение перечитывает конфиг файл.
                          JSON удобен тем что можно для разных версий приложения добавлять новые настройки и пока не меняется схема то все работает. Но это только мой Proof Of Concept и бизнес может просто не дать денег на это следуя правилу — «оно и так работает а как припечет то может тогда».
                            0
                            ААА. Сути это не меняет.
                            (Я говорил про конфиг для инициализации. Думаю брать напрямую из базы или через кеш со временем жизни(типа redis), если вдруг будет не доступен сервис с конфигами)

                            В любом случае, имея централизованный конфиг, его можно деплоить по тем же правилам, что и сам сервис, но отдельно от сервиса. Можно откатывать.
                            Очень удобно, если все это реализовать. Нужно ли — не знаю.
                            Может вы это и так уже делаете…

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое