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

RegistrationAccessor passed into IRegistrationSource does not account for registrations in child lifetime scope #1028

Closed
darren-clark opened this issue Sep 19, 2019 · 2 comments

Comments

@darren-clark
Copy link

darren-clark commented Sep 19, 2019

When RegistrationsFor is invoked due to resolving inside a child lifetime scope, that has scoped registrations, those registrations are not available through the passed in registrationAccessor parameter.

This is particularly problematic for sources such as ACTNARS that return a registration if there is not already one. In this case this results in multiple registrations for an already registered service.

Probably related to #218 and other lifetime scope related bugs.

Minimal repro:

    class Program
    {
        public interface IDependency
        {
        }

        public class Concrete
        {
            public Concrete(IDependency dependency)
            {
            }
        }

        public class Dependency: IDependency
        {
        }

        static void Main(string[] args)
        {
            var builder = new ContainerBuilder();
            builder.RegisterSource(new AnyConcreteTypeNotAlreadyRegisteredSource());
            using (var container = builder.Build())
            {
                using (var scope = container.BeginLifetimeScope(cfg => cfg.RegisterInstance(new Concrete(new Dependency()))))
                {
                    try
                    {
                        var concrete = scope.Resolve<IEnumerable<Concrete>>();
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine("Unexpected exception");
                    }
                }
            }
        }
    }
@tillig
Copy link
Member

tillig commented Nov 26, 2019

In the future it might help to see what's actually failing and where, like an exception with a call stack.

Here's an update to the minimal repro that builds without warnings:

using System;
using System.Collections.Generic;
using Autofac;
using Autofac.Features.ResolveAnything;

namespace autofacdemo
{
    class Program
    {
        public interface IDependency
        {
        }

        public class Concrete
        {
            public Concrete(IDependency dependency)
            {
            }
        }

        public class Dependency: IDependency
        {
        }

        static void Main(string[] args)
        {
            var builder = new ContainerBuilder();
            // Removing ACTNARS fixes things.
            builder.RegisterSource(new AnyConcreteTypeNotAlreadyRegisteredSource());
            using (var container = builder.Build())
            {
                // Removing the lifetime scope registration does not change the exception.
                using (var scope = container.BeginLifetimeScope(cfg => cfg.RegisterInstance(new Concrete(new Dependency()))))
                {
                    try
                    {
                        var concrete = scope.Resolve<IEnumerable<Concrete>>();
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine("Unexpected exception {0}", e);
                    }
                }
            }
        }
    }
}

When this runs, the exception is:

Unexpected exception Autofac.Core.DependencyResolutionException: An exception was thrown while activating λ:autofacdemo.Program+Concrete[] -> λ:autofacdemo.Program+Concrete[] -> λ:autofacdemo.Program+Concrete -> autofacdemo.Program+Concrete.
 ---> Autofac.Core.DependencyResolutionException: None of the constructors found with 'Autofac.Core.Activators.Reflection.DefaultConstructorFinder' on type 'autofacdemo.Program+Concrete' can be invoked with the available services and parameters:
