Мы начали работать с 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 не требует никаких изменений при изменении набора модулей.
Надеемся, что кому-то будет полезным. Будем рады любому фидбэку.
ссылка на оригинал статьи https://habrahabr.ru/post/325194/
Добавить комментарий