3

I'm using Autofac in an application to manage dependencies. I have an interface and a number of implementations for the interface. The actual implementations are registered as keyed services in the container.

What I would like to do is to resolve an instance of every service (hence the IEnumerable) that are registered with a specific keytype (hence the typed registration).

If I use the container directly, it works:

container.ResolveKeyed<IEnumerable<IService>>(MyServiceGroups.Group1);
// This returns the a list of IService implementor objects, that were previously registered with the given key

However, if I use the [KeyFilter] attribute in my constructors to resolve the dependencies, it has no effect and I get the list of all registered services, regardless of the value used at the keyed registrations.

public class MyBigService([KeyFilter(MyServiceGroups.Group1)] services)
{
   // here services contains one from every type, not just the ones registered with that particular key
}

What am I doing wrong? Is there any way to make this work? I could probably combine the Func and IEnumerable types and resolve from the container that way manually (since that works), but I'd like to keep this structure.

EDIT Concrete example with code:

public class SubserviceModule : Autofac.Module
{
  protected override void Load(ContainerBuilder builder)
  {

    builder.RegisterType<SubServiceA>().As<ISubService>().Keyed<ISubService>(ServiceType.TypeX);
    builder.RegisterType<SubServiceB>().As<ISubService>().Keyed<ISubService>(ServiceType.TypeX);

    builder.RegisterType<SubServiceC>().As<ISubService>().Keyed<ISubService>(ServiceType.TypeY);
    builder.RegisterType<SubServiceD>().As<ISubService>().Keyed<ISubService>(ServiceType.TypeY);
  }
}

public class ServiceModule : Autofac.Module
{
  protected override void Load(ContainerBuilder builder)
  {
     builder.RegisterType<Service1>();                     
     builder.RegisterType<Service2>();
  }
}

public abstract class ServiceBase
{
    public ServiceBase(IEnumerable<ISubService> subServices) {/*...*/}
}

public class Service1
{
   public ServiceA([KeyFilter(ServiceGroup.ServiceTypeX)] IEnumerable<ISubService> subServices) 
       : base(subServices) { /* ... */ }
}

public class Service2
{
   public ServiceB([KeyFilter(ServiceGroup.ServiceTypeY)] IEnumerable<ISubService> subServices) 
       : base(subServices) { /* ... */ }
}
Akos Nagy
  • 4,201
  • 1
  • 20
  • 37
  • Can you show how you are registering them with Autofac? – ovation22 Oct 20 '17 at 17:53
  • @ovation22 Edited the question with the modules and the service definitions. – Akos Nagy Oct 20 '17 at 18:09
  • See [Dependency injection type-selection](https://stackoverflow.com/a/34331154). You don't have a DI problem, you have an *application design* problem that can be resolved with design patterns. It is pretty simple to build a system that can select multiple classes based on a key, and relying on a DI container to do it means your app is tightly coupled to the DI container. – NightOwl888 Oct 20 '17 at 22:25
  • @NightOwl888 Thanks for the comment. The difference between your approach and mine is that you have a component that selects from instances of all the types. What I want is to receive only the selected ones (hence not instantiating the ones that the service doesn't need). As for the tight-coupling, the only problem I see is the attribute itself comes from Autofac. But if I write my own, I can do a similar resolve with any container manually (just as I did with Autofac in the gist). Also, the DI container for me is a stable dependency, so coupling is less of an issue. – Akos Nagy Oct 21 '17 at 07:29
  • Look at the solution again. It *can* select multiple types. The only thing you need to change is the implementation of `AppliesTo` for each type so multiple types are selected by a single key. If you want to lazy load the types, you can [combine strategy with abstract factory](https://stackoverflow.com/a/31971691/). – NightOwl888 Oct 21 '17 at 07:43
  • And what about the strategy itself? In order to be able to select the appropriate service, it has to call the `AppliesTo` method, but to do that, you need an instance. So the strategy itself must have an instance of every service, even if the service is not selected. Thank you for the factory combined solution; I'm not really sure how that works yet, but I'll dig through it, I'm sure there's a lot to learn from it. But even so it seems a lot of work compared to the Autofac solution, given that the container as Autofac is a stable dependency for me. – Akos Nagy Oct 21 '17 at 07:59
  • Depends on how you view "a lot of work". Do note if you use the container as a service locator, you will end up paying down the road in maintenance. But how much maintenance that actually is depends on many factors. – NightOwl888 Oct 21 '17 at 08:19
  • If your type lists won't change at runtime, another option could be [generic or inherited interfaces](https://stackoverflow.com/a/41811716/). Keep in mind to the .NET type system `ISomething` is a completely different type than `ISomething` – NightOwl888 Oct 21 '17 at 08:25
  • The type lists can change at runtime, so generics is not an option. Using the container as a service locator is a bad practice; that's exactly why I'd like to make it work with the attribute-style resolution and not have to manually resolve from the container. – Akos Nagy Oct 21 '17 at 08:55

1 Answers1

0

Well, I came up with an answer myself for now. I basically do what I'd expect Autofac would do: go though all the parameters, and use the current resolving context to resolve it. I check the parameter myself for the attribute, and if it's there, I resolve as a keyed service, otherwise, I just resolve. I also created a nice little extension method to hide the added complexity of this registration:

public static class AutofacExtensions
{
    public static IRegistrationBuilder<TService, SimpleActivatorData, SingleRegistrationStyle> RegisterModulePageViewModel<TService>(this ContainerBuilder builder) where TService : ServiceBase
    {
        return builder.Register(ctx => CreateInstance<TService>(ctx));
    }

    private static TService CreateInstance<TService>(IComponentContext ctx)
    {
        var ctor = typeof(TService).GetConstructors().Single();
        List<object> parameters = new List<object>();
        foreach (var param in ctor.GetParameters())
        {
            var keyAttribute = param.GetCustomAttribute<KeyFilterAttribute>();
            if (keyAttribute != null)
            {
                parameters.Add(ctx.ResolveKeyed(keyAttribute.Key, param.ParameterType));
            }
            else
            {
                parameters.Add(ctx.Resolve(param.ParameterType));
            }
        }
        return (TService)ctor.Invoke(parameters.ToArray());
    }
}
Akos Nagy
  • 4,201
  • 1
  • 20
  • 37