Cannot resolve parameter 'IDependency dependency' of constructor 'Void .ctor(IDependency)'.
   at Autofac.Core.Activators.Reflection.ReflectionActivator.GetValidConstructorBindings(IComponentContext context, IEnumerable`1 parameters)
   at Autofac.Core.Activators.Reflection.ReflectionActivator.ActivateInstance(IComponentContext context, IEnumerable`1 parameters)
   at Autofac.Core.Resolving.InstanceLookup.Activate(IEnumerable`1 parameters, Object& decoratorTarget)
   --- End of inner exception stack trace ---
   at Autofac.Core.Resolving.InstanceLookup.Activate(IEnumerable`1 parameters, Object& decoratorTarget)
   at Autofac.Core.Resolving.InstanceLookup.Execute()
   at Autofac.Core.Resolving.ResolveOperation.GetOrCreateInstance(ISharingLifetimeScope currentOperationScope, IComponentRegistration registration, IEnumerable`1 parameters)
   at Autofac.Core.Resolving.ResolveOperation.ResolveComponent(IComponentRegistration registration, IEnumerable`1 parameters)
   at Autofac.Core.Resolving.ResolveOperation.Execute(IComponentRegistration registration, IEnumerable`1 parameters)      at Autofac.Core.Lifetime.LifetimeScope.ResolveComponent(IComponentRegistration registration, IEnumerable`1 parameters)
   at Autofac.ResolutionExtensions.TryResolveService(IComponentContext context, Service service, IEnumerable`1 parameters, Object& instance)
   at Autofac.ResolutionExtensions.ResolveService(IComponentContext context, Service service, IEnumerable`1 parameters)   at Autofac.ResolutionExtensions.Resolve(IComponentContext context, Type serviceType, IEnumerable`1 parameters)         at Autofac.ResolutionExtensions.Resolve[TService](IComponentContext context, IEnumerable`1 parameters)
   at Autofac.ResolutionExtensions.Resolve[TService](IComponentContext context)
   at autofacdemo.Program.Main(String[] args) in C:\dev\autofacdemo\Program.cs:line 35

I find there are a couple of interesting things here, which I noted in the updated repro:

  • If you take ACTNARS out of the system, this example works great.
  • If you take the lifetime scope registrations out, it doesn't affect things - ACTNARS is really where this is falling down.

ACTNARS is a tricky beast. There are a couple of things at play:

First, the Concrete class is... concrete. But it takes an abstraction as a dependency, rather than an actual concrete class. ACTNARS alone won't be able to resolve it. It's sort of like if you did this:

var builder = new ContainerBuilder();
builder.RegisterType<Concrete>();
builder.RegisterType<Dependency>();
var container = builder.Build();
// This will FAIL because there's no `IDependency` registered.
container.Resolve<Concrete>();

Second, there's a false sense of security in using ACTNARS. It's not "try to resolve things and it's OK if you can't." It's more like... "If it's a concrete type, pretend I did builder.RegisterType<T> on it and behave accordingly."

You can totally register two identical things.

var builder = new ContainerBuilder();
builder.RegisterType<Dependency>();
builder.RegisterType<Dependency>();
var container = builder.Build();
// There will be two things in this collection.
container.Resolve<IEnumerable<Dependency>>();

I understand the larger issue here is that this demonstrates a problem with how ACTNARS (and possibly other registration sources) detect "what's registered" vs. "what's not." I'm not entirely sure how to solve that.

Just brainstorming about different factors...

  • If ACTNARS is registered in a parent scope, it appears on quick skim that it only gets to search registrations in that scope rather than in the child scope.
  • Would there be a race condition if ACTNARS was registered twice? What do we do about multiple modules that all query RegistrationsFor? Is it an ordering issue at that point, where the first registered would find no registrations but later ones would?
  • How does lifetime scope affect this in deeper ways? Would we accidentally start creating captive dependencies?
  • Is it expected that ACTNARS would change its behavior (provide a registration sometimes but not in other times) based on child lifetime scope registrations or is that going to be equally confusing to a different set of folks?

Likely what needs to happen is that the RegistrationsFor call needs to be tied - somehow - to the component registry of the scope doing the resolution rather than the scope in which the source is registered.

I'm not sure how to adjust for that off the top of my head. It may be part of the new ResolveRequest or something that we need to add. Unclear.

Definitely a bug, at least from the standpoint that it's doing something unexpected.

@tillig
Copy link
Member

tillig commented Nov 6, 2023

This issue has been open for quite some time without any traction. Much as we understand the challenges presented here, we also get very few substantial contributions and we don't have time to really dig into non-trivial issues like this. As such, we're closing this issue. If someone feels like they really want this, we would love to see a PR where this gets addressed.

@tillig tillig closed this as completed Nov 6, 2023
@tillig tillig added the wontfix label Nov 6, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants