根据选项选择具体实现
您可以使用AddSingleton(implementationFactory) 重载,根据选项类型选择接口的具体实现。通过使用此重载,您可以延迟解析要使用的具体类型,直到您可以访问IOptionsSnapshot<>。根据您的要求和您使用的接口的实现,您可以使用 IOptionsMonitor<> 来动态切换返回的类型。
如果有帮助,您可以将工厂模式视为使用 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<> &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 容器,您可以
- 在
ConfigureServices 期间手动读取您可用的配置,
- 手动
Bind 到您的选项类型,
- 跳过/重新实现配置委托,
- 加载所需的程序集,然后
- 填充 DI 容器。
我没有这方面的例子。我的经验是自动注册方法就足够了。
编译
此代码是使用针对 netcoreapp2.1 的 .NET SDK 3.1.101 并使用“Microsoft.Extensions.Configuration.*”包的 2.1.1 版编写和测试的。我以GitHub Gist 的形式发布了一份完整的工作副本。
结束的想法
在FooFactory 和AddAutoRegisterTypes 中使用反射假设它们不被频繁调用。如果您只是在启动时使用这些来获得长期服务,那应该没问题。
AddAutoRegisterTypes 中的程序集搜索会随着程序变大而变慢。有一些方法可以加快速度,比如只检查具有已知命名模式的程序集。
虽然所有这些都有效,但我很想知道是否有更优雅的方式来做这样的事情。自动注册方案可能有点太神奇了,但我觉得配置方案复制了很多选项系统提供的内容。