IOptions и его друзья

Во время разработки часто возникает потребность для вынесения параметров в конфигурационные файлы. Да и вообще — хранить разные конфигурационный константы в коде является признаком дурного тона. Один из вариантов хранения настроек — использования конфигурационных файлов. .Net Core из коробки умеет работать с такими форматами как: json, ini, xml и другие. Так же есть возможность писать свои провайдеры конфигураций. (Кстати говоря за работу с конфигурациями отвечает сервис IConfiguration и IConfigurationProvider — для доступа к конфигурациям определенного формата и для написания своих провайдеров)


image


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


На MSDN есть статья, которая должна раскрывать все вопросы. Но, как всегда, не все так просто.


IOptions


Does not support:
Reading of configuration data after the app has started.
Named options

Is registered as a Singleton and can be injected into any service lifetime.

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


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


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


IOptionsSnapshot


Is useful in scenarios where options should be recomputed on every request

Is registered as Scoped and therefore cannot be injected into a Singleton service.

Supports named options

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


MSDN нам говорит, что не может быть заинжекчен в Singletone — на самом деле может (это прям тема для отдельного поста), но тогда и сам он начинает себя вести как Singletone.


IOptionsMonitor


Is used to retrieve options and manage options notifications for TOptions instances.

Is registered as a Singleton and can be injected into any service lifetime.

Supports:
Change notifications
Named options
Reloadable configuration
Selective options invalidation (IOptionsMonitorCache)

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


IOptionsMonitorCache — интерфейс для построения обычного кэша на базе IOptionsMonitor.


Практика


Все тесты проводились на следующем окружении


sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.5
BuildVersion:   19F101

dotnet --version
3.1.301

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


Сразу оставлю ссылку на код, если кто-то захочет ознакомиться подробнее.


В качестве примера будет простое Web API


 public class Program
 {
     public static void Main(string[] args)
     {
         CreateHostBuilder(args).Build().Run();
     }

     public static IHostBuilder CreateHostBuilder(string[] args) =>
         Host.CreateDefaultBuilder(args)
             .ConfigureWebHostDefaults(webBuilder =>
             {
                 webBuilder.UseKestrel();
                 webBuilder.UseStartup<Startup>();
                 webBuilder.UseUrls("http://*:5010/");
             })
             .UseDefaultServiceProvider(options => options.ValidateScopes = false);
 }

Клиент, который будет к нему обращаться


 static async Task Main(string[] args)
 {
     using var client = new HttpClient();
     var prevResponse = String.Empty;

     while (true)
     {
         var response = await client.GetStringAsync("http://localhost:5010/settings");

         if (response != prevResponse) // пишем в консоль только, если настройки изменились
         {
             Console.WriteLine(response);
             prevResponse = response;
         }
     }
 }

В Web API создаем 3 сервиса, который принимает все 3 варианта конфигураций в конструктор и возвращают текущее значение.


private readonly IOptions<TestGroupSettings> _testOptions;
private readonly IOptionsSnapshot<TestGroupSettings> _testOptionsSnapshot;
private readonly IOptionsMonitor<TestGroupSettings> _testOptionsMonitor;

public ScopedService(IOptions<TestGroupSettings> testOptions, IOptionsSnapshot<TestGroupSettings> testOptionsSnapshot,
    IOptionsMonitor<TestGroupSettings> testOptionsMonitor)
{
    _testOptions = testOptions;
    _testOptionsSnapshot = testOptionsSnapshot;
    _testOptionsMonitor = testOptionsMonitor;
}

Сервисы будут 3х скоупов: Singletone, Scoped и Transient.


public void ConfigureServices(IServiceCollection services)
{
    services.Configure<TestGroupSettings>(Configuration.GetSection("TestGroup"));

    services.AddSingleton<ISingletonService, SingletonService>();
    services.AddScoped<IScopedService, ScopedService>();
    services.AddTransient<ITransientService, TransientService>();

    services.AddControllers();
}

В процессе работы нашего Web Api изменяем значение TestGroup.Test файла appsettings.json


Имеем следующую картину:
Сразу после запуска


SingletonService IOptions value: 0
SingletonService IOptionsSnapshot value: 0
SingletonService IOptionsMonitor value: 0

ScopedService IOptions value: 0
ScopedService IOptionsSnapshot value: 0
ScopedService IOptionsMonitor value: 0

TransientService IOptions value: 0
TransientService IOptionsSnapshot value: 0
TransientService IOptionsMonitor value: 0

Изменяем нашу настройку и получаем интересную картину
Сразу после изменения


SingletonService IOptions value: 0
SingletonService IOptionsSnapshot value: 0 // не изменилась
SingletonService IOptionsMonitor value: 0 // не изменилась

