Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configuration on ServiceCollection being ignored when using Populate on ContainerBuilder in a nested lifetime scope #659

Closed
Jetski5822 opened this issue Jul 13, 2015 · 10 comments
Labels

Comments

@Jetski5822
Copy link

Using 4.0.0-alpha4-73

I have noticed that when calling Populate with a service container that contains some configuration, the configuration itself is being ignored in all future requests.

public class MvcModule : Module {
    protected override void Load(ContainerBuilder builder) {
        ServiceCollection serviceCollection = new ServiceCollection();
        serviceCollection.AddMvc();

        serviceCollection.Configure<RazorViewEngineOptions>(options => {
            var expander = new ModuleViewLocationExpander();
            options.ViewLocationExpanders.Add(expander);
        });

        builder.Populate(serviceCollection);
    }
}

Any ideas on how to fix? or am I calling Configure in the wrong way?

@alexmg
Copy link
Member

alexmg commented Jul 15, 2015

Try passing in the IServiceCollection instance provided to ConfigureServices to the module instead of creating a new collection.

https://github.com/autofac/Autofac/blob/master/samples/AutofacWebApiSample/Startup.cs

In the sample Startup.cs file above line 29 would be changed to the following.

builder.RegisterModule(new AutofacModule(services));

That would also allow you to pass the service collection to more than one module.

@Jetski5822
Copy link
Author

I am not sure that will fix the issue.

So I think the issue, resides with how Options Are transferred from the Service Collection to Autofac Container using the Autofac Registration Populate method.

I am registering an Autofac module within a lifetimescope (https://github.com/OrchardCMS/Brochard/blob/78dcedf7cf2019ebeceb16baaf2d3122701568ad/src/OrchardVNext/Environment/ShellBuilders/ShellContainerFactory.cs#L72) - the module it registers is called MvcModule

All dependencies get transferred to the Autofac Registration in the Populate Method including a IConfigurateOption .... however, when this is transferred across the options are lost.... i.e. the Action is not getting called, so the state is lost. The action in my above instance...

            var expander = new ModuleViewLocationExpander();
            options.ViewLocationExpanders.Add(expander);

What do you think?

@Jetski5822
Copy link
Author

I have a small test project here https://github.com/Jetski5822/AutofacSample/blob/master/Startup.cs which shows the issue.

D:\AutofacSample>dnx . web
Registering Module
Registering TestViewLocationExpander
1 Options

Registering Module
0 Options <---- thats the problem. Should be 1!
Started

@Jetski5822
Copy link
Author

Hey guys, any idea on how to fix this?

@tillig
Copy link
Member

tillig commented Nov 9, 2015

Looking at the repro posted by @Jetski5822 it appears the problem is specifically encountered when registering the options in a nested lifetime scope. The repro shows that it works correctly when registered directly into the root container and I've independently verified that.

@tillig
Copy link
Member

tillig commented Nov 9, 2015

I figured it out, and I don't think there's anything we can do to fix this.

The problem is in Microsoft.Extensions.OptionsModel, in OptionsServiceCollectionExtensions.cs line 23, in the AddOptions extension method.

When you use the collection.Configure<TOptions>(Action<TOptions>) extension methods, part of what is assumed to be there is a call to collection.AddOptions().

That call is done for you indirectly by the call to collection.AddMvc() which is why you don't see it in your own code.

When AddOptions is run, it executes this on the service collection:

services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(OptionsManager<>)));

The important part there is that it's adding a singleton registration. Singletons get resolved - always - from the root container, not from a child lifetime scope.

When you (or one of the framework components, like the razor view engine) ask for an IOptions<TOptions> (like an IOptions<RazorViewEngineOptions>) what you're actually getting is an OptionsManager<TOptions> (OptionsManager<RazorViewEngineOptions>).

OptionsManager<TOptions> takes in an IEnumerable<IConfigureOptions<TOptions>>. When you call collection.Configure<TOptions>(Action<TOptions>), under the covers it registers a ConfigureOptions<TOptions> with your configuration action. Resolving OptionsManager<TOptions> brings all of the configuration actions together so they can execute.

When you call the Value property on OptionsManager<TOptions> that's when the configuration action actually executes.

The singleton registration means that the OptionsManager<TOptions> is going to try to locate those configuration actions from the root container, not from the child lifetime scope. It is, after all, a singleton so it can't depend on child scope dependencies. (We have a walkthrough of lifetime scope here.)

It works when you register everything in the root container rather than a nested child lifetime scope as evidenced by the @Jetski5822 demo project and our own unit tests.

Basically, since the configuration actions aren't registered in the root container... you're not going to get what you expect.

There are two ways to work with this:

  • You always need to register your configuration actions in the root container, OR
  • You should file an issue with the aspnet/Options repo and ask them to remove the singleton registration lifetime in AddOptions.

In any case, it's not something Autofac core can actually do anything about. The resolution of singleton dependencies from the root lifetime scope is correct and desired behavior.

@tillig tillig closed this as completed Nov 9, 2015
@tillig tillig changed the title Configuration on ServiceCollection being ignored when using Populate on ContainerBuilder Configuration on ServiceCollection being ignored when using Populate on ContainerBuilder in a nested lifetime scope Nov 9, 2015
@ckalan
Copy link

ckalan commented Sep 14, 2016

@Jetski5822 , I could manage the same thing you are after as follows:

Autofac Module

public class MyModule: Module
    {
        protected override void Load(ContainerBuilder builder)
        {

            builder.RegisterType<PluginsViewLocationExpander>().As<IViewLocationExpander>().InstancePerLifetimeScope();


            builder.Register(ctx => new ConfigureOptions<RazorViewEngineOptions>(options =>
            {
                ctx.Resolve<IEnumerable<IViewLocationExpander>>()
                       .ToList().ForEach(expander => options.ViewLocationExpanders.Add(expander));
            })).As<IConfigureOptions<RazorViewEngineOptions>>().InstancePerLifetimeScope();

            builder.RegisterType<OptionsManager<RazorViewEngineOptions>>().InstancePerLifetimeScope();

        }
    }

Test with dependencies:

"Autofac" :"4.1.0"
"Microsoft.AspNetCore.Mvc": "1.0.0",

Please not that, I don't use services.configure in my Startup.cs and I haven't tested yet but, you must still be able to add additional options with that since OptionsManager requires a IEnumerable<IConfigureOptions>

@PawelGerr
Copy link

@tillig
Copy link
Member

tillig commented Jun 16, 2017

Reopening this issue so we have something to attach to the PR.

@tillig
Copy link
Member

tillig commented Jul 19, 2017

Pulled #14. This will go out shortly as 4.2.0. Thanks for all the help!

@tillig tillig closed this as completed Jul 19, 2017
@ghost ghost removed the in progress label Jul 19, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants