В данной статье мне бы хотелось рассмотреть проблему загрузки настроек из конфигурационных файлов. Как правило, разработчики используют тяжеловесное и сложное API из пространства имен System.Configuration и считывают настройки шаг за шагом. В случае если в конфигурационном файле секция, которую надо считать, представляет из себя простую структуру (без вложенностей), то, в принципе, считывание не вызывает особых проблем. Однако, как только конфигурация усложняется и/или появляются вложенные подсекции, то распарсивание превращается в настоящую головную боль. Для простого и быстрого считывания настроек и загрузку их в память отлично подойдет библиотека ConfigurationParser, которая возьмет на себя все сложности работы с конфигурационными файлами.

Установка


Данная библиотека доступна для скачивания через NuGet. Или вы можете скачать исходники из GitHub.

Использование


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

Конфигурация системы
<ExternalSystemSettings>
    <AuthenticationSettings>
      <Login>DotNetCraft</Login>
      <Token>qwerty</Token>
      <Urls>
        <Url>https://github.com/DotNetCraft/ConfigurationParser</Url>
        <Url>https://github.com/DotNetCraft/ConfigurationParser</Url>
      </Urls>      
    </AuthenticationSettings>
    <StaffSettings Token="{D0C148F7-83C0-41B0-8F18-B47CAB09AD99}" Url="https://github.com/DotNetCraft/ConfigurationParser"/>
  </ExternalSystemSettings>

  <DatabasesSettings>
    <MongoSettings ConnectionString="mongo.url" DatabaseName="DotNetCraft"/>
    <SqlSettings>
      <item key="TenantA">
        <value>
          <SqlSettings ConnectionString="sql.TenantA.com"/>
        </value>
      </item>
      <item>
        <key>TenantB</key>
        <value>
          <SqlSettings>
            <ConnectionString>sql.TenantB.com</ConnectionString>
          </SqlSettings>
        </value>
      </item>     
    </SqlSettings>
  </DatabasesSettings>

  <SmtpSettings Host="gmail.com" Sender="no-reply">
    <Recipients>clien1@gmail.com;clien2@gmail.com;clien3@gmail.com</Recipients>
  </SmtpSettings>


Следующим шагом необходимо создать классы, в которых мы будем хранить настройки системы.

Конфигурационные классы
    #region ExternalSystemSettings
    class ExternalSystemSettings
    {
        public AuthenticationServiceSettings AuthenticationSettings { get; set; }
        public StaffServiceSettings StaffSettings { get; set; }
    }

    class AuthenticationServiceSettings
    {
        public string Login { get; set; }
        public string Token { get; set; }
        public List<string> Urls { get; set; }
    }

    class StaffServiceSettings
    {
        public Guid Token { get; set; }
        public string Url { get; set; }
    }
    #endregion

    #region DatabasesSettings
    class DatabasesSettings
    {
        public MongoDatabaseSettings MongoSettings { get; set; }
        public Dictionary<string, SqlSettings> SqlSettings { get; set; }
    }

    class MongoDatabaseSettings
    {
        public string ConnectionString { get; set; }
        public string DatabaseName { get; set; }
    }

    class SqlSettings
    {
        public string ConnectionString { get; set; }
    }
    #endregion

    #region Smtp
    class SmtpSettings
    {
        public string Host { get; set; }
        public string Sender { get; set; }

        [CustomStrategy(typeof(SplitRecipientsCustomStrategy))]
        public List<string> Recipients { get; set; }
    }
    #endregion


В конфигурационном файле нам необходимо объявить, что мы будем использовать ConfigurationParser.

Заполнение configSections в App.Config
<?xml version="1.0" encoding="utf-8" ?>
<configuration>

  <configSections>
    <section name="ExternalSystemSettings" type="DotNetCraft.ConfigurationParser.SimpleConfigurationSectionHandler, DotNetCraft.ConfigurationParser" />
    <section name="DatabasesSettings" type="DotNetCraft.ConfigurationParser.SimpleConfigurationSectionHandler, DotNetCraft.ConfigurationParser" />
    <section name="SmtpSettings" type="DotNetCraft.ConfigurationParser.SimpleConfigurationSectionHandler, DotNetCraft.ConfigurationParser" />
  </configSections>
…
</configuration>

Теперь мы можем считать нашу конфигурацию легко и непринужденно:

ExternalSystemSettings externalSystemSettings = (dynamic)ConfigurationManager.GetSection("ExternalSystemSettings");
DatabasesSettings databasesSettings = (dynamic)ConfigurationManager.GetSection("DatabasesSettings");

После выполнения кода у нас будет создано 2 объекта, которые и будут содержать наши настройки. Как вы можете справедливо заметить, мы не считываем SmtpSettings. Это сделано для того, чтобы продемонстрировать дополнительные возможности утилиты.

