[Из песочницы] 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();
                 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 _testOptions;
private readonly IOptionsSnapshot _testOptionsSnapshot;
private readonly IOptionsMonitor _testOptionsMonitor;

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

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

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

    services.AddSingleton();
    services.AddScoped();
    services.AddTransient();

    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, может там прояснят такое поведение.

© Habrahabr.ru