Autofac - how to register open generics as named or keyed types for use in a factory or using IIndex<,>

There's a bit to unpack here and, while I can kind of explain why things aren't working the way you want, the short answer is you're probably going to have to redesign how you're doing what you're doing. Which is likely not the answer you want, but... it's the answer.

To understand why, we need to ignore Autofac entirely for a moment and just look at the types you're working with.

You have an interface:

public interface IChartDataService<TSource, TTarget>

As an open generic, this could be IChartDataService<string, Exception> or it could be IChartDataService<object, object> or anything else. (Yes, I get there may be constraints, but stick with me - as an open generic, those types could change all over the place.) So when you talk about IChartDataService<,> in that form, that's what you're saying: "The open generic where anything could be in either of those type parameter spots."

Now, sometimes folks will create an equally generic implementation, like:

public class ChartDataService<TSource, TTarget>
  : IChartDataService<TSource, TTarget>

Taking a step back to Autofac, that's what RegisterGeneric is for - to handle the open generic implementations. If you had this sort of thing, you'd register like:

builder.RegisterGeneric(typeof(ChartDataService<,>))
       .As(typeof(IChartDataService<,>));

In that case, you're actually registering an open generic.

However, that's not what you have. You have types that implement an interface - a very specific interface. Let's look at what you have:

public class TreantService : IChartDataService<SourceDto, TargetDto>

Based on that, we see:

  • TreantService isn't a generic at all. That class, itself, has no generic type parameters.
  • TreantService implements a closed generic interface. Put another way, you can cast a TreantService to an IChartDataService<SourceDto, TargetDto> but you can't cast it to IChartDataService<string, Exception> or anything else.

For all intents and purposes, IChartDataService<SourceDto, TargetDto> in this case is no different than any other standard interface like IComparable or something. Once it's a closed generic like this, it's just a type.

So let's say you have a bunch of these, with a bunch of different DTO types:

public class FirstService
  : IChartDataService<FirstSource, FirstTarget> { }
public class SecondService
  : IChartDataService<SecondSource, SecondTarget> { }
public class ThirdService
  : IChartDataService<ThirdSource, ThirdTarget> { }

Sure, they all implement IChartDataService<TSource, TTarget> but they're closed generics. You can't cast them to the same underlying interface.

Now let's say you want to store them all in a List<T>. What would the T be there to make that work?

It'd be object - the only common base class they have. You'd have to use List<object>. Which, yeah, sucks, but as you found there's no such thing as a List<IChartDataService<,>>, or a dictionary that would hold an open generic as the value. If you think about it, that makes sense, because let's say you wanted to pull them back out:

// Pretend this is a thing.
var list = new List<IChartDataService<,>>();
foreach(var item in  list)
{
  // What type is `item` in this loop?
  // How does the compiler know what types
  // to fill in for the open generic? It's
  // not a dynamic language, so you can't
  // "switch types" on every loop iteration.
}

Hopefully at this point you can start seeing some of the problems with what you're trying to do, and it's not a problem with Autofac - it's a problem with how the interfaces are designed and how you want to consume them.

This is why you'll sometimes see non-generic interfaces like System.IEnumerable and generic counterparts like System.Generic.IEnumerable<T>. IEnumerable for the things that are common regardless of the generic type parameters and the generic to make things strongly typed.

I can't really tell you how to solve the problem because how you approach it will largely be dependent on your application code and what exactly you're doing. But, a recap of what I've covered here is where I'd personally start if it was me:

  • Ignore Autofac. Try to design things that you could mock out entirely in a unit test (like having a constructor parameter with the right types that can simulate what you'll actually see). If you can't get it to compile or work with all your types without Autofac, Autofac is not going to magically somehow make it work.
  • Consider a common non-generic interface. Sort of like that IEnumerable vs IEnumerable<T> difference. It may be that for implementation purposes it's nice to have that generic, but the actual common methods that get called don't require the generic (or maybe could take object?).