Search code examples
c#dictionarygenericsinterfacecovariance

Covariance for value in Dictionary


I'm trying to add a more derived type as value to a dictionary<..., base> but i get the following error: Cannot convert from HandleIntegrationEvent<TR> to HandleIntegrationEvent<IBaseEvent>

Example

using System.Threading.Tasks;
using System.Collections.Generic;

public interface IBaseEvent
{
    string Name { get; }
}

public class UserCreatedEvent: IBaseEvent
{
    public string Name { get; } = "UserCreatedEvent";
}

public delegate Task HandleIntegrationEvent<in TR>(TR @event) where TR: IBaseEvent;

public class IntegrationBus
{
  private readonly IDictionary<string, HandleIntegrationEvent<IBaseEvent>> _listeners = new Dictionary<string, HandleIntegrationEvent<IBaseEvent>>();

  public void RegisterEventListener<TR>(string @event, HandleIntegrationEvent<TR> listener) where TR: IBaseEvent
  {
   // ERROR: cannot convert from HandleIntegrationEvent<TR> to HandleIntegrationEvent<IBaseEvent>
    _listeners.Add(@event, listener);
  } 
}

I just can't understand it and have been trying to understand the problem for some time now. As I understand it, the generic constraint should ensure that an instance has implemented the IBaseEvent interface.

I just have a mental block in the meantime

EDIT Found another great post https://stackoverflow.com/a/12841831 explaining the reason for dictionary covariance & contravariance. But I'm still a bit confused


Solution

  • The problem is that you have the variance backwards, it's covariance that you need not contravariance. It's confusing, because delegates make the variance flip around. And in this case, it's impossible to achieve, because the delegate itself needs to be contravariant in order to allow TR to be used as a parameter.

    In other words: a variable (or in this case the parameter of Dictionary.Add) of type HandleIntegrationEvent<IBaseEvent> cannot accept a delegate of HandleIntegrationEvent<TR>, but a variable of HandleIntegrationEvent<TR> can accept a HandleIntegrationEvent<IBaseEvent>.

    Suppose counter-factually that it were true that it could accept such a delegate. You have a location and a function:

    HandleIntegrationEvent<IBaseEvent> somelocation;
    
    public Task HandleUserCreatedEvent(UserCreatedEvent event)
    {
      ..
    }
    

    You take a HandleIntegrationEvent<UserCreatedEvent> and store it there.

    somelocation = HandleUserCreatedEvent;
    

    Someone now retrieves the delegate, and passes a different type to it

    await somelocation(new SomeOtherEvent());
    

    OOPS!

    So this is not allowed. But because you are storing these delegates in a more generalized Dictionary, you don't have much option other than to do a cast check at runtime, and either throw an exception or return null.

    The only thing you can do to prevent such a check is to store the actual event object with the delegate (closing over it in some way) and therefore the delegate itself knows how to handle the call. But it sounds like that isn't going to work for your use case.