MS DI, как настроить сервисы, используя информацию, известную только во время выполнения - PullRequest
0 голосов
/ 11 апреля 2020

Я использую Microsoft.Extensions.DependencyInjection 2.1.1 и у меня есть сервисы, которые используют шаблон параметров для получения своей конфигурации. Я хочу иметь возможность выбрать конкретную реализацию службы, используя информацию, известную только во время выполнения (например, чтение из конфигурации).

Если я знаю все возможные конкретные реализации службы и их параметры во время компиляции, я может сделать что-то вроде следующего, чтобы выбрать и настроить реализацию:

if (useImplementation1)
{
    services.Configure<MyServiceImplementation1Options>(config.GetSection("MyServiceImplementation1"));
    services.AddSingleton<IMyService, MyServiceImplementation1>();
}
else
{
    services.Configure<MyServiceImplementation2Options>(config.GetSection("MyServiceImplementation2"));
    services.AddSingleton<IMyService, MyServiceImplementation2>();
}

Есть ли способ настроить эту службу и ее параметры, используя только информацию, известную во время выполнения, например:

Type myServiceOptionsType = ... from configuration, e.g. typeof(MyServiceImplementation1Options)
Type myServiceImplementationType = ... from configuration, e.g. typeof(MyServiceImplementation1)
string myServiceConfigSection = ... from configuration, e.g. "MyServiceImplementation1"

??? what do I do next?

ОБНОВЛЕНИЕ

Что, я надеюсь, прояснит, что я ищу. Вот примеры классов: предположим, что Внедрение1 получает данные из файла XML, Внедрение 2 получает данные из базы данных SQL.

Код реализации 1 (в сборке MyAssembly):

public class MyServiceImplementation1Options
{
    public Uri MyXmlUrl {get; set;}
}
public class MyServiceImplementation1 : IMyService
{
    public MyServiceImplementation1(IOptions<MyServiceImplementation1Options> options)
    {
       ...
    }
    ... Implement IMyService ...
}

Реализация2 код (в сборке OtherAssembly):

public class MyServiceImplementation2Options
{
    public string ConnectionString {get; set;}
    public string ProviderName {get; set;}
}
public class MyServiceImplementation2 : IMyService
{
    public MyServiceImplementation2(IOptions<MyServiceImplementation2Options> options)
    {
       ...
    }
    ... Implement IMyService ...
}

Теперь я хотел бы выбрать между этими двумя реализациями, не обязательно иметь доступ во время компиляции к сборкам (MyAssembly и OtherAssembly), которые содержат реализации. Во время выполнения я считывал данные из файла конфигурации, который мог бы выглядеть примерно так (в дальнейшем представьте, что ключи и значения представляют собой словарь, переданный в MemoryConfigurationProvider - иерархическая конфигурация представлена ​​с использованием разделителей двоеточий. Она также может быть настроена использование appsettings.json с иерархией, представленной с использованием вложенности):

Конфигурация реализации1:

Key="MyServiceConcreteType" Value="MyServiceImplementation1,MyAssembly"
Key="MyServiceOptionsConcreteType" Value="MyServiceImplementation1Options,MyAssembly" 
Key="MyServiceOptionsConfigSection" Value="MyServiceImplementation1"

Key="MyServiceImplementation1:MyXmlUrl" Value="c:\MyPath\File.xml"

Конфигурация реализации2:

Key="MyServiceConcreteType" Value="MyServiceImplementation2,OtherAssembly"
Key="MyServiceOptionsConcreteType" Value="MyServiceImplementation2Options,OtherAssembly" 
Key="MyServiceOptionsConfigSection" Value="MyServiceImplementation2"

Key="MyServiceImplementation2:ConnectionString" Value="Server=...etc..."
Key="MyServiceImplementation2:ProviderName" Value="System.Data.SqlClient"

Ответы [ 3 ]

2 голосов
/ 11 апреля 2020

ОК, теперь я вижу, где путаница. Поскольку метод Configure не имеет не универсальных c версий, поэтому вы не знали, как передать в метод известный тип во время выполнения?

В этом случае я бы использовал ConfigureOptions метод, который позволяет вам передавать тип конфигуратора в качестве параметра. Тип должен реализовывать IConfigureOptions<T>, который определяет метод Configure(T) для настройки параметров T.

