Stackoverflow adventures: Dependency injection on the loose

Akos Nagy
Oct 24, 2017

Sometimes I do these posts where I describe an interesting question on SO. And sometimes I do posts about dependency injection and dependency injection containers. Well, this time, these two series cross-over (the Avengers of blog posts :) ).

So Autofac has a number of nice features described as implicit relationship types. One is the enumeration type. This gives you the ability to register multiple types to an interface, and then resolve them all as part of an IEnumerable. Pretty cool.
The other one is keyed registration. With this, you can add a key to a registration (like an enum), and then resolve the one that was registered with a given key. Again, pretty cool.

What I wanted to do is combine the two: register multiple implementations of an interface, and add a key to every registration. Then resolve all registered types for a given key in an IEnumerable. Someting like this:

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) { /* ... */ }
}

And what I found is that this didn't work. Even though I had specified the KeyFilter for my services, all the registered types are resolved for the IEnumerable. However, if I resolve it manually from the container directly (using the ResolveKeyed() method), then it works.

So what can I do? I dig through the docs, but didn't find anything useful. Then I asked the question on SO. I got some useful comments (worth checking out), but not the answer to my question.

But I wanted to make this work. And so I did. I tweaked the registration a bit: after all, the manual resolve was working. Here's what I came up with:

public static class AutofacExtensions
{
    public static IRegistrationBuilder<TService, SimpleActivatorData, SingleRegistrationStyle> RegisterWithKeyedResolution<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());
    }
}

It's not magic — I just did what I had expected Autofac would do...

Akos Nagy