📜 ⬆️ ⬇️

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, 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.

image

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 ConfigureServices

Pros:


Minuses:


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")); //      builder.Populate(services); Container = builder.Build(); 

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

Pros:


Minuses:


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 services

Pros:


Minuses:


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 / XML

This 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; } 

Source: https://habr.com/ru/post/325194/


All Articles