Передача параметров конфигураций в модули Autofac-а в ASP.NET Core

от автора

Мы начали работать с ASP.NET Core практически сразу после релиза. В качестве IoC-контейнера выбрали Autofac, так как реализации привычного нам Windsor под Core нет (не было).

Рассмотрим различные способы регистраций зависимостей, требующих параметров конфигураций, а также решение, к которому пришли и которым теперь пользуемся.

image

Краткая вводная

Регистрации зависимостей мы разносим по модулям и затем регистрируем их через 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *