Search code examples
asp.net-coreautofac

Autofac cannot resolve ASP. NET Core Controller when trying to pass parameters to register


We have been using Autofac as the IoC Container for dependency injection. In order to pass parameters to Web API controllers during the registration, which we were hoping to be passed to the Controller constructor. It is a double typed parameter with the name ratio in the constructor below.

However, Autofac throws an exception because it cannot resolve the last parameter with the type of double (ratio here) and cannot create an instance of DataProcessController.

public DataProcessController(ILogger logger, IDataProvider dataProvider, double ratio)
{
  ...
}

by using WithParameter method in order to specify the value to use.

builder.RegisterType<DataProcessController>().AsSelf()
            .WithParameter("ratio", 0.4);

Also I call AddControllersAsServices in order to register the controller as a service and resolve it from the container.

services.AddMvc()
        .AddControllersAsServices()
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

and call Populate() after finishing registrations.

builder.Populate(services);
return new AutofacServiceProvider(builder.Build());

All defined in the function below:

    public IServiceProvider ConfigureServices(IServiceCollection services)
    {
        services.AddOptions();
        services.AddHttpClient();
        services.AddMvc()
            .AddControllersAsServices()
            .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

        //Autofac
        var builder = new ContainerBuilder();
        builder.RegisterInstance(_logger).As<ILogger>().SingleInstance();

        //All other dependencies are defined here....

        builder.RegisterType<DataProcessController>().AsSelf()
            .WithParameter("ratio", 0.4);

        // auto-discover examples from this assembly
        services.AddSwaggerExamplesFromAssemblyOf<Startup>();

        builder.Populate(services);
        return new AutofacServiceProvider(builder.Build());
    }

I tried to pass both primitive data types and reference data types like another class as a container to hold the double values, but all failed to resolve the controller by Autofac.


Solution

  • You need to call builder.Populate(services) BEFORE you add things to the container to override.

    If you look at the docs on how the new IServiceProviderFactory stuff works, it's basically:

    • Use ConfigureServices to register things with IServiceCollection
    • Behind the scenes in the service provider factory, a ContainerBuilder is created and calls builder.Populate(services) for you
    • Use ConfigureContainer to register things with Autofac directly
    • Behind the scenes in the service provider factory the final container is built and put in an IServiceProvider

    In the older integration where you build the AutofacServiceProvider yourself, you have to be careful about order. It's still Autofac - last in wins.

    When you do services.AddControllersAsServices() that is like saying builder.RegisterType<DataProcessController>().AsSelf()... but it's not actually talking to the builder yet. It's like a "stored registration" that won't "play back" until you call Populate.

    Thus, if you simplify your registrations and put things in the order they actually execute:

    // These will NOT be executed until builder.Populate()
    // services.AddOptions();
    // services.AddHttpClient();
    // services.AddMvc()
    //    .AddControllersAsServices()
    //    .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    
    var builder = new ContainerBuilder();
    builder.RegisterInstance(_logger).As<ILogger>().SingleInstance();
    builder.RegisterType<DataProcessController>().AsSelf()
        .WithParameter("ratio", 0.4);
    
    // Still not executed until builder.Populate()
    services.AddSwaggerExamplesFromAssemblyOf<Startup>();
    
    // Here goes everything!
    // - AddOptions => ContainerBuilder
    // - AddHttpClient => ContainerBuilder
    // - AddMvc => ContainerBuilder
    // - AddControllersAsServices => ContainerBuilder
    // ---- OH NO! JUST OVERWROTE THE WITHPARAMETER REGISTRATION!
    // - AddSwaggerExamplesFromAssemblyOf => ContainerBuilder
    builder.Populate(services);
    
    return new AutofacServiceProvider(builder.Build());
    

    Here's what I'd recommend:

    BEST CASE: MOVE TO THE SERVICE PROVIDER FACTORY SUPPORT

    In the documentation, this is the one where in ASP.NET 1.1-2.2 you don't build the AutofacServiceProvider yourself and, instead, register Autofac at the web host level. This is the best option because it prepares you more for upgrade to ASP.NET Core 3.0 when you're ready. Once you hit 3.0 you'll have to do this anyway.

    FALLBACK OPTION: ORGANIZE YOUR REGISTRATIONS

    If you can't switch the integration to force you to use separate ConfigureServices and ConfigureContainer methods, at least organize the code in ConfigureServices so you're ready. This will also ensure everything gets called in the right order to avoid this issue now and in the future.

    • Group all of the services.AddWhatever() registrations for IServiceCollection at the top. For example, don't let services.AddSwaggerExamplesFromAssembly linger down among Autofac registrations.
    • Create the ContainerBuilder and immediately call Populate. Do not put any registrations between those two things.
    • Group all of the builder.Register<T>() registrations with ContainerBuilder after the builder.Populate call.
    • Leave the creation of the service provider as the absolute last thing in the method like you have now.