ScopedService IOptions value: 0
ScopedService IOptionsSnapshot value: // стала пустой
ScopedService IOptionsMonitor value: 0 // не изменилась

TransientService IOptions value: 0
TransientService IOptionsSnapshot value: // стала пустой
TransientService IOptionsMonitor value: 0 // не изменилась

Следующий вывод в консоль (конфиг больше не менялся)


SingletonService IOptions value: 0
SingletonService IOptionsSnapshot value: 0 // не изменилась
SingletonService IOptionsMonitor value: 0 // не изменилась

ScopedService IOptions value: 0
ScopedService IOptionsSnapshot value: changed setting // изменилась
ScopedService IOptionsMonitor value: 0 // не изменилась

TransientService IOptions value: 0
TransientService IOptionsSnapshot value: changed setting // изменилась
TransientService IOptionsMonitor value: 0 // не изменилась

Последний вывод (конфиг также не менялся)


SingletonService IOptions value: 0
SingletonService IOptionsSnapshot value: 0 // не изменилась
SingletonService IOptionsMonitor value: changed setting // изменилась

ScopedService IOptions value: 0
ScopedService IOptionsSnapshot value: changed setting // изменилась
ScopedService IOptionsMonitor value: changed setting // изменилась

TransientService IOptions value: 0
TransientService IOptionsSnapshot value: changed setting // изменилась
TransientService IOptionsMonitor value: changed setting // изменилась

Что имеем в итоге? А имеем то, что IOptionsMonitor — не такой шустрый, как нам говорит документация. Как можно заметить IOptionsSnapshot может вернуть пустое значение. Но, он работает быстрее, чем IOptionsMonitor.


Пока не особо понятно откуда берется это пустое значение. И самое интересное, что подобное поведение проявляется не всегда. Как-то через раз в моем примере IOptionsMonitor и IOptionsSnapshot отрабатывают одновременно.


Выводы


Если вам нужно передавать конфигурацию, которые никогда в процессе жизни вашего приложения не будут меняться используете IOptions.


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


Если же вам нужны наиболее актуальные значения (или почти) используйте IOptionsMonitor.


Буду рад, если вы запустите пример у себя, и расскажете, повторяется подобное поведение или нет. Возможно мы имеем баг на MacOS, а может это by design.


