Search code examples
autofac

Autofac - dynamic Resolve based on registered provider


I'm having problems finding proper solution for my problem, namely:

Let's consider workflow:

  1. Application starts
  2. Main components are registered in Autofac
  3. Application loads plugin assembly and registers modules within it
  4. Container is being build
  5. Plugin handling logic is run

Plugin can add its own controllers. To properly handle that I had to prepare interface which will provide me types of custom controllers:

interface ICustomControllerProvider
{
    IEnumerable<Type> GetControllerTypes();
}

Based on the above my app knows how to integrate specified types as controllers. All controllers are also defined as services, so Autofac deals with their creation, and so...

Problem: I want to avoid specifying custom controller type twice

public PluginControllerProvider : ICustomControllerProvider
{
    public IEnumerable<Type> GetControllerTypes()
    {
        // 1st type specification
        // controller types are specified here, so they could be integrated with app
        yield return typeof(ControllerX);
        yield return typeof(ControllerY); 
    }
}

public class PluginModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterType<PluginControllerProvider>().As<ICustomControllerProvider>();
        // 2nd type specification
        // controllers have to be register in module as well
        builder.RegisterType<ControllerX>();
        builder.RegisterType<ControllerY>();
    }
}

Is there any way how ControllerX and ControllerY could be managed by Autofac, where I specified them only in PluginControllerProvider? I tried achieving that by providing custom registration source and resolving ICustomControllerProvider, however I cannot resolve ICustomControllerProvider based on arguments provided by IEnumerable<IComponentRegistration> RegistrationsFor(Service service, Func<Service, IEnumerable<ServiceRegistration>> registrationAccessor) from IRegistrationSource


Solution

  • Autofac does not offer the ability to inject a list of types registered with Autofac. You'll have to get that yourself by working with the lifetime scope registry.

    Before I get into how you might solve this, I should probably note:

    • Listing the types registered is not a normal thing from a DI perspective. It'd be like listing all the controllers in your MVC application - you don't normally need to do that, and if you did, you'd likely need to build a whole metadata structure on top of it like the ApiExplorer that was built to do that on top of ASP.NET Core. That structure wouldn't be supported or involved with the DI system because you're querying about the system, not injecting live instances into the system.
    • If you are relying on DI to resolve controllers, you probably don't need a whole separate controller provider. Once you know what type you need for the request, you'd just resolve it.

    All that's to say, while I'll answer the question, the way the design here is posed may be something you'd want to look at. What you're doing, trying to involve DI with listing metadata about the app... seems somewhat backwards (you'd feed the DI container based on the list of types, not get the list of types from the DI container).

    But let's just go with it. Given:

    • There are controllers registered with Autofac
    • The controllers have no common base class or interface
    • There's no attributes on the controllers you could query

    What I'd do is register all the controllers with some metadata. You need something to be able to locate the controllers in the list of all the types in the container.

    builder.RegisterType<ControllerX>().WithMetadata("controller", true);
    builder.RegisterType<ControllerY>().WithMetadata("controller", true);
    

    Now in the plugin controller, you need to inject an ILifetimeScope because you have to query the list of stuff registered. The ILifetimeScope that gets injected into the controller will be the same scope from which the plugin controller itself was resolved.

    You can use the injected scope to query things in the component registry tagged with your metadata.

    public class PluginControllerProvider : ICustomControllerProvider
    {
        private readonly Type[] _controllerTypes;
        public PluginController(ILifetimeScope scope)
        {
            _controllerTypes = scope
                .ComponentRegistry
                .Registrations
                .Where(r => r.Metadata.ContainsKey("controller"))
                .Select(r => r.Activator.LimitType)
                .ToArray();
        }
    
        public IEnumerable<Type> GetControllerTypes()
        {
            return _controllerTypes;
        }
    }
    

    Now, disclaimers:

    • If you are registering more controllers in child lifetime scopes (e.g., during a BeginLifetimeScope() call), you will need a controller provider from that scope or it won't get all the controller types. The provider needs to come from the scope that has all the registrations.
    • If you're using registration sources (like the AnyConcreteTypeNotAlreadyRegisteredSource), this won't capture things that come from the registration sources. It'll only capture things that come from direct registrations of a type (or lambda) on a ContainerBuilder.

    But it should work.