Search code examples
c#.netdependency-injectionautofac

Autofac: register parameters in a separate module to the registration of the type


I have a DataAccess class which takes a string as a parameter:

public class DataAccess : IDataAccess
{
    public DataAccess(string connectionString)...
}

public DataAccessModule : Module
{
     protected override void Load(ContainerBuilder builder) => builder.RegisterType<DataAccess>().As<IDataAccess>();
}

I publish that as a couple of nuget packages (one for the DataAccess class, one for the Module).

In my client I have ConfigReader class which reads a config, which is where I get the connection string from.

public DataAccessModule : Module
{
     protected override void Load(ContainerBuilder builder)
     {
         builder.RegisterType<ConfigReader>().As<IConfigReader>();
         builder.Register(c => c.Resolve<IConfigReader>().Read()).As<Config>();
     }
}

Now I need to pass the connection string as a parameter to the IDataAccess.

Note that I never resolve the IDataAccess - its a parameter of some other classes.

What I want to do is Register the parameter, but I can't see that autofac allows me to do that.

The following wont work either as it leads to infinite recursion:

builder.Register(c => {
   var config = c.Resolve<Config>();
   return c.Resolve<IDataAccess>(new Parameter(typeof(string), config.ConnectionString));
});

Solution

  • Before I get going, there are a couple of relevant docs you may want to check out in detail:

    It looks like you got partway there, but let's see what we can do.

    First, it doesn't matter that things are in different modules. Once it all ends up in the container it doesn't matter where it's coming from. That's sort of a red herring thrown into the question title.

    Given that, let's boil this down:

    // Original registrations, not the solution
    builder.RegisterType<ConfigReader>().As<IConfigReader>();
    builder.Register(c => c.Resolve<IConfigReader>().Read()).As<Config>();
    builder.Register(c => {
       var config = c.Resolve<Config>();
       return c.Resolve<IDataAccess>(new Parameter(typeof(string), config.ConnectionString));
    });
    

    Next thing I'd recommend is to always be explicit in delegate registrations. (That's actually one of the documented best practices.) It helps with the perf, but it also helps see things more clearly. Given that, let's update a little.

    // Being specific with the lambda registration.
    builder.RegisterType<ConfigReader>().As<IConfigReader>();
    builder.Register(c => c.Resolve<IConfigReader>().Read()).As<Config>();
    builder.Register(c => {
       var config = c.Resolve<Config>();
       return c.Resolve<IDataAccess>(new Parameter(typeof(string), config.ConnectionString));
    }).As<IDataAccess>();
    

    Now it's clear why the infinite recursion happens (this is where I got sort of stuck reading quickly) - you're trying to resolve the same thing you're registering (as you noted in the comments). So... you have to unwind that (as I mentioned in the comments).

    What you want is DataAccess to be the IDataAccess. So, somewhere in there you need to either register DataAccess as IDataAccess or otherwise tell Autofac how to create that.

    One way to do that is to just new it up in the lambda. That's how the FAQ shows it in the sample.

    // New-up DataAccess in the lambda.
    builder.RegisterType<ConfigReader>().As<IConfigReader>();
    builder.Register(c => c.Resolve<IConfigReader>().Read()).As<Config>();
    builder.Register(c => {
       var config = c.Resolve<Config>();
       return new DataAccess(config.ConnectionString);
    }).As<IDataAccess>();
    

    Now, you could get fancier with it, but I'm a big fan of keeping it simple. If this is all you need, call it good.

    But... let's say you're really, really trying to get all the parameters into Autofac registrations and there's a reason you don't want to new it up.

    Using ResolvedParameter can do it all without a lambda.

    // Resolved parameters all the way!
    builder.RegisterType<ConfigReader>().As<IConfigReader>();
    builder.Register(c => c.Resolve<IConfigReader>().Read()).As<Config>();
    builder
      .RegisterType<DataAccess>()
      .As<IDataAccess>()
      .WithParameter(
        new ResolvedParameter(
               (pi, ctx) => pi.ParameterType == typeof(string) && pi.Name == "connectionString",
               (pi, ctx) => ctx.Resolve<Config>().ConnectionString));
    

    The two docs I linked above have more examples and information. There are more ways to do this, but this is just a couple. I know the docs are a little long, but it would be worth taking the time to read them through. Good luck!