Back to Home

Passing configuration parameters to Autofac modules in ASP.NET Core

.net core

Passing configuration parameters to Autofac modules in ASP.NET Core

    We started working with ASP.NET Core almost immediately after the release. Autofac was chosen as the IoC container, since there is no implementation of the familiar Windsor for Core (it was not).

    We will consider various ways of registering dependencies that require configuration parameters, as well as the solution we have arrived at and are now using.

    image

    Brief Introductory


    We distribute the registration of dependencies into modules and then register them through RegisterAssemblyModules. Everything is comfortable, everything is fine. But as always there is a “BUT”. It is convenient and perfectly smooth until our services require parameters from configuration files. It is difficult to imagine a situation in which it is not necessary to transfer the settings of your application to the configuration files. At a minimum, you need to make connection strings in the configuration.

    We collect IConfigurationRoot in the constructor of the Startup class and put it in the Configuration property. Accordingly, further it can be used in the ConfigureServices method. In general, a standard scenario.

    public Startup(IHostingEnvironment env)
    {
    	IConfigurationBuilder builder = new ConfigurationBuilder()
            ...
    	...
    	Configuration = builder.Build();
    }
    public IConfigurationRoot Configuration { get; }
    

    How can I solve the problem with services that require configuration settings?


    1. Do not register such services in modules, but register them in ConfigureServices

    Pros:

    • We hide most of the registrations in modules and register in one line through RegisterAssemblyModules.

    Minuses:

    • You have to clutter up ConfigureServices with registrations of other services that require parameters;
    • Registration of such services in fact refers to specific modules, but are not located in them, which is not always trivial.

    As a result, work with the container looks like this:

    ContainerBuilder builder = new ContainerBuilder();
    builder.RegisterAssemblyModules(typeof(SomeModule).GetTypeInfo().Assembly);
    builder.RegisterType()
    	.As()
    	.WithParameter("connectionString", Configuration.GetConnectionString("SomeConnectionString"));
    // Множество других регистраций параметрозависимых сервисов 
    builder.Populate(services);
    Container = builder.Build();
    

    2. Add to the modules, in which there are parameter-dependent services, properties for each parameter and register each module separately.

    Pluses:

    • All registrations are logically divided into modules and lie where they should.

    Minuses:

    • There can be many registrations of modules and all this simply clutters up ConfigureServices (especially if a large number of parameters are required to be transferred to the modules);
    • When a new module appears, you must remember to add registration to ConfigureServices.

    As a result, work with the container looks like this:

    ContainerBuilder builder = new ContainerBuilder();
    builder.RegisterModule();
    // Другие регистрации параметронезависимых модулей
    builder.RegisterModule(new SomeModuleWithParameters
    {
    	ConnectionString = Configuration.GetConnectionString("SomeConnectionString")
    	// Другие параметры
    });
    // Другие регистрации параметрозависимых модулей
    builder.Populate(services);
    Container = builder.Build();
    

    With this approach, it is quite possible to get by with one property of the IConfigurationRoot type and pass the entire Configuration to parameter-dependent modules.

    3. Register parameter-dependent services as a delegate (through the Register method), in which to resolve IConfigurationRoot and other dependencies necessary for such services.

    Pluses:

    • All registrations are logically divided into modules and lie where they should;
    • Working with the container in ConfigureServices looks clean and does not require changes when new modules appear.

    Minuses:

    • Horrible registrations of parameter-dependent services, especially if other services should be injected into them;
    • Registration of parameter-dependent services needs to be changed if the composition of their dependencies changes.

    As a result, work with the container looks like this:

    // Не забываем зарегистрировать IConfigurationRoot
    services.AddSingleton(Configuration);
    ContainerBuilder builder = new ContainerBuilder();
    builder.RegisterAssemblyModules(typeof(SomeModule).GetTypeInfo().Assembly);
    builder.Populate(services);
    Container = builder.Build();
    

    But at the same time, registration of parameter-dependent services in the modules looks like this:

    builder.Register(componentContext =>
    	{
    		IConfigurationRoot configuration = componentContext.Resolve();
    		return new SomeServiceWithParameter(
    			componentContext.Resolve(), 
    			// Резолвим другие зависимости
    			configuration.GetConnectionString("SomeConnectionString"));
    	})
    	.As();
    

    4. Autofac configuration via JSON / XML

    This option was not even considered due to an obvious problem - we want to give the opportunity to change only certain parameters, but not the dependency registrations themselves.

    In the end, which option is worse is a moot point. Obviously, only one of them did not suit us.

    What have we done


    Added IConfiguredModule interface:

    public interface IConfiguredModule
    {
    	IConfigurationRoot Configuration { get; set; }
    }
    

    We inherited the ConfiguredModule class from Module and implemented the IConfiguredModule interface:

    public abstract class ConfiguredModule : Module, IConfiguredModule
    {
    	public IConfigurationRoot Configuration { get; set; }
    }
    

    We added this extension for ContainerBuilder:

    public static class ConfiguredModuleRegistrationExtensions
    {
    	// В generic-параметр TType передается тип, находящийся в сборке, в которой мы будем искать IModule-и
    	// + Передаем IConfigurationRoot, которым мы будем означивать Configuration в ConfiguredModule-х
    	public static void RegisterConfiguredModulesFromAssemblyContaining(
    		this ContainerBuilder builder, 
    		IConfigurationRoot configuration)
    	{
    		if (builder == null)
    			throw new ArgumentNullException(nameof(builder));
    		if (configuration == null)
    			throw new ArgumentNullException(nameof(configuration));
    		// Извлекаем из сборки, в которой лежит TType, все типы, реализующие IModule
    		IEnumerable moduleTypes = typeof(TType)
    			.GetTypeInfo()
    			.Assembly.DefinedTypes
    			.Select(x => x.AsType())
    			.Where(x => x.IsAssignableTo());
    		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);
    		}
    	}
    }
    

    These ~ 40 lines of code give us the opportunity to work with the container like this:

    ContainerBuilder builder = new ContainerBuilder();
    builder.RegisterConfiguredModulesFromAssemblyContaining(Configuration);
    builder.Populate(services);
    Container = builder.Build();
    

    If the module is parameter-independent, then we, as before, inherit it from Module - there are no changes.

    public class SomeModule : Module
    {
    	protected override void Load(ContainerBuilder builder)
    	{
    		builder.RegisterType().As();
    	}
    }
    

    If it is parameter-dependent, then we inherit it from ConfiguredModule and can retrieve the parameters through the Configuration property.

    public class SomeConfiguredModule : ConfiguredModule
    {
    	protected override void Load(ContainerBuilder builder)
    	{
    		builder.RegisterType()
    			.As()
    			.WithParameter("connectionString", Configuration.GetConnectionString("SomeConnectionString"));
    	}
    }
    

    The container work code itself in ConfigureServices does not require any changes when changing the set of modules.

    We hope that someone will be useful. We are looking forward to any feedback.

    UPD Added a more concise solution from the comments from mayorovp (only the use of the container was wrapped in using):

    public static ContainerBuilder RegisterConfiguredModulesFromAssemblyContaining(
    	this ContainerBuilder builder, 
    	IConfigurationRoot configuration)
    {
    	if (builder == null)
    		throw new ArgumentNullException(nameof(builder));
    	if (configuration == null)
    		throw new ArgumentNullException(nameof(configuration));
    	var metaBuilder = new ContainerBuilder();
    	metaBuilder.RegisterInstance(configuration);
    	metaBuilder.RegisterAssemblyTypes(typeof(TType).GetTypeInfo().Assembly)
    		.AssignableTo()
    		.As()
    		.PropertiesAutowired();
    	using (IContainer metaContainer = metaBuilder.Build())
    	{
    		foreach (IModule module in metaContainer.Resolve>())
    			builder.RegisterModule(module);
    	}
    	return builder;
    }
    

    Read Next