Выбор конкретной реализации на основе параметров
Вы можете выбрать конкретную реализацию интерфейса на основе типа параметров, используя перегрузку 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 загрузкой сборки, вы можете
- вручную читать конфигурацию, доступную вам во время
ConfigureServices
, - вручную
Bind
для вашего типа опции, - пропускать / переопределять делегатов конфигурации,
- загрузить необходимые сборки и
- заполнить контейнер DI.
У меня нет примера для этого. Мой опыт показывает, что достаточно автоматической регистрации c.
Компиляция
Этот код был написан и протестирован с использованием NET SDK 3.1.101 для netcoreapp2.1
и с использованием версии 2.1.1 пакетов «Microsoft.Extensions.Configuration. *». Я опубликовал полную рабочую копию в виде GitHub Gist .
Заключительные мысли
Использование отражения в FooFactory
и AddAutoRegisterTypes
предполагает, что это не звонил часто. Если вы просто используете их один раз при запуске для долгоживущего сервиса, все должно быть в порядке.
Поиск сборки в AddAutoRegisterTypes
может замедляться по мере увеличения размера программы. Есть несколько способов ускорить его, например, проверять сборки с известным шаблоном имен.
Хотя все это работает, я хотел бы знать, есть ли более элегантный способ сделать что-то подобное. Схема автоматической регистрации может быть слишком волшебной, но я считаю, что схема конфигурации дублирует многое из того, что предоставляет система опций.