C# Autofac Generic interface resolve to instance of base class instead of child

Viewed 166

First, some background on my code.

I have a interface for items that can be saved/used with a data store/repository type service:

public interface IItem
{
    Guid Id { get; set; }
}
public interface IDataStore<T> where T: IItem { ... }

An example of such an item:

public class ItemNormalExample : IItem
{
    public Guid Id { get; set; }
    public string RandomProperty { get; set; }
}

And then I also have special/specific type of item, which other items inherit from. This is where my problem lies.

public interface ISpecialItem : IItem
{
    string ImportantString { get; set; }
}
public class ItemSpecialBase : ISpecialItem
{
    public Guid Id { get; set; }
    public string ImportantString  { get; set; }
}
public class ItemSpecialExample : ItemSpecialBase 
{
    public string RandomProperty { get; set; }
}

I am using Autofac to resolve instances of this data store of each concrete implementation of IItems. I.e.:

builder.RegisterType<DataStore<ItemNormalExample>>().As<IDataStore<ItemNormalExample>>();

This works as intended, however, for my objects inheriting from the ItemSpecialBase base class, I would like these to be instantiated as a DataStore of the base class type instead of the actual child class.

Something like this (This doesn't work):

builder.RegisterType<DataStore<ItemSpecialBase>>().As<IDataStore<ItemSpecialExample>>();

My intend is for every object inheriting from ItemSpecialBase to use the same DataStore. My DataStore class supports this and works fine.

How can I accomplish this using Autofac?

1 Answers

Looks like you have run into a Variant Generic Interface problem!

Specifically, when you do...

builder
  .RegisterType<DataStore<ItemSpecialBase>>()
  .As<IDataStore<ItemSpecialExample>>();

...you're attempting to cast a Storage<Base> into an IStorage<Child>, which is known as Contravariance.

The Contravariance problem can be simplified as so:

class Animal { }
class Cat : Animal { }
class Dog : Animal { }

interface IStorage<T> {
    void Put(T thing);
    T? Get();
}
class Storage<T> : IStorage<T> {
    public void Put(T t) { }
    public T? Get() { return default(T); }
}

void Main() {
    var builder = new ContainerBuilder();

    // What you're attempting to do:
    builder.RegisterType<Storage<Animal>>().As<IStorage<Cat>>();
    builder.Build();

    // Is equivalent to this:
    IStorage<Cat> cats = new Storage<Animal>();

    // The compiler will stop you from doing either.
}

The problem is that generics are validated at compile-time. And the compiler notices that, while you can Put a Cat in an Animal Storage, when you try to Get something out of storage it cannot guarantee you'll get a Cat - the storage may contain Dogs. (note the in and out, which are relevant to the covariant/contravariant discussion).

One thing you can do is to acknowledge to the compiler that you will only attempt to put a Child class in, and will not try to get a Child out:

// Notice the `in` constraint
interface IStorage<in T> {
    // We can pass `T` in as parameter
    void Put(T thing);
    
    // We cannot return `T` anymore, because we promised to
    // only use T in the `in` direction, and not out.
    object? Get();
}

// This stays the same. `in` can only be enforced against interfaces.
class Storage<T> : IStorage<T> {
    public void Put(T t) { }
    public object? Get() { return default(T); }
}

void Main() {
    var builder = new ContainerBuilder();

    // This is now allowed:
    builder.RegisterType<Storage<Animal>>().As<IStorage<Cat>>();
    builder.Build();
    
    // And this too:
    IStorage<Cat> cats = new Storage<Animal>();
}
Related