Мы начали работать с ASP.NET Core практически сразу после релиза. В качестве IoC-контейнера выбрали Autofac, так как реализации привычного нам Windsor под Core нет (не было).
Рассмотрим различные способы регистраций зависимостей, требующих параметров конфигураций, а также решение, к которому пришли и которым теперь пользуемся.
Краткая вводная
Регистрации зависимостей мы разносим по модулям и затем регистрируем их через RegisterAssemblyModules. Все удобно, все прекрасно. Но как всегда есть «НО». Это удобно и прекрасно ровно до тех пор, пока наши сервисы не требуют параметров из файлов конфигураций. Ситуацию, в которой не требуется выносить настройки вашего приложения в файлы конфигураций, представить достаточно сложно. Как минимум требуется вынести в конфигурации строки подключений.
Мы собираем IConfigurationRoot в конструкторе Startup-класса и кладем его в свойство Configuration. Соответственно, дальше его можно использовать в методе ConfigureServices. В общем, стандартный сценарий.
public Startup(IHostingEnvironment env)
{
IConfigurationBuilder builder = new ConfigurationBuilder()
...
...
Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; }
Как можно решить проблему с сервисами, требующими параметры конфигурации
1. Не выносить регистрации таких сервисов в модули, а регистрировать их в ConfigureServices
Плюсы:
- Большую часть регистраций прячем в модули и регистрируем в одну строчку через RegisterAssemblyModules.
Минусы:
- Приходится загромождать ConfigureServices регистрациями остальных сервисов, требующих параметры;
- Регистрации таких сервисов по факту относятся к конкретным модулям, но расположены не в них, что не всегда тривиально.
В итоге работа с контейнером выглядит так:
ContainerBuilder builder = new ContainerBuilder();
builder.RegisterAssemblyModules(typeof(SomeModule).GetTypeInfo().Assembly);
builder.RegisterType<SomeServiceWithParameter>()
.As<ISomeServiceWithParameter>()
.WithParameter("connectionString", Configuration.GetConnectionString("SomeConnectionString"));
// Множество других регистраций параметрозависимых сервисов
builder.Populate(services);
Container = builder.Build();
2. Добавить в модули, в которых есть параметрозависимые сервисы, свойства для каждого параметра и регистрировать каждый модуль по-отдельности
Плюсы:
- Все регистрации логически разбиты по модулям и лежат там, где и должны.
Минусы:
- Регистраций модулей может быть много и все это просто загромождает ConfigureServices (особенно если в модули требуется передать большое количество параметров);
- При появлении нового модуля нужно не забывать добавлять регистрацию в ConfigureServices.
В итоге работа с контейнером выглядит так:
ContainerBuilder builder = new ContainerBuilder();
builder.RegisterModule<SomeModule>();
// Другие регистрации параметронезависимых модулей
builder.RegisterModule(new SomeModuleWithParameters
{
ConnectionString = Configuration.GetConnectionString("SomeConnectionString")
// Другие параметры
});
// Другие регистрации параметрозависимых модулей
builder.Populate(services);
Container = builder.Build();
При таком подходе вполне можно обойтись и одним свойством типа IConfigurationRoot и передавать в параметрозависимые модули целиком Configuration.
3. Регистрировать параметрозависимые сервисы как делегат (через метод Register), в котором резолвить IConfigurationRoot и остальные необходимые для таких сервисов зависимости
Плюсы:
- Все регистрации логически разбиты по модулям и лежат там, где и должны;
- Работа с контейнером в ConfigureServices выглядит чисто и не требует изменений при появлении новых модулей.
Минусы:
- Ужасные регистрации параметрозависимых сервисов, особенно если в них должны инъектится другие сервисы;
- Регистрации параметрозависимых сервисов нужно менять, если меняется состав их зависисмостей.
В итоге работа с контейнером выглядит так:
// Не забываем зарегистрировать IConfigurationRoot
services.AddSingleton(Configuration);
ContainerBuilder builder = new ContainerBuilder();
builder.RegisterAssemblyModules(typeof(SomeModule).GetTypeInfo().Assembly);
builder.Populate(services);
Container = builder.Build();
Но при этом регистрации параметрозависимых сервисов в модулях выглядят вот так:
builder.Register(componentContext =>
{
IConfigurationRoot configuration = componentContext.Resolve<IConfigurationRoot>();
return new SomeServiceWithParameter(
componentContext.Resolve<SomeOtherService>(),
// Резолвим другие зависимости
configuration.GetConnectionString("SomeConnectionString"));
})
.As<ISomeServiceWithParameter>();
4. Конфигурация Autofac через JSON/XML
Данный вариант даже не рассматривали из-за очевидной проблемы — мы хотим дать возможность изменять только определенные параметры, но никак не сами регистрации зависимостей.
В итоге, какой из вариантов хуже — вопрос спорный. Очевидно было только то, что ни один из них нас не устраивал.
Что сделали мы
Добавили интерфейс IConfiguredModule:
public interface IConfiguredModule
{
IConfigurationRoot Configuration { get; set; }
}
Отнаследовали класс ConfiguredModule от Module и реализовали интерфейс IConfiguredModule:
public abstract class ConfiguredModule : Module, IConfiguredModule
{
public IConfigurationRoot Configuration { get; set; }
}
Добавили вот такой extension для ContainerBuilder:
public static class ConfiguredModuleRegistrationExtensions
{
// В generic-параметр TType передается тип, находящийся в сборке, в которой мы будем искать IModule-и
// + Передаем IConfigurationRoot, которым мы будем означивать Configuration в ConfiguredModule-х
public static void RegisterConfiguredModulesFromAssemblyContaining<TType>(
this ContainerBuilder builder,
IConfigurationRoot configuration)
{
if (builder == null)
throw new ArgumentNullException(nameof(builder));
if (configuration == null)
throw new ArgumentNullException(nameof(configuration));
// Извлекаем из сборки, в которой лежит TType, все типы, реализующие IModule
IEnumerable<Type> moduleTypes = typeof(TType)
.GetTypeInfo()
.Assembly.DefinedTypes
.Select(x => x.AsType())
.Where(x => x.IsAssignableTo<IModule>());
foreach (Type moduleType in moduleTypes)
{
// Создаем модуль нужного типа
var module = Activator.CreateInstance(moduleType) as IModule;
// Если модуль реализует IConfiguredModule, то означиваем его свойство Configuration переданным в метод IConfigurationRoot
var configuredModule = module as IConfiguredModule;
if (configuredModule != null)
configuredModule.Configuration = configuration;
// Ну и просто регистрируем созданный модуль
builder.RegisterModule(module);
}
}
}
Эти ~40 строк кода дают нам возможность работать с контейнером вот так:
ContainerBuilder builder = new ContainerBuilder();
builder.RegisterConfiguredModulesFromAssemblyContaining<SomeModule>(Configuration);
builder.Populate(services);
Container = builder.Build();
Если модуль параметронезависимый, то мы как и раньше наследуем его от Module — тут никаких изменений.
public class SomeModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<SomeService>().As<ISomeService>();
}
}
Если же параметрозависимый, то наследуем его от ConfiguredModule и можем извлекать параметры через свойство Configuration.
public class SomeConfiguredModule : ConfiguredModule
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<SomeServiceWithParameter>()
.As<ISomeServiceWithParameter>()
.WithParameter("connectionString", Configuration.GetConnectionString("SomeConnectionString"));
}
}
Сам же код работы с контейнером в ConfigureServices не требует никаких изменений при изменении набора модулей.
Надеемся, что кому-то будет полезным. Будем рады любому фидбэку.
Автор: Рожков Константин