Продолжу разбираться с этой темой, а пока завел issue, может там прояснят такое поведение.

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 18

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

    IOptions и его друзья

    Стоп. Зачем вы путаете IOptions, который нужен для работы с настройками, и работу с конфигурацией, для которой есть IConfiguration? Они даже в пакетах разных лежат.


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

    Нет, неправильно. Никакого конфигурационного файла. Просто результат применения Configure.


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

    … а как он задумывал и почему?


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

    Именно так. "Многие люди" привыкли к тому, что изменения файла конфигурации приводит к перезагрузке приложения (скажем, asp.net себя так вел и ведет). Поэтому для них неизменность настроек — более-менее ожидаема.


    MSDN нам говорит, что не может быть заинжекчен в Singletone — на самом деле может

    … если валидацию скоупов не делать. А если делать — то не может.


    .UseDefaultServiceProvider(options => options.ValidateScopes = false);

    Вот поэтому и "можно".


    Обновляет информацию о конфигурации при каждом запросе. И что немаловажно, не изменяет ее во время запроса.

    Какого такого запроса?


    И, что характерно, нет, он не "обновляет информацию о конфигурации". Он просто заново выполняет Configure.


    А имеем то, что IOptionsMonitor — не такой шустрый, как нам говорит документация.

    А документация ничего не говорит о скорости. Про реальное время — это ваши собственные домыслы. А в реальности там стоит change token, который зависит от файловой системы, и который никогда особо быстрым не был.


    Но, он [IOptionsSnapshot] работает быстрее, чем IOptionsMonitor.

    Не "быстрее". Он возвращает вам актуальные значения на момент первого создания в scope.


    Честное слово, лучше бы вы рассказали, как, собственно, IOptions-что-нибудь настраиваются. И как именно обрабатываются изменения. Тогда было бы понятно, что конкретно происходит.

      0

      Чем настройки отличаются от конфигурации?

        0

        В данном случае, конфигурация (IConfiguration) — это нечто внешнее по отношению к приложению, а настройки (IOptions) — внутреннее.

          0
          Спасибо за классификацию) давно задаюсь вопросом как различить эти «синонимы».
          но что насчет Settings?
            +1

            К счастью, settings в этой системе терминов нет.

              +1
              Что-то, что настраивает сам пользователь через интерфейс приложения.
                0
                а в чем разница с опциями тогда?
                  +1

                  Опции (которые IOptions) могут быть просто захардкожены.

        0
        может кто подскажет если уж это тема затронута.
        есть возможность переписать значения некоторых полей в своей конфигурацию в appsettings.json через интерфейс IOptions…?

        допустим в ConfigureServices сделал запись:

        services.Configure<ModelSettingsForMachine>(_configuration.GetSection("SettingsForMachine"));
        

        и чтобы переписать какое либо значение пришлось сделать такой обходной метод с обращением самому файлу:
        
        public static void AddOrUpdateAppSetting<T>(string key, T value)
                {
                    try
                    {
                        var filePath = Path.Combine(AppContext.BaseDirectory, "appsettings.json");
        
                        Console.WriteLine("appSettings.json filePath " + filePath);
                        string json = File.ReadAllText(filePath);
                        dynamic jsonObj = Newtonsoft.Json.JsonConvert.DeserializeObject(json);
        
                        string path = key.Split(":").Aggregate((i, j) => i + "." + j);
                        dynamic acme = jsonObj.SelectToken(path);
                        acme.Value = value;
                        
                        string output = Newtonsoft.Json.JsonConvert.SerializeObject(jsonObj, Newtonsoft.Json.Formatting.Indented);
                        File.WriteAllText(filePath, output);
        
                    }
                    catch (ConfigurationErrorsException)
                    {
                        Console.WriteLine("Error writing app settings");
                    }
                }


        есть более правильные варианты?
          +1
          есть возможность переписать значения некоторых полей в своей конфигурацию в appsettings.json через интерфейс IOptions…?

          Нет. Во-первых, IOptions вообще не про конфигурационные файлы. Во-вторых, у IConfigurationProvider есть метод Load, но нет метода Save, как и у FileConfigurationProvider. Вся эта инфраструктура — только для чтения:


          Configuration providers read configuration data from key-value pairs using a variety of configuration sources
          +1
          Да и вообще — хранить разные конфигурационный константы в коде является признаком дурного тона.

          Сильное заявление ©
          Принцип YAGNI говорит нам о том, что задачу надо решать наиболее простым способом, удовлетворяющим критерии.
          Если константы достаточно, используйте её.
          Вынося её в конфиг, вы


          1. Пишите лишний код, ведь теперь вам теперь нужна проверка валидности значения, юнит тесты для разных значений.
          2. Усложняете разработку, т.к. вам теперь надо держать в уме и проверять, что код работает с разными значениями.
          3. Тратите время коллег. Им нужно будет понять: зачем это настраивается, т.к. "константа является признаком дурного тона" не самое очевидное правило.
          4. Делаете ваш софт менее предсказуемым, ведь значения в конфиге могут меняться. (забыли обновить конфиг на продакшене, "но на моей машине работает!" ©)
          5. Делаете некоторые типы рефакторинга более трудными.
          6. "Замусориваете" конфиг, усложняя жизнь тем, кто будет разверывать и сопровождать ваш софт
            +1

            Да просто само понятие "конфигурационная константа" — оксюморон. Либо константа, либо конфигурация.

            0

            А это вообще реальный кейс, изменение appsettings.json во время работы приложения? Любой мало-мальски серьёзный проект деплоится не руками, cicd и так далее, если это прод — нельзя просто так и подправить конфиг. Ну цикл же! Тикет, ветка, MR, ревью, мерж, qa и вот это всё. Совершенно точно приложение передеплоится, нет же реальной необходимости читать конфиг без перезапуска.

              0
              ну допустим у меня проект на asp.net core хостится на малинке, к ней подключены по последовательному порту несколько девайсов, в добавок используются штатные пины для управления другими девайсами. настроку портов и пинов увел в appsettings.json, ну не создавать же отдельный файл когда есть тот который подходит по философии использования.

              и если так случилось что наименование порта или использование пина (или второстепенных настроек) изменилось то надо менять настройку в файле руками ( за исключением если нет админской панели, что делает по сути тоже самое)
                0

                И вам обязательно нужно, чтобы приложение не перестартовалось в это время?

                  0
                  в моем случаи это добавляет удобства что с приложеним можно работать без необходимости его перезапускать. то что я описал это делается один раз перед тем как комплекс уходит в эксплуатацию, за исключением тех моментов когда надо разбираться на месте с вопросами «а что случилось и почему нет связи с инструментами».

                  и есть нюансы когда даешь инструкцию заказчику по настройке, а там пункт: «после настройки перезапустите приложение» — человек может это воспринять как то, что приложение криво работает и совет из ряда «перегрузите роутер»
                0
                Если конфиг хранится во внешней системе. Один из вариантов, что именно подменяется settings.json
                В вашем случае — вы совершенно правы
                0
                Если вам нужно передавать конфигурацию, которые никогда в процессе жизни вашего приложения не будут меняться используете IOptions.

                … только сейчас обратил внимание. А то, что IOptions не умеет именованные экземпляры — типа, не важно?

                Only users with full accounts can post comments. Log in, please.