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

в 15:31, , рубрики: .net, .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 не требует никаких изменений при изменении набора модулей.

Надеемся, что кому-то будет полезным. Будем рады любому фидбэку.

Автор: Рожков Константин

Источник

Поделиться

* - обязательные к заполнению поля