Can't add generic Interface to List with same constraint

Viewed 83

I'm struggling with generics and don't really know what I'm doing wrong.

This is my example:

public class Repository // Base-class for all repositories
{
    public virtual int GetStatus()
    {
        return 0;
    }
}

Repository is just a base class.

public class CarRepository : Repository // base-class for all car repositories
{
    private object dataSource;

    public override int GetStatus()
    {
        return dataSource.GetHashCode(); // dummy
    }

    public virtual int GetPrice(string carName)
    {
        if (carName.Equals("BMW", StringComparison.OrdinalIgnoreCase)) {
            return 100;
        }

        return 50;
    }
}

CarRepository simply provides basic methods to interact with cars.

public class HttpCarRepository : CarRepository // loads the car data from REST Api
{
    private dynamic httpClient; // just as an example
    public override int GetStatus()
    {
        return httpClient.IsConnected();
    }

    public override int GetPrice(string carName)
    {
        return httpClient.GetAsync("/rest/car/BMW").Result;
    }
}

There might also be an DataBaseCarRepository that loads the data from a database. You get the point.

That's for the setup.

Now, I want to cache the results. To keep it generic, I've created this construct:

public interface ICache<TRepo> // Basic Cache Interface
    where TRepo : Repository
{
    TRepo Repository { get; set; }
}
public class CarCache : CarRepository, ICache<CarRepository> 
{
    public CarRepository Repository { get; set; }
    private dynamic cache;

    public CarCache(CarRepository repo)
    {
        this.Repository = repo;
    }

    public override int GetPrice(string carName)
    {
        if (!this.cache.Contains(carName)) {
            this.cache.Add(carName, this.Repository.GetPrice(carName));
        }

        return cache[carName];
    }
}

CarCache derives from the base class CarRepository to make it possible to override the methods. It also implements ICache<T> which provides a reference to an actual implementation of CarRepository, such as HttpCarRepository.

Now I want to add the CarCache to a list of caches.

public class Manager
{
    public List<ICache<Repository>> Caches;
}

I've used Repository as the generic type because the ICache<T> interface constraints the type to Repository.

Now the problem: I've got a method to add a cache that looks like this

static void Add<TCache>(Repository repo)
    where TCache : Repository, ICache<TCache>
{
    ICache<TCache> newEntry = Activator.CreateInstance(typeof(TCache), repo) as ICache<TCache>;
    Caches.Add(newEntry); // Error: Cannot convert from ICache<TCache> to ICache<Repository>
}

That confuses me. From my understanding this should work because I've added the constraint where TCache : Repository to the method, so adding an item of that type to a list of ICache<Repository> should work. It's the same constraint.

What's the problem here?

1 Answers

One solution would be to make ICache<TRepo> covariant.

You would need to make TRepo Repository get-only to comply with covariant restrictions:

public interface ICache<out TRepo> where TRepo : Repository
{
    TRepo Repository { get; }
}

This would work fine as long as the property is only set via your constructor:

public class CarCache : CarRepository, ICache<CarRepository> 
{
    public CarRepository Repository { get; }

    public CarCache(CarRepository repo)
    {
        this.Repository = repo; // Fine to set Repository here
    }

    // ...
}

Or you could make the setter private to allow other methods of the implementation class to set the value:

public class CarCache : CarRepository, ICache<CarRepository> 
{
    public CarRepository Repository { get; private set; }

    // ...
    
    void SetRepository(CarRepository repo)
    {
        this.Repository = repo;
    }
}
Related