Инъекции IOptions

Мне кажется, что плохой идеей иметь службу домена требуется экземпляр IOptions для передачи его конфигурации. Теперь у меня есть дополнительные (ненужные?) Зависимости в библиотеке. Я видел множество примеров инъекций IOptions по всему Интернету, но я не вижу дополнительных преимуществ.

Почему бы просто не ввести настоящий POCO в службу?

    services.AddTransient<IConnectionResolver>(x =>
    {
        var appSettings = x.GetService<IOptions<AppSettings>>();

        return new ConnectionResolver(appSettings.Value);
    });

Или даже используйте этот механизм:

        AppSettings appSettings = new AppSettings();

        Configuration.GetSection("AppSettings").Bind(appSettings);

        services.AddTransient<IConnectionResolver>(x =>
        {      
             return new ConnectionResolver(appSettings.SomeValue);
        });

Использование настроек:

public class MyConnectionResolver 
{
     // Why this?
     public MyConnectionResolver(IOptions<AppSettings> appSettings)
     {
           ... 
     }

     // Why not this?
     public MyConnectionResolver(AppSettings appSettings)
     {
           ... 
     }

     // Or this
     public MyConnectionResolver(IAppSettings appSettings)
     {
           ... 
     }
}

Почему дополнительные зависимости? Что IOptions покупают меня вместо старого школьного способа инъекции?

Ответ 1

Технически ничто не мешает вам регистрировать свои классы POCO с помощью ASP.NET Core Dependency Injection или создавать класс-оболочку и возвращать из IOption<T>.Value.

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

Как вы можете видеть в этом примере кода, если вы регистрируете свои параметры через services.Configure<AppSettings>(Configuration.GetSection("AppSettings")); он будет читать и связывать настройки из appsettings.json в модель и дополнительно отслеживать ее для изменений. Когда appsettings.json будет отредактирован и будет пересобирать модель с новыми значениями, как показано здесь.

Конечно, вам нужно решить для себя, если вы хотите протечь немного инфраструктуры в свой домен или передать дополнительные функции, предлагаемые пакетом Microsoft.Extension.Options. Это довольно маленький пакет, который не привязан к ASP.NET Core, поэтому его можно использовать независимо от него.

Пакет Microsoft.Extension.Options достаточно мал, что он содержит только абстракции и конкретные services.Configure IConfiguration перегрузку, которая для IConfiguration (которая ближе связана с получением конфигурации, командной строки, json, environment, azure key vault и т.д. ) представляет собой отдельный пакет.

Так что в целом, зависимости от "инфраструктуры" довольно ограничены.

Ответ 2

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

Это сильно раздражало меня, что я написал утилиту для ввода POCO в мою библиотеку классов, заполненную значениями из раздела appSettings.json. Добавьте следующий класс в проект приложения:

public static class ConfigurationHelper
{
    public static T GetObjectFromConfigSection<T>(
        this IConfigurationRoot configurationRoot,
        string configSection) where T : new()
    {
        var result = new T();

        foreach (var propInfo in typeof(T).GetProperties())
        {
            var propertyType = propInfo.PropertyType;
            if (propInfo?.CanWrite ?? false)
            {
                var value = Convert.ChangeType(configurationRoot.GetValue<string>($"{configSection}:{propInfo.Name}"), propInfo.PropertyType);
                propInfo.SetValue(result, value, null);
            }
        }

        return result;

    }
}

Возможно, есть некоторые улучшения, которые можно было бы сделать, но он работал хорошо, когда я тестировал его с помощью простых значений строк и целых чисел. Вот пример того, где я использовал это в проекте приложения Startup.cs → ConfigureServices для класса настроек с именем DataStoreConfiguration и секции appSettings.json с тем же именем:

services.AddSingleton<DataStoreConfiguration>((_) =>
    Configuration.GetObjectFromConfigSection<DataStoreConfiguration>("DataStoreConfiguration"));

Конфигурация appSettings.json выглядела примерно так:

{
  "DataStoreConfiguration": {
    "ConnectionString": "Server=Server-goes-here;Database=My-database-name;Trusted_Connection=True;MultipleActiveResultSets=true",
    "MeaningOfLifeInt" : "42"
  },
 "AnotherSection" : {
   "Prop1" : "etc."
  }
}

Класс DataStoreConfiguration был определен в моем проекте библиотеки и выглядел следующим образом:

namespace MyLibrary.DataAccessors
{
    public class DataStoreConfiguration
    {
        public string ConnectionString { get; set; }
        public int MeaningOfLifeInt { get; set; }
    }
}

С помощью этой конфигурации приложений и библиотек мне удалось вставить конкретный экземпляр DataStoreConfiguration непосредственно в мою библиотеку, используя инъекцию конструктора без оболочки IOption:

using System.Data.SqlClient;

namespace MyLibrary.DataAccessors
{
    public class DatabaseConnectionFactory : IDatabaseConnectionFactory
    {

        private readonly DataStoreConfiguration dataStoreConfiguration;

        public DatabaseConnectionFactory(
            DataStoreConfiguration dataStoreConfiguration)
        {
            // Here we inject a concrete instance of DataStoreConfiguration
            // without the `IOption` wrapper.
            this.dataStoreConfiguration = dataStoreConfiguration;
        }

        public SqlConnection NewConnection()
        {
            return new SqlConnection(dataStoreConfiguration.ConnectionString);
        }
    }
}

Развязка является важным соображением для DI, поэтому я не уверен, почему Microsoft подключила пользователей к связям своих библиотек классов с внешней зависимостью, например IOptions, независимо от того, насколько тривиальна она кажется или какие преимущества она якобы предоставляет. Я также хотел бы предположить, что некоторые из преимуществ IOptions кажутся чрезмерными. Например, это позволяет мне динамически изменять конфигурацию и отслеживать изменения - я использовал три других контейнера DI, которые включали эту функцию, и я никогда не использовал ее один раз... Между тем, я могу фактически гарантировать вам, что команды захотят ввести классы или интерфейсы POCO в библиотеки, чтобы их параметры заменили ConfigurationManager, и опытные разработчики не будут довольны внешним интерфейсом обертки. Я надеюсь, что утилита, аналогичная тому, что я описал здесь, включена в будущие версии ASP.NET Core OR, и кто-то дает мне убедительный аргумент в пользу того, почему я ошибаюсь.

Ответ 3

Я не выношу рекомендации IOptions. Это дрянной дизайн, чтобы заставить это на разработчиков. IOptions должны быть четко документированы как необязательные, о иронии.

Это то, что я делаю для своих значений конфигурации

var mySettings = new MySettings();
Configuration.GetSection("Key").Bind(mySettings);

services.AddTransient(p => new MyService(mySettings));

Вы сохраняете сильную типизацию и не нуждаетесь в использовании IOptions в своих службах/библиотеках.