Search code examples
c#.netdependency-injectionautofacioc-container

Autofac registers components multiple times


In a previous question about how I visualize the graph of my dependencies I got the foundation for the code I now use to visualize my dependency graph as it is resolved by Autofac.

Running the code I get a tree that results in code like the following.

Usd.EA.Bogfoering.WebApi.Controllers.BogfoerController (3851,7 ms. / 0,0 ms.) Depth: 0
   Usd.EA.Bogfoering.WebApi.Controllers.BogfoerController (3851,7 ms. / 0,4 ms.) Depth: 1
      Usd.Utilities.WebApi.Controllers.UnikOwinContext (0,1 ms. / 0,0 ms.) Depth: 2
         Usd.Utilities.WebApi.Controllers.UnikOwinContext (0,1 ms. / 0,0 ms.) Depth: 3

In the start I thought there was a problem with the code, and that it for some reason resulted in the components getting resolved multiple times. As Steven points out, this could happen when a component is registered as InstancePerDependency. But as several of my components are registered as InstancePerLifetime or SingleInstance dependencies, those dependencies shouldn't be resolved twice in the graph.

Steven does mention that "the first resolve of the InstancePerDependency dependency seems to have more dependencies than the next resolve, because this graph only shows resolves. Perhaps this is what's going on." But as I'm seeing InstancePerLifetime components being registered multiple times, on several occasions throughout the graph, I have the feeling that there's something else going on here.

What could be going on here?

How the dependencies are registered

The following code is the one we use to register our assemblies:

public static void RegisterAssemblies(this ContainerBuilder containerBuilder, IList<Assembly> assemblies, params Type[] typesToExclude)
{
  if (containerBuilder != null && assemblies.Any())
  {
    var allTypes = assemblies.SelectMany(assembly => assembly.GetTypes()).Where(t => !typesToExclude.Any(t2 => t2.IsAssignableFrom(t))).ToList();
    RegisterAllClassesWithoutAttribute(containerBuilder, allTypes);

    RegisterClassesThatAreSingleton(containerBuilder, allTypes);

    RegisterClassesThatAreInstancePerLifetimeScope(containerBuilder, allTypes);

    RegisterGenericInterfaces(containerBuilder, allTypes);

    RegisterRealOrTestImplementations(containerBuilder, allTypes);

    RegisterAutofacModules(containerBuilder, allTypes);

    containerBuilder.Register(c => UnikCallContextProvider.CurrentContext).As<IUnikCallContext>();
  }
}

private static void RegisterAutofacModules(ContainerBuilder containerBuilder, List<Type> allTypes)
{
  var modules = allTypes.Where(type => typeof(IModule).IsAssignableFrom(type) && type.GetCustomAttribute<DoNotRegisterInIocAttribute>() == null);
  foreach (var module in modules)
  {
    containerBuilder.RegisterModule((IModule) Activator.CreateInstance(module));
  }
}

private static void RegisterRealOrTestImplementations(ContainerBuilder containerBuilder, List<Type> allTypes)
{
  if (StaticConfigurationHelper.UseRealImplementationsInsteadOfTestImplementations)
  {
    var realTypes = allTypes.Where(type => type.GetCustomAttribute<RealImplementationAsInstancePerLifetimeScopeAttribute>() != null).ToArray();
    containerBuilder.RegisterTypes(realTypes).AsImplementedInterfaces()
      .InstancePerLifetimeScope();
  }
  else
  {
    var testTypes = allTypes.Where(type => type.GetCustomAttribute<TestImplementationAsInstancePerLifetimeScopeAttribute>() != null).ToArray();
    containerBuilder.RegisterTypes(testTypes).AsImplementedInterfaces()
      .InstancePerLifetimeScope();
  }
}

private static void RegisterGenericInterfaces(ContainerBuilder containerBuilder, List<Type> allTypes)
{
  var typesAsGenericInterface = allTypes.Where(type => type.GetCustomAttribute<RegisterAsGenericInterfaceAttribute>() != null).ToArray();
  foreach (var type in typesAsGenericInterface)
  {
    var attribute = type.GetCustomAttribute<RegisterAsGenericInterfaceAttribute>();
    containerBuilder.RegisterGeneric(type).As(attribute.Type);
  }
}

private static void RegisterClassesThatAreInstancePerLifetimeScope(ContainerBuilder containerBuilder, List<Type> allTypes)
{
  var typesAsInstancePerDependency = allTypes.Where(type => type.GetCustomAttribute<InstancePerLifetimeScopeAttribute>() != null).ToArray();
  containerBuilder.RegisterTypes(typesAsInstancePerDependency).InstancePerLifetimeScope().AsImplementedInterfaces();
}

private static void RegisterClassesThatAreSingleton(ContainerBuilder containerBuilder, List<Type> allTypes)
{
  var typesAsSingleton = allTypes.Where(type => type.GetCustomAttribute<SingletonAttribute>() != null).ToArray();
  containerBuilder.RegisterTypes(typesAsSingleton).SingleInstance().AsImplementedInterfaces();
}

private static void RegisterAllClassesWithoutAttribute(ContainerBuilder containerBuilder, List<Type> allTypes)
{
  var types = allTypes.Where(type => !typeof(IModule).IsAssignableFrom(type) &&
                                     type.GetCustomAttribute<DoNotRegisterInIocAttribute>() == null &&
                                     type.GetCustomAttribute<SingletonAttribute>() == null &&
                                     type.GetCustomAttribute<RealImplementationAsInstancePerLifetimeScopeAttribute>() == null &&
                                     type.GetCustomAttribute<TestImplementationAsInstancePerLifetimeScopeAttribute>() == null &&
                                     type.GetCustomAttribute<InstancePerLifetimeScopeAttribute>() == null &&
                                     type.GetCustomAttribute<RegisterAsGenericInterfaceAttribute>() == null).ToArray();
  containerBuilder.RegisterTypes(types).AsSelf().AsImplementedInterfaces();
}