В различных проектах вам может понадобиться записывать разнообразные настройки в конфигурационные файлы и иметь свою логику как с ними работать. Утилита спроектирована таким образом, что вы можете ее легко расширить путем добавления новой стратегии и покрыть все сценарии, которые вам нужны. Для примера, давайте рассмотрим секцию Recipients:

<Recipients>clien1@gmail.com;clien2@gmail.com;clien3@gmail.com</Recipients>

Эта секция содержит список email-ов, разделенных точкой с запятой. Согласно нашему ТЗ и классу мы должны записать каждый адрес как отдельный элемент в массиве. Для выполнения поставленной задачи нам надо создать класс SplitRecipientsCustomStrategy и реализовать интерфейс ICustomMappingStrategy

Посльзовательская стратегия
class SplitRecipientsCustomStrategy : ICustomMappingStrategy
    {
        #region Implementation of ICustomMappingStrategy

        public object Map(string input, Type itemType)
        {
            string[] items = input.Split(';');
            List<string> result = new List<string>();

            result.AddRange(items);

            return result;
        }

        public object Map(XmlNode xmlNode, Type itemType)
        {
            string input = xmlNode.InnerText;
            return Map(input, itemType);
        }

        #endregion
    }

Внутри нам необходимо реализовать алгоритм распарсивания значения из секции в соответствии с нашим заданием. Необходимо учитывать, что метод Map(string input, Type itemType) вызывается если значение прочитано из атрибута, а метод Map(XmlNode xmlNode, Type itemType) вызывается если значение прочитано из секции. Согласно конфигурационному файлу, значение будет прочитано из секции.

После этого нам необходимо пометить свойство Recipients атрибутом CustomStrategy в котором нужно указать, что за «стратегия» будет использоваться для данного поля:

[CustomStrategy(typeof(SplitRecipientsCustomStrategy))]
public List<string> Recipients { get; set; }

Вот и все, теперь мы можем считать настройки электронной почты.

SmtpSettings smtpSettings = (dynamic)ConfigurationManager.GetSection("SmtpSettings");

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

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