Например, этот тип настраивает MyServiceImplementation1Options с использованием IConfiguration:

class ConfigureMyServiceImplementation1 : 
    IConfigureOptions<MyServiceImplementation1Options>
{
    public ConfigureMyServiceImplementation1(IConfiguration config)
    {
    }

    public void Configure(MyServiceImplementation1Options options)
    {
        // Configure MyServiceImplementation1Options as per configuration section
    }
}

MyServiceImplementation1Options.Configure метод вызывается при разрешении IOptions<MyServiceImplementation1Options>, и вы можете ввести IConfiguration в тип для чтения конфигурации из указанного раздела.

И вы можете использовать такой тип в Startup :

// Assume you read this from configuration
var optionsType = typeof(MyServiceImplementation1Options);

// Assume you read this type from configuration
// Or somehow could find this type by options type, via reflection etc
var configureOptionsType = typeof(ConfigureMyServiceImplementation1);

// Assume you read this type from configuration
var implementationType = typeof(MyServiceImplementation1);

// Configure options using ConfigureOptions instead of Configure
// By doing this, options is configure by calling 
// e.g. ConfigureMyServiceImplementation1.Configure
services.ConfigureOptions(configureOptionsType);

С точки зрения регистрации службы существуют не универсальные c версии Add* методов. Например, код ниже регистрирует тип, известный во время выполнения как IMyService:

// Register service
services.AddSingleton(typeof(IMyService), implementationType);
1 голос
/ 12 апреля 2020

Как упомянул @weichch, основной проблемой здесь является отсутствие перегрузки общего типа c Configure. Я думаю, что это может рассматриваться как упущение со стороны Microsoft (но было бы неплохо создать запрос функции для этого).

Кроме того, решение weichch, вы также можете использовать отражение для вызова Configure<T> метод по вашему выбору. Это будет выглядеть так:

// Load configuration
var appSettings = this.Configuration.GetSection("AppSettings");
Type serviceType =
    Type.GetType(appSettings.GetValue<string>("MyServiceConcreteType"), true);
Type optionsType =
    Type.GetType(appSettings.GetValue<string>("MyServiceOptionsConcreteType"), true);
string section = appSettings.GetValue<string>("MyServiceOptionsConfigSection");

// Register late-bound component
services.AddSingleton(typeof(IMyService), serviceType);

// Configure the component.
// Gets a Confige<{optionsType}>(IServiceCollection, IConfiguration) method and invoke it.
var configureMethod =
    typeof(OptionsConfigurationServiceCollectionExtensions).GetMethods()
    .Single(m => m.GetParameters().Length == 2)
    .MakeGenericMethod(typeof(string));

configureMethod.Invoke(
    null, new object[] { services, this.Configuration.GetSection("AppSettings") });

Где конфигурация может выглядеть следующим образом:

{
  "AppSettings": {
    "MyServiceConcreteType": "MyServiceImplementation1,MyAssembly",
    "MyServiceOptionsConcreteType": "MyServiceImplementation1Options,MyAssembly",
    "MyServiceOptionsConfigSection": "MyServiceImplementation1",
  },
  "MyServiceImplementation1": {
    "SomeConfigValue": false
  }
}
0 голосов
/ 12 апреля 2020

Выбор конкретной реализации на основе параметров

Вы можете выбрать конкретную реализацию интерфейса на основе типа параметров, используя перегрузку AddSingleton(implementationFactory). Используя эту перегрузку, вы можете отложить разрешение конкретного типа для использования до тех пор, пока не получите доступ к IOptionsSnapshot<>. В зависимости от ваших требований и реализаций интерфейса, который вы используете, вы можете динамически переключать возвращаемый тип, используя вместо этого IOptionsMonitor<>.

Если это поможет, вы можете думать о фабричном шаблоне как о способе curry создание объекта при использовании контейнера DI.

class Program
{
    static void Main()
    {
        // WebHostBuilder should work similarly.
        var hostBuilder = new HostBuilder()
            .ConfigureAppConfiguration(cfgBuilder =>
            {
                // Bind to your configuration as you see fit.
                cfgBuilder.AddInMemoryCollection(new[]
                {
                    KeyValuePair.Create("ImplementationTypeName", "SomeFooImplementation"),
                });
            })
            .ConfigureServices((hostContext, services) =>
            {
                // Register IFoo implementation however you see fit.
                // See below for automatic registration helper.
                services
                    .AddSingleton<FooFactory>()
                    // Registers options type for FooFactory
                    .Configure<FooConfiguration>(hostContext.Configuration)
                    // Adds an IFoo provider that uses FooFactory.
                    // Notice that we pass the IServiceProvider to FooFactory.Get
                    .AddSingleton<IFoo>(
                        sp => sp.GetRequiredService<FooFactory>().Get(sp));
            });

        IHost host = hostBuilder.Build();
        IFoo foo = host.Services.GetRequiredService<IFoo>();
        Debug.Assert(foo is SomeFooImplementation);
    }
}

// The interface that we want to pick different concrete
// implementations based on a value in an options type.
public interface IFoo
{
    public string Value { get; }
}

// The configuration of which type to use.
public class FooConfiguration
{
    public string ImplementationTypeName { get; set; } = string.Empty;
}

// Factory for IFoo instances. Used to delay resolving which concrete
// IFoo implementation is used until after all services have been
// registered, including configuring option types.
public class FooFactory
{
    // The type of the concrete implementation of IFoo
    private readonly Type _implementationType;

    public FooFactory(IOptionsSnapshot<FooConfiguration> options)
    {
        _implementationType = ResolveTypeNameToType(
            options.Value.ImplementationTypeName);
    }

    // Gets the requested implementation type from the provided service
    // provider.
    public IFoo Get(IServiceProvider sp)
    {
        return (IFoo)sp.GetRequiredService(_implementationType);
    }

    private static Type ResolveTypeNameToType(string typeFullName)
    {
        IEnumerable<Type> loadedTypes = Enumerable.SelectMany(
               AppDomain.CurrentDomain.GetAssemblies(),
               assembly => assembly.GetTypes());

        List<Type> matchingTypes = loadedTypes
            .Where(type => type.FullName == typeFullName)
            .ToList();

        if (matchingTypes.Count == 0)
        {
            throw new Exception($"Cannot find any type with full name {typeFullName}.");
        }
        else if (matchingTypes.Count > 1)
        {
            throw new Exception($"Multiple types matched full name {typeFullName}.");
        }

        // TODO: add check that requested type implements IFoo

        return matchingTypes[0];
    }
}

Заполнение контейнера на основе параметров

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

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

Один обходной путь, который вызвал у меня проблемы: постройте поставщика услуг при заполнении набора сервисов. Какие объекты живут, у какого поставщика услуг здесь возникает путаница, а время создания поставщика услуг сильно меняет результаты. Такой подход вызывает проблемы при отладке. Я бы этого избегал.

Если вы можете сделать упрощенное предположение, что все возможные конкретные типы реализации находятся в сборках, которые уже загружены, то вы могли бы рассмотреть схему «автоматической регистрации». Это означает, что вам не нужно добавлять AddSingleton<> & c. для каждого нового типа.

// when configuring services, add a call to AddAutoRegisterTypes()

// ...
            .ConfigureServices((hostContext, services) =>
            {
                services
                    // Finds and registers config & the type for all types with [AutoRegister]
                    .AddAutoRegisterTypes(hostContext.Configuration)
                    // ...
            });

// ...

// The first concrete implementation. See below for how AutoRegister
// is used & implemented.
[AutoRegister(optionsType: typeof(FooFirstOptions), configSection: "Foo1")]
public class FooFirst : IFoo
{
    public FooFirst(IOptionsSnapshot<FooFirstOptions> options)
    {
        Value = $"{options.Value.ValuePrefix}First";
    }

    public string Value { get; }
}

public class FooFirstOptions
{
    public string ValuePrefix { get; set; } = string.Empty;
}

// The second concrete implementation. See below for how AutoRegister
// is used & implemented.
[AutoRegister(optionsType: typeof(FooSecondOptions), configSection: "Foo2")]
public class FooSecond : IFoo
{
    public FooSecond(IOptionsSnapshot<FooSecondOptions> options)
    {
        Value = $"Second{options.Value.ValueSuffix}";
    }

