2

I am trying to add to a Dictionary where the value is a class with two generics. The two generics must derive from an abstract class, BaseEntity:

internal abstract class BaseEntity
{
    ...
}

internal class DataEtlModelRegistration<T, TResult> where T : BaseEntity where TResult : BaseEntity
{
    ...
}

internal class DataEtlContext : IDataEtlContext
{
    private readonly Dictionary<Type, DataEtlModelRegistration<BaseEntity, BaseEntity>> models = new Dictionary<Type, DataEtlModelRegistration<BaseEntity, BaseEntity>>();

    public void RegisterModelType<T, TResult>() where T : BaseEntity where TResult : BaseEntity
    {
        models.Add(typeof(T), new DataEtlModelRegistration<T, TResult>());
    }
}

I would expect this to be valid since the RegisterModelType method ensures that T and TResult both derive from the BaseEntity class by nature of the type contraints.

However, I am receiving the following error:

Argument 2: cannot convert from '...DataEtlModelRegistration<T, TResult>' to '...DataEtlModelRegistration<BaseEntity, BaseEntity>'.

The error lint is on the following code:

new DataEtlModelRegistration<T, TResult>()

Can anyone explain why this is, and propose a possible solution?

Barnaby Dove
  • 183
  • 1
  • 6
  • I's obviously not valid as DataEtlModelRegistration is not derived from DataEtlModelRegistration – Selvin Jul 12 '19 at 10:37

3 Answers3

2

You might want to check co- and contravariance.

You haven't shown us the definition of your DataEtlModelRegistration<T, TResult> class, but let's imagine that it has a method with signature:

void Accept(T t);

(any method accepting T as a parameter will do).

Now let's also imagine DerivedEntity inheriting from BaseEntity. And now let's change our universe to one in which models.Add(typeof(T), new DataEtlModelRegistration<T, TResult>()); is valid code and call RegisterModelType<DerivedEntity, TAnything>, where TAnything can be, well, anything deriving from BaseEntity.

So an object of type DataEtlModelRegistration<DerivedEntity, TAnything> is now in the dictionary under the key typeof(DerivedEntity). Let's try to extract it:

DataEtlModelRegistration<BaseEntity, BaseEntity> model = models[typeof(DerivedEntity)];

Now, since entity is of type DataEtlModelRegistration<BaseEntity, BaseEntity>, this code should work (provided BaseEntity has an available default ctor):

model.Accept(new BaseEntity());

Bang, the type system is broken. You've passed BaseEntity to a method that accepts DerivedEntity as a parameter. BaseEntity is not DerivedEntity, you cannot do that.

Because of that, generic types are invariant by default. Basically it means that, e.g. List<DerivedEntity> is not a List<BaseEntity>, because you shouldn't be able to add any BaseEntity to a list of DerivedEntitys. So, if your class contains a method accepting T (or TResult, the same logic applies) as a parameter, you cannot do what you're trying to do.

However, if there is no such method, then you could make your types covariant by using an interface:

interface IModelRegistration<out T, out TResult> where T : BaseEntity where TResult : BaseEntity
{
    ...
}

internal class DataEtlModelRegistration<T, TResult> : IModelRegistration<T, TResult> where T : BaseEntity where TResult : BaseEntity
{
    ...
}

Basically you're telling the compiler "Hey, this interface will never accept any objects of the generic types, it only returns them". That code will fail to compile if the interface IModelRegistration contains a method with T or TResult as its parameter. Now it's legal to say:

private readonly Dictionary<Type, IModelRegistration<BaseEntity, BaseEntity>> models = new Dictionary<Type, IModelRegistration<BaseEntity, BaseEntity>>();

models.Add(typeof(DerivedEntity), new DataEtlModelRegistration<DerivedEntity, DerivedEntity>());

You will be able to extract an object from the dictionary as an instance of the IModelRegistration interface.

IModelRegistration<BaseEntity, BaseEntity> model = models[typeof(DerivedEntity)];

There's no way for you to break the type system now, because we know for a fact that the IModelRegistration interface has no methods that would accept an object of any of its type parameters.

You can also check out this question, where I gave an explanation for how contravariance works.

V0ldek
  • 9,623
  • 1
  • 26
  • 57
-1

You cannot inherit the type definitions. Even if T and TResult inherit from BaseEntity it doesn't mean that List<BaseEntity> = new List<T> works. Otherwise you could do this List<object> list = new List<String>(). You can create List<BaseEntity> and fill it with T and TResult objects, though.

You can either

models.Add(typeof(T), new DataEtlModelRegistration<BaseEntity, BaseEntity>());

and insert Tand TResult objects into the DataEtlModelRegistration or

private readonly Dictionary<Type, DataEtlModelRegistration<T, TResult>> models = new Dictionary<Type, DataEtlModelRegistration<T, TResult>>();

if it is important that the objects' types matter.

Juho Rutila
  • 2,316
  • 1
  • 25
  • 40
-1

DataEtlModelRegistration is not derived from DataEtlModelRegistration even if generic parameters have some relationship. You can't avoid it for classes. It is possible to workaround with interfaces, since they support co-variance, but I am not sure interfaces will work for you, since you may need data and method implementations in those classes.

A possible alternative is to have non-generic base class for DataEtlModelRegistration with all necessary virtual methods (or even interface). And the dictionary above will have it as the value type parameter.

yurexus
  • 323
  • 1
  • 5