Комментарии (44)


  1. Sirion
    26.03.2017 16:14

    Тупой вопрос от js-кодера: а почему плохо просто построить XML DOM и читать всё оттуда?


    1. DotNetCraft
      27.03.2017 01:33

      Приветствую

      В мире .NET разработчики привыкли работать с объектами. Поэтому практически в любом приложении можно встретить строчки

      AppSettingsSection appSettings  = (AppSettingsSection)config.GetSection("appSettings");
      string connectionString = appSettings.Settings["SqlConnectionString"].Value;
      MyAppSettings myAppSettings = new MyAppSettings (connectionString);
      


      Используем myAppSettings.ConnectionString по своему усмотрению.

      Это очень простой пример, но даже в нем нам приходится собственноручно создавать объект с настройками и заполнять его. Библиотека все это сделает за нас.

      Все усложняется, когда появляются настройки со многими вложенностями. Например, в моем текущем проекте у нас в зависимости от страны — свои настройки, в которых прописываются подключения к различные сторонним сервисам, строки подключения к БД и тд. В результате чего для загрузки конфигурационного файла пришлось бы писать очень много кода, а так — создал классы и загрузил все одной строчкой.


      1. Deosis
        27.03.2017 07:58
        +1

        Дополню ответ DotNetCraft.
        В случае прямой работы придется вводить название секций и атрибутов в виде строк.
        А это прямой путь к опечаткам и головной боли при переименовании.


        1. Sirion
          27.03.2017 12:52
          +1

          А каким образом приведённый здесь подход избавляет от опечаток и боли? Всё равно где-то придётся прописывать имя тега, откуда берётся настройка, телепатию в дотнет вроде ещё не завезли. И если что-то переименуется, всё равно придётся что-то в связи с этим менять. Разве нет?


          1. Deosis
            28.03.2017 07:24

            В примере автора имя тега совпадает с именем поля, если изменится, то просто дописываем атрибут, а библиотека за нас смаппит. Даже для загрузки секции можно написать обертку, чтобы не вводить её имя вручную.
            У вас же при изменении атрибута или секции придется изменять все места, где она используется.


          1. wlbm_onizuka
            29.03.2017 12:09

            лучше ошибиться в одном месте, чем в нескольких. разве нет?


          1. DotNetCraft
            29.03.2017 12:12

            Дополню Deosis

            Если в конфигурационном файле будет жлемент, а в классах его не будет, то система сгенерит Exception. Таким образом происходит проверка конфига и классов.


  1. MonkAlex
    26.03.2017 16:26
    +7

    Ещё наверно тупее вопрос — а почему просто не использовать json? Пишешь нужные тебе классы, автоматически сериализуешь\десериализуешь без всяких монструозных инструментов. Читабельно, быстро, просто, удобно.


    1. DotNetCraft
      27.03.2017 01:13
      +1

      Приветствую

      Listrigon правильно ответил про стандартную систему конфигурационных файлов .NET. От себу лишь слегка добавлю, что в .NET для хранения конфигурации используется App.Config, в котором и записывается все настройки. Формат хранения данных — XML.

      Более подробно можно прочитать здесь:
      1. https://msdn.microsoft.com/en-us/library/1xtk877y.aspx
      2. https://msdn.microsoft.com/en-us/library/ms254494(v=vs.110).aspx


    1. Listrigon
      27.03.2017 01:13
      +1

      Скорее всего потому, что все это дело является частью стандартной системы конфигурационных файлов .NET.
      Чтобы не делать отдельного файла под свои настройки нужно быть частью структуры этого файла, который пользуется и другими библиотеками. Хотя да, с JSON было бы все проще


  1. Caraul
    26.03.2017 20:05
    +1

    А в чем отличие от Custom Configuration Sections? Только то, что POCO без атрибутов? Я не вижу в System.Configuration особых сложностей и тяжестей.


    1. DotNetCraft
      27.03.2017 01:57

      Приветствую
      На мой взгляд прелесть в том, что кода становится меньше.

      Для примера возьмем секцию из статьи по вашей ссылке

      <StartupFolders>
         <Folders>
           <Folder folderType = "A" path="c:\foo" />
           <Folder folderType = "B" path="C:\foo1" />
         </Folders>
       </StartupFolders>
      


      Создадим классы

      class StartupFolders
      {
          public Folders Folders { get; set; }
      }
      
      class Folders
      {
          public List<Folders> Folders { get; set; }
      }
      
      class Folder
      {
          public string FolderType { get; set; }
          public string Path { get; set; }
      }
      


      Вызовем загрузчик
      StartupFolders startupFolders= (dynamic)ConfigurationManager.GetSection("StartupFolders");
      


      Ну и как бы все :) Мы загрузили все настройки.

      Обратите внимание, что я нигде не использовал ключи а-ля «Folders», что, с моей точки хрения, уменьшает ошибку при копипасте.

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


      1. lair
        27.03.2017 02:16

        … а теперь, значит, вы хотите переименовать свойство Folders.Folders в Folders.NewFolders, но не хотите трогать конфигурационные файлы (т.е., в них элемент так и должен остаться Folders). И как это сделать?


        1. DotNetCraft
          27.03.2017 02:48
          +1

          Если я правильно понял вопрос, то

          [PropertyMapping("Folders ")]
          public List<Folders> NewFolders { get; set; }
          


          Это означает, что в конфигу мы ищем Folders и грузим данные в NewFolders.

          Хотя, если честно, то я почти не сталкивался с ситуациями, когда надо было поменять имя… Вот структуру — это да, частое явление. То мы храним это в Листе, то давайте сделаем словарь…

          З.Ы. Забыл упомянуть об этом аттрибуте в статье.


  1. SergeyVoyteshonok
    26.03.2017 22:05
    +2

    вместо

                string[] items = input.Split(';');
                List<string> result = new List<string>();
    
                result.AddRange(items);
    
                return result;
    


    может лучше
    return input.Split(';').ToList();
    


    1. DotNetCraft
      27.03.2017 01:36
      +1

      Приветствую

      Да, соглашусь, что можно input.Split(';').ToList();

      Однако, я хотел акцентировать внимание на то, что в этом методе можно написать любую логику. Например, в текущем примере мы можем добавить проверку правильность email'ов и сгенерить Exception если что-то не так.


  1. msin
    27.03.2017 01:36
    +1

    Зачем такой многострочный метод Map(string input, Type itemType)?
    Гораздо короче и понятнее будет так:

            public object Map(string input, Type itemType)
            {
                return input.Split(';').ToList();
            }
    


    1. DotNetCraft
      27.03.2017 01:37
      +2

      Приветствую

      Да, соглашусь, что можно input.Split(';').ToList();

      Однако, я хотел акцентрировать внимание на то, что в этом методе можно написать любую логику. Например, в текущем примере мы можем добавить проверку правильность email'ов и сгенерить Exception если что-то не так.


  1. Deosis
    27.03.2017 08:04
    +2

    В .NET настройки делятся на настройки приложения и пользовательские. (вторые доступны для изменения, первые только для чтения [если не править файл вручную])
    Библиотека как-либо обрабатывает эти различия?
    Например, пользователь переопределил несколько настроек и хочет их сохранить.
    Что нужно использовать вместо Configuration.Save()?


    1. DotNetCraft
      27.03.2017 13:00

      Основное предназначение этой библиотеки — чтение настроек приложения, поэтому методы сохранения не предусмотрены.

      Если честно, то я не стрононник что-либо менять в конфиге приложения в рантайме. Если что-то и надо заменить, то, используя, например, TeamCity+Octopus, создаем релиз и деплоим его с измененными настройками. Это относится к настройкам приложения.

      Если же речь идет о пользовательских, то, как правило эти данные хранятся в БД и выходят за рамки данной либы.


      1. Bronx
        29.03.2017 04:43

        я не стрононник что-либо менять в конфиге приложения в рантайме.

        Рантайм — это не единственный сценарий. Есть ещё конфигурационные утилиты, мастера всякие и прочие средства развёртывания, которые помогают правильно готовить конфиги, и им весьма желательно пользоваться теми же самыми классами.

        ЗЫ. Атрибут «configSource» продолжает нормально поддерживаться?


        1. Bronx
          29.03.2017 05:01

          И, заодно, я так понимаю, у вас нет поддержки стандартного паттерна коллекций в web/app.config?

          <items>
              <clear />
              <add name="Item 1" />
              <add name="Item 2" />
          </items>
          


          1. DotNetCraft
            29.03.2017 12:08

            Под «рантаймом» я понимаю, когда программа запущена и она сама меняет ее конфиг. Вот я именно против этого. А вот когда в процессе развертывания кто-то меняет конфиг, так это я «за». Например, мы создаем релизный конфиг и там указываем заменить connectionString на реальзое значение в зависимости от среды развертывания. Что-то мне подсказывает, что в этом вопросе мы по одну сторону баррикады.

            Что касается <clear/> и configSource… Я с этимт «параметрами» сталкивался очень давно и опыт был негативным, так что я стараюсь их избегать. Более того, это лично мое мнение, задвать в конфигурации, то что конфигурация в таком-то файле как-то странновато. Смысл данного действия для меня представляется только в том, чтобы разделить Debug от Release, ну, или разные конфиги для разных клиентов. Все это легко делается путем создания конфигурационного файла с разметкой и, в момент создания билда, автоматически подставляем нужные параметры.


            1. Bronx
              29.03.2017 20:59

              «clear/add/remove» упрощает создание иерархических конфигов с настройками по-умолчанию: есть некий корневой конфиг с дефолтными элементами коллекции, и в своём под-конфиге можно переиспользовать их, можно добавить свои, можно убрать некоторые (или все) дефолтные. Без этого придётся каждый раз указывать все необходимые элементы. С одной стороны, это вроде как хорошо — все элементы явно присутствуют, с другой — конфиг может быть сильно перегружен вспомогательными элементами, появляется соблазн копипастить и т.п.

              configSource — это как "#include" или «using» или «import» — полезен тем, способствует модуляризации: можно разбить огромный неудобоваримый конфиг с кучей настроек «обо всём» на несколько более сфокусированных конфигов. Например, ту часть, что настраивается девелопером и далее не предполагается перенастраивать (всякие там assemblyBindings и проч), хранить в основном модуле, а части, которые нужно настраивать при деплое — в своих модулях. Кроме того, можно создать несколько под-конфигов на разные случаи (скажем, с разными тонкими настройками производительности), и подключать/переключать их одним движением, не переписывая основной конфиг, и не дублируя его.

              В чём был ваш негативный опыт? У меня как раз негативный опыт от одного супер-конфига, в основном из-за его вырвиглазности и из-за копипасты, распространяющейся как рак.


              1. Bronx
                29.03.2017 21:09

                Забыл добавить: при разбиении конфига на части и иерархии/композиции появляется возможность тонко настраивать безопасность, назначая разные права на разные секции.


                1. DotNetCraft
                  30.03.2017 13:20

                  Если я правильно понял, то все эти приемы направлены чтобы превратить мегагромадный конфиг во что-то удобоваримое…

                  Опыт как раз в том, что огромный конфиг пытались разбить на подконфиги и тем самым только усугубив проблему, вместо того, чтобы разобраться в чем причина. А проблема-то была в том, что система взяла на себя слишком много (получился этакий монолит-гигант) и плюс не было нормального деплоймента (конфиги ручками менялись для debug, test, sandbox и пр). Зато потом, когда все-таки разбили этот кусок гиганта на сервисы (мини или микро — это пожеланию), то монстрообразный конфиг пропал. Вместо него появились несколько мелких конфигов и для каждой среды свой конфиг.
                  Пример:

                  • app.config — это конфигурация у разработкича
                  • app.Release.config — это для всех остальных (тест, релиз и пр)

                  При деплое меням переменные в указанном конфиге. Например, при деплое тестировщикам имеем конфиг вида
                  <DatabaseSettings  ConnectionString=#{CurrentConnectionString}/>
                  

                  В Octopus'e задаем значение для CurrentConnectionString
                  • CurrentConnectionString = TestDatabase для тестирования
                  • CurrentConnectionString = SandBox для предрелиза
                  • CurrentConnectionString = Release для релиза.

                  Нажимаем на кнопку и деплоим куда надо. Переменная заменится на нужное значение автоматом.


                  1. Bronx
                    30.03.2017 21:45

                    Для меня «мегагаромадный» начинается уже с момента, когда кастомные настройки составляют менее 50% от веса всего конфига. Т.е. если конфиг занимает целый экран, и в нём всего одна настройка — коннект к базе данных, — то это кандидат на декомпозицию.

                    Возможно это связано с тем, что деплоймент у нас действительно отличается от вашего — мы не деплоим сервисы у себя, мы отдаём их заказчикам. И нам нужно обеспечить, чтобы наши конфиги были простыми и понятными, даже если у заказчика нет полноценного деплоймента и он выкатывает сервисы вручную, настраивая конфиги в Блокноте. К сожалению, почти все статьи заточены на единственный сценарий деплоймента сервисов у себя дома, где можно поставить сколь угодно продвинутый деплоймент.


                    1. DotNetCraft
                      03.04.2017 13:58

                      Натройка конфига в блокноте — это шедеврально. Посоветуйте им хотя Nodepad++. А вообще, предложите им свои услуги по настройке.

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


                      1. Bronx
                        04.04.2017 02:54

                        Натройка конфига в блокноте — это шедеврально.

                        «Когда б вы знали, из какого сора...»

                        А вообще, предложите им свои услуги по настройке.

                        У нас есть solution team, но его тоже нужно содержать, обучать и, что хуже, постоянно переобучать, иначе они сами оказываются тормозом, а не источником дохода. Часто проще сделать простые конфиги и внятную документацию, понятную даже дураку.

                        всегда стараемся минимизировать ручное редактирование конфига.

                        Мы тоже, но без деплой-сервера (а мы не можем гарантировать его налиичие у каждого из кучи мелких кастомеров) это означает написание собственного конфигурационного визарда для каждого микросервиса. Это дополнительные затраты на девелопмент и тестирование, без особого финансового выхлопа. Мы предпочитаем, чтобы кастомер сам заботился об автоматизации, и чтобы даже при ручном деплое он не сильно отвлекал нас вопросами, которых можно было бы избежать, сделав всё просто и хорошо это документировав.

                        конфигурационный файл должен содержать только ту информацию, которая нужна данному конкретному микросервису.

                        И я о том же. Но даже у самого микро- из микросервисов есть два типа конфигурационных данных: те, что диктуются выбранным фреймворком/библиотеками, настроены девелопером и не предполагаются к изменению кастомером (скажем, многочисленные настройки WCF со всем ворохом сервисов, байндингов, behaviours и т.п., или настройки dependency injector-а, или настройки доступа к разным путям), и те, которые предполагаются быть настроенными кастомером (в простейшем случае, секция «appSettings»). Причём настроек первого типа может быть огромное количество, и зависит оно не от девелопера. Девелопер рад бы иметь малеький конфигурационный файл с только теми настройками, которые он сам создал, но без разбиения конфига на части это практически нереально.


  1. Sinatr
    27.03.2017 10:31
    +1

    Никогда не понимал возни с Settings (или как там этот класс зовут) и вообще использование словарей для хранения/доступа к настройкам…

    Пишется свой класс, расставляются аттрибуты, сериализуется/десериализуется чем вам больше нравится (json.net, XmlSerializer, ...) в куда вам необходимо (в MemoryStream -> byte[] -> db или же в FileStream)… Если конфигурация одна — синглтон, несколько — доступ через менеджер (Config.Active, Config.Load, etc.).

    Все вышеперечисленное занимает буквально десяток строк (если не реализовывать что-то слишком навороченное, к примеру версионинг).

    Имхо нет смысла в надстройке над чем-то дефолтовым и кривым. Плюс я не очень понял чем же лучше

    SmtpSettings smtpSettings = (dynamic)ConfigurationManager.GetSection(«SmtpSettings»)
    особенно доставляет dynamic и (на внимательность) «StmpSettings».


    1. MonkAlex
      27.03.2017 11:45

      Как оказалось, некоторые любят app.config.
      Я бы ещё понял web.config, но app.config мне категорически не нравится. Пользователя туда не пошлешь, там могут быть и программные настройки.


      1. DotNetCraft
        27.03.2017 13:10
        +1

        Это утилита работает также с web.config…

        Пользователя и не надо туда слать. Для этого существуют другие места. App.Config предназначен для хранения настроек приложения и очень хорошо с этим справлется. Добавляем к нему build-сервер (например, TeamCity) и Octopus (мне он нравится) и мы получаем развертывание приложения в один клик/чекин для разных сред (Dev, Test, Sandbox, Release). Поэтому я не понимаю почему он вам так не нравится…


    1. DotNetCraft
      27.03.2017 13:29

      Как правило это работает на маленький проекта без автоматических билдов и развертываний или в самом начале разработки, когда приложение опять-таки маленькое. Когда приложение разрастается до несколько десятков сервисов, да еще и с подключением к сотне других сторонних программ… Вот тут и начинается, что надо бы такую настроечку, а вот тут при чтении конфигурации обязательно проверить, что переменные в Octopuse имеют правильные значения и получается, что изначальные 10 строчек кода в разных проектах превращабтся в 20 в одном, 50 в другом, 10 в третьем, причем от исходных может вообще ничего не остаться.


  1. IlyaMS
    27.03.2017 12:53

    В .NET Core из коробки идет хорошее решение, которое и вложенность любую поддерживает, и принцип разделения ответственности, и встроенный в фреймворк DI container, и вообще. Вот тут можно глянуть, начиная с раздела «Using Options and configuration objects».


    1. DotNetCraft
      27.03.2017 13:22
      -1

      Неплохая утилита, но есть как минимум два «но».
      1. Читает json.
      2. Нужен .NET Core

      Мне больше всего не нравится то, что конфигураци хранится в json файлах… Сейчас попробую объяснить почему.

      При создание проекта в .NET автоматически будет создан app.config, в который студия поместит какие-то свои данные. Затем, например, мы добавим WCF сервис и в том же файле увидим его параметры. При этом, давайте предположим, что мы используем самописный ORM и поэтому строку подключения можем хранить где угодно и мы выбрали json… Как-то выглядит, что настройки расползлись по двум файлам вместо одного…


      1. IlyaMS
        27.03.2017 22:43

        Не, .NET Core же как раз в json переполз настройками, в том числе и теми, что «автоматически будет создан...»
        В последней версии, правда, вроде обратно переползли, я её еще не ковырял.

        Это что касаемо пункта 1 вашего.
        Что касаемо пункта 2 — ну это уже надо как данность воспринимать. Вероятнее всего, что в ближайшие пару лет все дружной толпой будем в сторону .NET Core двигаться.


        1. DotNetCraft
          28.03.2017 01:11
          +1

          Пока это будущее наступит пройдет время в течении которого надо выпускать продукты, да еще и поддерживать и развивать старые…

          Но, все равно, спасибо за комментарий и ссылку на статью.


      1. OREZ
        28.03.2017 09:11

        .NET Core поддерживает из коробки чтение JSON и XML конфигураций. По умолчанию просто выбирается JSON.


  1. eugenebb
    28.03.2017 01:24
    +1

    У нас используется немного другое решение.

    T4 читает config file и строит правильные классы, включая анализ какого типа должно быть свойство.

    Преимущество того что все доступно на момент компиляции и то что не надо руками ничего делать.
    Автоматически включены кэш и т.п.


    1. DotNetCraft
      28.03.2017 01:24

      Интересный подход. По-сути, вы написали свою «библиотеку», подобной ConfigurationParser, но на Т4.

      Кстати, а если в Т4 появляется ошибка, то как тестировать? Несколько раз посоздавали разные конфиги и на этом все?
      На и в догонку, а как быть если адреса почты написаны через точку с запятой, а в системе они должны быть с листе, да еще и проверены с помощью RegEx?

      И на последок, если это не коммерческая тайна, то можно взглянуть на код?


      1. eugenebb
        28.03.2017 18:11

        T4 тестируется точно также как и любой другой код, т.е. при исполнении если произошла ошибка выдается сообщение где и что случилось. Есть отладчик в котором можно по шагам пройтись. Не помню чтобы были проблемы с тестированием.

        Так как это кастомный T4 шаблон, то можно реализовать все что угодно, плюс с появлением partial в C#, автогенерирование и расширение кода делается очень удобно.

        Если адреса почты написаны через точку с запятой, то добавляем проверку на этот случай и при совпадении, генерируем код который в результате будет возвращать список строк и при первом обращении парсить значения из конфига и проверять на правильность адреса.

        Реальный код скорее всего нельзя, но сделал простой пример, включая список емайлов.

        Добавить в проект как файл с раширением .tt в ту же директорию где и web.config (либо добавить относительный путь до него в строчке с Host.ResolvePath(«Web.config»)

        В результате у вас будет автоматически создан cs файл.
        Если конфиг изменился, выбрать «Run Custom Tool» и cs файл пересоздасться.

        Длинный код
        <#@ assembly name="System.Core" #>
        <#@ assembly name="System.Data" #>
        <#@ assembly name="System.Xml" #>
        <#@ import namespace="System.Linq" #>
        <#@ import namespace="System.Data.SqlClient" #>
        <#@ import namespace="System.Text" #>
        <#@ import namespace="System.Xml" #>
        <#@ import namespace="System.Collections.Generic" #>
        <#@ import namespace="System.Text.RegularExpressions" #>
        <#@ template language="C#" debug="true" hostspecific="true" #>
        <#@ output extension = ".cs" #>
        using System;
        using System.Configuration;
        using System.Linq;
        using System.Text.RegularExpressions;
        
        <#
        	var configs = new List<KeyValuePair<String, String>>(); 	
        	configs.Add(new KeyValuePair<String, String>("TestConfig", System.IO.File.ReadAllText(Host.ResolvePath("Web.config"))));
        
        	var emailRegex = new Regex(@"^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)(,|;)([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)");
        #>
        
        //////////////////////////////////
        // Autogenerated class based on web.config file
        //
        // WARNING: Any changes to the file will be overwritten the next time the source code will be autogenerated
        //////////////////////////////////
        
        namespace MyNamespace.Config
        {
        <# 
        	foreach(var config in configs) { 
        #>
        
        	public static class <#= config.Key #>
        	{
        		private static readonly object _lock = new object();
        		private static Regex _emailRegex = new Regex(@"^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$");
        
        <# 
        	XmlDocument xmlDoc = new XmlDocument();
        	xmlDoc.LoadXml(config.Value);
        
        	foreach(XmlElement node in xmlDoc.SelectNodes("/configuration/appSettings/add"))
        	{
        		string key = node.GetAttribute("key");
        
        		string keyType = "string";
        		string converterBegin = "";
        		string converterEnd = "";
        
        		string safeKey = Regex.Replace(key, "[^A-Za-z0-9_]", "");
        		string keyFirstLower = safeKey.Substring(0,1).ToLower() + safeKey.Substring(1);
        		string keyFirstUpper = safeKey.Substring(0,1).ToUpper() + safeKey.Substring(1);
        		string value = node.GetAttribute("value");
        		
        		if(emailRegex.IsMatch(value))
        		{
        			keyType = "string[]";
        			converterBegin = "!String.IsNullOrEmpty(s) ? ";
        			converterEnd = ".Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries) : new string[0];\r\n" +
        							"						if(_" + keyFirstLower + ".Any(a => !_emailRegex.IsMatch(a)))\r\n" +
        							"						     throw(new Exception(\"Invalid email in config\"))";
        		} 
        		else if(Regex.IsMatch(value, @"^[0-9]+$"))
        		{
        			keyType = "int?";
        			converterBegin = "!String.IsNullOrEmpty(s) ? Int32.Parse(";
        			converterEnd = ") : (int?)null";
        		}
        		else if(Regex.IsMatch(value, @"^(true|false)$", RegexOptions.IgnoreCase))
        		{
        			keyType = "bool";
        			converterBegin = "!String.IsNullOrEmpty(s) ? Boolean.Parse(";
        			converterEnd = ") : false";
        		}
        		else if(Regex.IsMatch(value, @"^https?://[^{]+$", RegexOptions.IgnoreCase))
        		{
        			keyType = "Uri";
        			converterBegin = "!String.IsNullOrEmpty(s) ? new Uri(";
        			converterEnd = ") : null";
        		}
        		
        #>
        		private static bool _isLoaded<#= keyFirstUpper #>;
        		private static <#= keyType #> _<#= keyFirstLower #>;
        		public static <#= keyType #> <#= keyFirstUpper #> 
        		{
        			get
        			{
        				if(!_isLoaded<#= keyFirstUpper #>)
        				{
        					lock(_lock)
        					{
        						 string s = ConfigurationManager.AppSettings["<#= key #>"];						 
        						_<#= keyFirstLower #> = <#= converterBegin #>s<#= converterEnd#>;
        
        						_isLoaded<#= keyFirstUpper #> = true;
        					}
        				}
        
        				return _<#= keyFirstLower #>;
        			}
        
        		}
        <#
        
        		if(keyType != "string")
        		{
        #>
        
        		private static bool _isLoaded<#= keyFirstUpper #>AsString;
        		private static string _<#= keyFirstLower #>AsString;
        		public static string <#= keyFirstUpper #>AsString 
        		{
        			get
        			{
        				if(!_isLoaded<#= keyFirstUpper #>AsString)
        				{
        					lock(_lock)
        					{
        						 _<#= keyFirstLower #>AsString = ConfigurationManager.AppSettings["<#= key #>"];
        						_isLoaded<#= keyFirstUpper #>AsString = true;
        					}
        				}
        
        				return _<#= keyFirstLower #>AsString;
        			}
        		}
        
        <#		
        		}
        	}
        
        #>
        	}
        
        <# } #>
        }
        



        1. DotNetCraft
          29.03.2017 12:27

          Спасибо за пример. Попробую поиграться…

          А тем временем позвольте немного критики.

          Если я правильно понял, то вот этот длинный код создает класс с одной переменной и распарсивает значение из конфига.На мой взгляд как-то не рационально.

          Также, если мы захотим добавить еще логики, то мы должны менять tt? Например, повторные адреса игнорировать. Можно, конечно, написать partial class и там реализовать метод со всякими проверками, а в tt его только дергать… По-моему как-то слишком сложно. Учитывая, что конфиг это та часть программы, которая очень редко меняется.

          З.Ы. Еще раз спасибо за пример — буду разбираться.


          1. eugenebb
            29.03.2017 16:46

            Самое простое — скопировать в проект и попробовать, результат сразу будет виден.

            Тот пример создает класс с несколькими properties которые будут соответствовать вашей конфигурации.

            пример

            <configuration>
              <appSettings>
                <add key="AdminEmails" value="admin1@test.com,admin2@test.com" />
                <add key="EnableRegistration" value="false" />
                <add key="TempDir" value="c:\temp\" />
                <add key="MaxNumberUsers" value="123" />
                <add key="ApiServer" value="http://www.google.com" />
              </appSettings>
            </configuration>
            
            


            Результат будет что-то типа (упрощенно)

            namespace MyNamespace.Config
            {
                    public static class TestConfig
            	{
                             public static string[] AdminEmails { get; }
            
                             public static bool EnableRegistration { get; }
            
                             public static string TempDir { get; }
            
                             public static int MaxNumberUsers { get; }
            
                             public static Uri ApiServer { get; }
                    }
            }
            
            
            


            Если в проекте несколько конфигураций, добавить

            configs.Add(new KeyValuePair<String, String>("Test2Config", System.IO.File.ReadAllText(Host.ResolvePath("../Project2/Web.config"))));
            
            configs.Add(new KeyValuePair<String, String>("Test3Config", System.IO.File.ReadAllText(Host.ResolvePath("../Project3/Web.config"))));
            


            будет созданно еще два класса.

            Если вы хотите добавть логики «Например, повторные адреса игнорировать.» — то это должно быть сделано где-то все равно, в tt или еще где-то, разница не большая.

            Моя практика показывает что в большинстве случаев, ничего волшебного в конфигах обычно не хранят.

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

            Преимущество кодогенерации в том что сделал один раз и потом оно за тебя работает.

            Если у вас один проект и вы его годами поддерживаете, то наверное как сделали, так и ок.
            А если создается что-то новое, более-менее часто, то как скопировав tt в новый проект, у вас уже все сгенерированно.

            Нашли ошибку и исправить — поменяли шаблон и везде все поменяно. Надо сгенерировать mock config для тестов — поменяли пару строчек в шаблоне и он вам тестовые классы сделал.

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

            Например для WebApi, шаблоны через reflection создают TypeScript клиент для доступа к этому API.

            Есть шаблоны которые по XML и JSON создают структуры классов, другие по структуре базы создают data access layer, по структуре классов создают формы для работы с ними и т.п.
            Некоторые шаблоны используются только для scaffolding, другие регенирируют код при обновлениях.

            Всегда есть масса мест где можно заменить ручную работу.


            1. DotNetCraft
              30.03.2017 13:31

              А если создается что-то новое, более-менее часто, то как скопировав tt в новый проект, у вас уже все сгенерированно.

              Новый проект — новый конфиг. А учитывая движение в сторону микросервисов, то получается, что и конфиги почти пыстые. Максимум 5-7 микросервисов (это мой опыт) и всякие частные настройки для конкретного сервиса. Т.о. повторное использование tt тут не подходит. Да и любое повторное использование кода не подходит. Конфиг почти каждый раз пишется с 0.

              Надо сгенерировать mock config для тестов — поменяли пару строчек в шаблоне и он вам тестовые классы сделал.

              Посмотрите мой ответ Bronx. Там я описываю, как создавать конфиги для разных целей без особых усилий.

              В общем, пока я не увидел преимущество автоматической генерации конфигурационных классов с из заполнением. Скорей всего, тут кто к чему привык, ибо серебрянной пули не существует…

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

              Вот после этого я спорить не буду ибо согласен. Если есть смысл и возможность, то надо генерить.