Search code examples
c#genericscastingcqrs

some confusion with generic types in c#


I'm playing with cqs a little bit and I'm trying to implement this in a class library (so there's no IOC, IServiceProvider, etc). Here is some code that I wrote:

public interface IQuery<TResult>
{
}

public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
    TResult Handle(TQuery query);
}

public class Query : IQuery<bool>
{
    public int Value { get; set; }
}

public class QueryHandler : IQueryHandler<Query, bool>
{
    public bool Handle(Query query)
    {
        return query.Value > 0;
    }
}

public class Dispatcher
{
    private readonly Dictionary<Type, object> handlers = new Dictionary<Type, object>();

    public Dispatcher()
    {
        handlers.Add(typeof(Query), new QueryHandler());
    }

    public T Dispatch<T>(IQuery<T> query)
    {
        IQueryHandler<IQuery<T>, T> queryHandler;

        if (!this.handlers.TryGetValue(query.GetType(), out object handler) ||
            ((queryHandler = handler as IQueryHandler<IQuery<T>, T>) == null))
        {
            throw new Exception();
        }

        return queryHandler.Handle(query);
    }
}

And this si how I am calling my code:

Query query = new Query();
Dispatcher dispatcher = new Dispatcher();
var result = dispatcher.Dispatch(query);

But the problem is that inside the dispatcher, I don't know why the variable handler cannot be casted as IQueryHandler<IQuery<T>,T>. Here is some extra data: enter image description here

PS: I know how to make this work(with dynamic), but I want to understand why THIS code isn't working.


Solution

  • This code does not work because IQueryHandler is invariant on the TQuery generic parameter. TQuery needs to be covariant in order for handler to be convertible to IQueryHandler<IQuery<T>, T>, but that is impossible, as I will explain later. You could however, make TQuery contravariant, which allows you to convert handler to IQueryHandler<ASubclassOfQuery, T>. TResult can be covariant though. This is the code to do this:

    public interface IQueryHandler<in TQuery, out TResult> where TQuery : IQuery<TResult>
    

    See this page for more info about generic variances.

    As for why handler is not IQueryHandler<IQuery<T>, T>, let's first suppose that it is, which means this code would compile:

    IQueryHandler<IQuery<T>, T> q = handler;
    q.Handle(new MyQuery<T>());
    

    where MyQuery is defined like this:

    class MyQuery<T> : IQuery<T> {}
    

    However, handler is of runtime type QueryHandler. QueryHandler.Handle only handles Query objects, not MyQuery<T> objects! We have a contradiction, and hence our assumption that handler is a IQueryHandler<IQuery<T>, T> must be false.