Where the assemblies that are delivered to the RegisterAssemblies method could be fetched like this:

private List<Assembly> GetAssemblies()
{
  var assemblies = AssemblyResolveHelper.LoadAssemblies(AppDomain.CurrentDomain.BaseDirectory,
    new Regex(@"Usd.EA.*\.dll"),
    SearchOption.TopDirectoryOnly);
  assemblies.AddRange(AssemblyResolveHelper.LoadAssemblies(AppDomain.CurrentDomain.BaseDirectory,
    new Regex(@"Usd.Utilities.*\.dll"),
    SearchOption.TopDirectoryOnly));

  assemblies.Add(GetType().Assembly);
  return assemblies.Distinct().ToList();
}

The attributes

The attributes used in RegisterAllClassesWithoutAttribute are custom attributes that we manually assign to individual classes

using System;

[AttributeUsage(AttributeTargets.Class)]
public class DoNotRegisterInIocAttribute : Attribute
{
}

Used like this

[ExcludeFromCodeCoverage]
[DoNotRegisterInIoc]
public sealed class TestClass : ITestClass

When I'm not overwriting Autofacs MaxResolveDepth I get the following error

Failed An error occurred when trying to create a controller of type 'BogfoerController'. Make sure that the controller has a parameterless public constructor. An exception was thrown while activating λ:Usd.EA .Bogfoering.WebApi.Controllers.BogfoerController -> Usd.EA.Bogfoering.WebApi.Controllers.BogfoerController -> ...... Probable circular dependency between factory-scoped components. Chain includes 'Activator = DomainWrapper (DelegateActivator), Services = SomeService, Lifetime = Autofac.Core.Lifetime.CurrentScopeLifetime, Sharing = None, Ownership = ExternallyOwned'


Solution

  • Short answer: This is casused by the Autofac behaviour when resolving services from a child ILifetimeScope created by calling BeginLifetimeScope(Action<ContainerBuilder> configurationAction).

    Long answer: I have set up a simple test to prove above statement. I have generated a 51 test classes referencing themselves.

    public class Test0
    {
        public Test0() { }
    }
    
    public class Test1
    {
        public Test1(Test0 test) { }
    }
    
    (...)
    
    public class Test50
    {
        public Test50(Test49 test) { }
    }
    

    Registered them in a newly created container and tried to resolve the "Test50" class directly from the container. As you already found out. There is hard coded limit of 50 dependencies depth in the Autofac library, which you can see it on the GitHub page. After reaching this limit the DependencyResolutionException is thrown stating "Probable circular dependency between factory-scoped components." And this is exactly what happened in my first test.

    Now you have asked, why are you seeing multiple registrations of the same dependencies. So here comes the fun part. When you are trying to resolve your instance, you are probably gonna use the BeginLifetimeScope function to create new ILifetimeScope. This would be still ok, unless you are going to add some new registrations to the child scope using one of the overloads. See example below:

    using (var scope = container.BeginLifetimeScope(b => { }))
    {
        var test = scope.Resolve<Test49>();
    }
    

    I'm resolving only 50 dependencies (which have previously worked), but now, it yields an exception:

    Exception message

    As you can see, this is exactly the same behaviour as you previously described. Each dependency is now showed 2 times. On that image, you can also see that the dependency graph has only reached the Test25 class. This has effectively reduced the previous max depth by a half (whole 25 dependencies!). We can test this by successuflly resolving Test24 class, but exception is thrown when trying to resolve the Test25. This goes even funnier, how do you think, what happens if we add another scope?

    using (var scope1 = container.BeginLifetimeScope(b => { }))
    {
        using (var scope2 = scope1.BeginLifetimeScope(b => { }))
        {
            var test2 = scope2.Resolve<Test49>();
        }
    }
    

    You probably guessed it, now you can only resolve the dependencies of depth 50 / 3 = ~16.

    Yet another exception

    Conclusion: Creating nested scopes is limiting the actual available maximum depth of the dependencies graph N times, where the N is the depth of the scope. To be honest, scopes created without extending the container builder do not affect this number. In my opinion, this is a huge absurd, to have hard-coded magic number, which is nowhere in the documentation, cannot be easily configured, doesn't even represent the actual maximum depth and when overflowed, it throws misleading exception stating that you have circular dependencies in the graph somewhere.

    Solutions: As a resolution to this issue you could not use this overload of this function. This could be not possible due to architecture limitations, or even the 3rd party framework which could be using the Autofac as DI container.

    Another solution that you have already mentioned is overwriting the MaxResolveDepth using dirty reflection.

    string circularDependencyDetectorTypeName = typeof(IContainer).AssemblyQualifiedName.Replace(typeof(IContainer).FullName, "Autofac.Core.Resolving.CircularDependencyDetector");
    Type circularDependencyDetectorType = Type.GetType(circularDependencyDetectorTypeName);
    FieldInfo maxResolveDepthField = circularDependencyDetectorType.GetField("MaxResolveDepth", BindingFlags.Static | BindingFlags.NonPublic);
    
    maxResolveDepthField.SetValue(null, 500);
    

    On the Autofac's GitHub you can also read that they are already planning to change the behaviour of the CircularDependencyDetector, so it could handle the infinite depth of dependencies, but those plans were mentioned in 2018 and they even couldn't change that exception message by this date.