    public string Value { get; }
}

public class FooSecondOptions
{
    public string ValueSuffix { get; set; } = string.Empty;
}

// Attribute used to annotate a type that should be:
// 1. automatically added to a service collection and
// 2. have its corresponding options type configured to bind against
//    the specificed config section.
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class AutoRegisterAttribute : Attribute
{
    public AutoRegisterAttribute(Type optionsType, string configSection)
    {
        OptionsType = optionsType;
        ConfigSection = configSection;
    }

    public Type OptionsType { get; }

    public string ConfigSection { get; }
}

public static class AutoRegisterServiceCollectionExtensions
{
    // Helper to call Configure<T> given a Type argument. See below for more details.
    private static readonly Action<Type, IServiceCollection, IConfiguration> s_configureType
        = MakeConfigureOfTypeConfig();

    // Automatically finds all types with [AutoRegister] and adds
    // them to the service collection and configures their options
    // type against the provided config.
    public static IServiceCollection AddAutoRegisterTypes(
        this IServiceCollection services,
        IConfiguration config)
    {
        foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
        {
            foreach (Type type in assembly.GetTypes())
            {
                var autoRegAttribute = (AutoRegisterAttribute?)Attribute
                    .GetCustomAttributes(type)
                    .SingleOrDefault(attr => attr is AutoRegisterAttribute);

                if (autoRegAttribute != null)
                {
                    IConfiguration configForType = config.GetSection(
                        autoRegAttribute.ConfigSection);
                    s_configureType(
                        autoRegAttribute.OptionsType,
                        services,
                        configForType);
                    services.AddSingleton(type);
                }
            }
        }

        return services;
    }

    // There is no non-generic analog to
    // OptionsConfigurationServiceCollectionExtensions.Configure<T>(IServiceCollection, IConfiguration)
    //
    // Therefore, this finds the generic method via reflection and
    // creates a wrapper that invokes it given a Type parameter.
    private static Action<Type, IServiceCollection, IConfiguration> MakeConfigureOfTypeConfig()
    {
        const string FullMethodName = nameof(OptionsConfigurationServiceCollectionExtensions) + "." + nameof(OptionsConfigurationServiceCollectionExtensions.Configure);

        MethodInfo? configureMethodInfo = typeof(OptionsConfigurationServiceCollectionExtensions)
            .GetMethod(
                nameof(OptionsConfigurationServiceCollectionExtensions.Configure),
                new[] { typeof(IServiceCollection), typeof(IConfiguration) });

        if (configureMethodInfo == null)
        {
            var msg = $"Cannot find expected {FullMethodName} overload. Has the contract changed?";
            throw new Exception(msg);
        }

        if (   !configureMethodInfo.IsGenericMethod
            || configureMethodInfo.GetGenericArguments().Length != 1)
        {
            var msg = $"{FullMethodName} does not have the expected generic arguments.";
            throw new Exception(msg);
        }

        return (Type typeToConfigure, IServiceCollection services, IConfiguration configuration) =>
        {
            configureMethodInfo
                .MakeGenericMethod(typeToConfigure)
                .Invoke(null, new object[] { services, configuration });
        };
    }
}

Заполнение контейнера на основе конфигурации

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

  1. вручную читать конфигурацию, доступную вам во время ConfigureServices,
  2. вручную Bind для вашего типа опции,
  3. пропускать / переопределять делегатов конфигурации,
  4. загрузить необходимые сборки и
  5. заполнить контейнер DI.

У меня нет примера для этого. Мой опыт показывает, что достаточно автоматической регистрации c.

Компиляция

Этот код был написан и протестирован с использованием NET SDK 3.1.101 для netcoreapp2.1 и с использованием версии 2.1.1 пакетов «Microsoft.Extensions.Configuration. *». Я опубликовал полную рабочую копию в виде GitHub Gist .

Заключительные мысли

Использование отражения в FooFactory и AddAutoRegisterTypes предполагает, что это не звонил часто. Если вы просто используете их один раз при запуске для долгоживущего сервиса, все должно быть в порядке.

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...