We started working with ASP.NET Core almost immediately after the release. Autofac was chosen as the IoC container, as there is no implementation of the Windsor we are used to under Core (not).
Consider the various ways of registering dependencies that require configuration parameters, as well as the solution to which we have arrived and are now using.

Brief introductory
We register dependencies by modules and then register them via RegisterAssemblyModules. Everything is comfortable, everything is beautiful. But as always there is a “BUT”. This is convenient and perfectly smooth as long as our services do not require parameters from configuration files. It’s quite difficult to imagine a situation in which you don’t need to put your application settings into configuration files. At a minimum, you need to make connection lines in the configuration.
')
We collect IConfigurationRoot in the Startup class constructor and put it in the Configuration property. Accordingly, it can be further used in the ConfigureServices method. In general, the standard script.
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 make registration of such services into modules, but register them in ConfigureServicesPros:
- Most of the registrations are hidden in the modules and register in one line through RegisterAssemblyModules.
Minuses:
- You have to clutter up with ConfigureServices with registrations of other services that require parameters;
- Registration of such services in fact relate to specific modules, but are not located in them, which is not always trivial.
As a result, the work with the container looks like this:
ContainerBuilder builder = new ContainerBuilder(); builder.RegisterAssemblyModules(typeof(SomeModule).GetTypeInfo().Assembly); builder.RegisterType<SomeServiceWithParameter>() .As<ISomeServiceWithParameter>() .WithParameter("connectionString", Configuration.GetConnectionString("SomeConnectionString"));
2. Add the modules for which there are parameter-dependent services, properties for each parameter and register each module separatelyPros:
- All registrations are logically divided into modules and are where they should be.
Minuses:
- There may 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 modules);
- When a new module appears, you need to remember to add registration to ConfigureServices.
As a result, the work with the container looks like this:
ContainerBuilder builder = new ContainerBuilder(); builder.RegisterModule<SomeModule>(); // builder.RegisterModule(new SomeModuleWithParameters { ConnectionString = Configuration.GetConnectionString("SomeConnectionString") // }); // builder.Populate(services); Container = builder.Build();
With this approach, it is quite possible to manage with one property of the IConfigurationRoot type and transfer it into parameter-specific modules as a whole Configuration.
3. Register parameter-dependent services as a delegate (via the Register method) in which to resolve IConfigurationRoot and other dependencies necessary for such servicesPros:
- 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:
- Terrible registrations of parameter-dependent services, especially if other services must be injected into them;
- The registration of parameter-dependent services needs to be changed if the composition of their dependencies changes.
As a result, the 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, the registration of parameter-dependent services in modules looks like this:
builder.Register(componentContext => { IConfigurationRoot configuration = componentContext.Resolve<IConfigurationRoot>(); return new SomeServiceWithParameter( componentContext.Resolve<SomeOtherService>(), // configuration.GetConnectionString("SomeConnectionString")); }) .As<ISomeServiceWithParameter>();
4. Autofac configuration via JSON / XMLThis option was not even considered because of an obvious problem - we want to give the opportunity to change only certain parameters, but not the dependencies themselves.
In the end, which of the options is worse - a moot point. It was obvious only that none of them did not suit us.
What did we do
Added IConfiguredModule interface:
public interface IConfiguredModule { IConfigurationRoot Configuration { get; set; } }
Inherited the ConfiguredModule class from the Module and implemented the IConfiguredModule interface:
public abstract class ConfiguredModule : Module, IConfiguredModule { public IConfigurationRoot Configuration { get; set; } }
Added here such an extension for 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); } } }
These ~ 40 lines of code give us the opportunity to work with the container like this:
ContainerBuilder builder = new ContainerBuilder(); builder.RegisterConfiguredModulesFromAssemblyContaining<SomeModule>(Configuration); builder.Populate(services); Container = builder.Build();
If the module is parameter-independent, then we still inherit it from the Module - there are no changes.
public class SomeModule : Module { protected override void Load(ContainerBuilder builder) { builder.RegisterType<SomeService>().As<ISomeService>(); } }
If parameter-dependent, then we inherit it from ConfiguredModule and we can retrieve parameters through the Configuration property.
public class SomeConfiguredModule : ConfiguredModule { protected override void Load(ContainerBuilder builder) { builder.RegisterType<SomeServiceWithParameter>() .As<ISomeServiceWithParameter>() .WithParameter("connectionString", Configuration.GetConnectionString("SomeConnectionString")); } }
The very code for working with containers in ConfigureServices does not require any changes when changing the set of modules.
We hope that someone will be useful. We will be glad to any feedback.
UPD. Added a more concise solution from
mayorovp comments (only the use of the container was wrapped in using):
public static ContainerBuilder RegisterConfiguredModulesFromAssemblyContaining<TType>( 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<IModule>() .As<IModule>() .PropertiesAutowired(); using (IContainer metaContainer = metaBuilder.Build()) { foreach (IModule module in metaContainer.Resolve<IEnumerable<IModule>>()) builder.RegisterModule(module); } return builder; }