【问题标题】:MS DI how to configure services using information known only at runtimeMS DI 如何使用仅在运行时已知的信息来配置服务
【发布时间】:2020-04-12 17:14:50
【问题描述】:

我正在使用 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?

更新

我希望能澄清我在寻找什么。下面是示例类:假设 Implementation1 从 XML 文件中获取数据,Implementation2 从 SQL 数据库中获取数据。

Implementation1 代码(在程序集 MyAssembly 中):

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

Implementation2 代码(在组装 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"

Implementation2 配置:

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"

【问题讨论】:

  • 好的,我删除了我的答案以减少混淆。我的问题是:您的应用程序中是否需要多个可供选择的实现?或者您只需将您的应用配置为使用一种实现方式?
  • 正如它目前所写的,目前还不清楚你在问什么。您能否重新格式化问题,以便我们更清楚地了解当前问题以及您实际上想要做什么?您的运行时示例 sudo-code 不完整,因此不清楚。
  • @weichch - 我不需要一个以上的实现:我只想能够将我的应用程序配置为使用一种在编译时未知的实现。我已经更新了问题,希望能更清楚。
  • @Joe 是配置文件让我感到困惑。您是否使用 .net-core 配置(即 appsettings.json)?看起来您将不得不使用一些反射措施来根据从配置中提取的内容手动配置服务集合。
  • 此配置在启动时是否已知,并且在应用程序运行期间保持不变,还是要求在应用程序继续运行时能够更改配置?

标签: c# .net-core dependency-injection


【解决方案1】:

好的,现在我知道混乱在哪里了。因为Configure方法没有非泛型版本,所以不知道如何将运行时已知的类型传递给方法?

在这种情况下,我会使用ConfigureOptions 方法,它允许您将配置器类型作为参数传递。该类型必须实现IConfigureOptions&lt;T&gt;,它定义了Configure(T)方法来配置T的选项。

例如这种类型使用IConfiguration配置MyServiceImplementation1Options

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

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

MyServiceImplementation1Options.Configure 方法在解析IOptions&lt;MyServiceImplementation1Options&gt; 时被调用,您可以将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);

在服务注册方面,Add* 方法有非泛型版本。例如,下面的代码在运行时注册类型为IMyService

// Register service
services.AddSingleton(typeof(IMyService), implementationType);

【讨论】:

  • 我不明白这有什么帮助。您的 ServiceFactory 似乎需要在编译时知道具体的实现(Service1 和 Service2)。另外,在我的例子中,Service1 和 Service2 有不同的配置选项。
  • @Joe OK 现在你的评论让我想知道我是否真的理解你的问题。你不知道你有什么具体类型吗?配置部分、选项和您的服务之间的关系是什么?
  • 例如:假设IMyService是一个数据访问服务。我今天有一个具体的实现,可以从 XML 文件中获取数据。明天我可能会有一个替代实现,它从 SQL 数据库中获取数据。或来自 Web API。在这个阶段,我不知道存在哪些实现,因为它们还没有写出来。每个可能的实现都有自己的配置部分(XML 文件的路径、SQL 数据库的连接字符串或 Web api 的 URL),并且有自己的带有相关选项的选项类。
  • 试用后,我仍然对这个解决方案不太满意。 IConfigureOptions&lt;TOptions&gt; 实现的明显位置是在相应的服务实现程序集中。但要实现这一点,我需要调用 config,GetSection(section).Bind(options),这需要我依赖 Microsoft.Extensions.Configuration.Binder 和 Microsoft.Extensions.Configuration,而之前我只依赖于 Microsoft.Extensions.Options。我必须说我发现这个选项模式很复杂,而且记录不充分:(
  • ... cont'd. 我可以通过一个接口(例如IOptionsBinder)和一个方法来解决这个依赖关系,例如Bind(IConfiguration configuration, object options),然后将其注入到我的 IConfigureOptions&lt;TOptions&gt; 实现中。但如果这是一个有用/推荐的模式,我很惊讶抽象不是开箱即用的。
【解决方案2】:

根据选项选择具体实现

您可以使用AddSingleton(implementationFactory) 重载,根据选项类型选择接口的具体实现。通过使用此重载,您可以延迟解析要使用的具体类型,直到您可以访问IOptionsSnapshot&lt;&gt;。根据您的要求和您使用的接口的实现,您可以使用 IOptionsMonitor&lt;&gt; 来动态切换返回的类型。

如果有帮助,您可以将工厂模式视为使用 DI 容器时创建 curry 对象的一种方式。

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&lt;&gt; &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 容器,您可以

  1. ConfigureServices 期间手动读取您可用的配置,
  2. 手动 Bind 到您的选项类型,
  3. 跳过/重新实现配置委托,
  4. 加载所需的程序集,然后
  5. 填充 DI 容器。

我没有这方面的例子。我的经验是自动注册方法就足够了。

编译

此代码是使用针对 netcoreapp2.1 的 .NET SDK 3.1.101 并使用“Microsoft.Extensions.Configuration.*”包的 2.1.1 版编写和测试的。我以GitHub Gist 的形式发布了一份完整的工作副本。

结束的想法

FooFactoryAddAutoRegisterTypes 中使用反射假设它们不被频繁调用。如果您只是在启动时使用这些来获得长期服务,那应该没问题。

AddAutoRegisterTypes 中的程序集搜索会随着程序变大而变慢。有一些方法可以加快速度,比如只检查具有已知命名模式的程序集。

虽然所有这些都有效,但我很想知道是否有更优雅的方式来做这样的事情。自动注册方案可能有点太神奇了,但我觉得配置方案复制了很多选项系统提供的内容。

【讨论】:

    【解决方案3】:

    正如@weichch 所提到的,这里的主要痛点是不存在非泛型Configure 重载。我认为这可以看作是微软的疏忽(但为此创建一个功能请求将是一个好主意)。

    除了weichch的解决方案,您还可以利用反射来调用您选择的Configure&lt;T&gt;方法。这看起来像这样:

    // 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
      }
    }
    

    【讨论】:

    • 谢谢。就我而言,这些服务将在内部开发,因此我可以要求它们包含 IConfigureOptions&lt;T&gt; 实现,并且 @weichch 的解决方案将起作用。
    猜你喜欢
    • 1970-01-01
    • 2010-10-01
    • 2021-10-16
    • 2017-03-31
    • 2011-12-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多