Contravariant interface to common interface

52 Views Asked by At

I have following contravariant interface and its implementation

public interface IMessageSender<in TMessage>
{
    Task SendAsync(TMessage @object);
}

public class MessageSender<TMessage> : IMessageSender<TMessage>
{
    public Task SendAsync(TMessage @object)
    {
        throw new NotImplementedException();
    }
}

I have hierarchy of classes that should represent TMessage .

public class Base { }
public class Descendant1 : Base { }
public class Descendant2 : Base { }

So I have two senders for simplicity

var descendant1Sender  = new MessageSender<Descendant1>();
var descendant2Sender  = new MessageSender<Descendant2>();

How can I cast these two senders to common interface to pass for example to the method. For example.

static void SomeMethod( IServiceBusMessageSender<Base> baseSender)
{
    baseSender.SendAsync(new Descendant1());
}

SomeMethod(descendant1Sender);  //This won't compile because of contravariance
1

There are 1 best solutions below

6
Guru Stron On

You can't treat IMessageSender<Descendant1> as IMessageSender<Base> because it is not one, contravariance or not, it requires parameter to be Descendant1 or Descendant1's descendant.

Contravariance allows to use IMessageSender<Base> as IMessageSender<Descendant1>, not vice versa. I.e. the following will work (and will result in compilation error if you make the interface invariant):

IMessageSender<Base> baseSender = ...;
IMessageSender<Descendant1> descendantSender = baseSender;

Demo

UPD

As workaround you can look into introducing non-generic version of interface and/or some typechecking:

public interface IMessageSender
{
    Task SendAsync(Base @object);
    bool CanSend(Base msg);
}
public interface IMessageSender<in TMessage> : IMessageSender
{
    Task SendAsync(TMessage @object);
}

public class MessageSender<TMessage> : IMessageSender<TMessage>
{
    public bool CanSend(Base msg) => typeof(TMessage).IsAssignableFrom(msg.GetType());

    public Task SendAsync(TMessage @object) => Task.CompletedTask;

    async Task IMessageSender.SendAsync(Base @object) // explicit interface implementation
    {
        if(@object is TMessage msg)
        {
            await SendAsync(msg);
        }
        else 
        {
            throw new Exception(); // or ignore
        }
    }
}

The idea being that SomeMethod will check if handler can send the message.