Problem with Generics C# when matching types of abstract class

83 views Asked by At

I was trying to run this code, but there is a problem that an object's type doesn't match with its abstract class type when I am trying to check

it's on the line if (handler is Handler<IRequest<T>, T> h)

void Main()
{
    var requestHandler = new RequestHandler();
    var result = requestHandler.Handle(new GetAge());
    Console.WriteLine(result);
}


public interface IRequest<T> { }

public class GetAge : IRequest<int> { }

public interface IHandler {}

public abstract class Handler<TRequest, TResponse> : IHandler where TRequest : IRequest<TResponse>
{
    public TResponse Handle(IRequest<TResponse> request)
    {
        return Handle((TRequest)request);
    }

    protected abstract TResponse Handle(TRequest requst);
}

public class GetAgeHandler : Handler<GetAge, int>
{
    protected override int Handle(GetAge request)
    {
        return 20;
    }
}

public class RequestHandler
{
    public Dictionary<Type, IHandler> requestHandlers = new()
    {
        [typeof(GetAge)] = new GetAgeHandler()
    };

    public T Handle<T>(IRequest<T> request)
    {
        var handler = requestHandlers[request.GetType()];
        if (handler is Handler<IRequest<T>, T> h)
        {
            return h.Handle(request);
        }
        return default;
    }
}

It looks like it should print 20, but it doesn't.


Eventually I made it to work adding an extra Interface, and changing the Handle method in RequestHandler, but now the code became complicated.

Is there a way to make it simplier with using covariance or contravariance? I've tried using them, but couldn't achieve much.

As @Hayden commented, it should be possible, but how?

void Main()
{
    var requestHandler = new RequestHandler();
    var result1 = requestHandler.Handle(new GetAge());
    var result2 = requestHandler.Handle(new GetName());
    Console.WriteLine(result1);
    Console.WriteLine(result2);
}


public interface IRequest<T> { }

public class GetAge : IRequest<int> { }

public class GetName : IRequest<string> { }

public interface IHandler { }

public interface IHandler<TResponse> : IHandler 
{
    public TResponse Handle(IRequest<TResponse> request);
}

public abstract class Handler<TRequest, TResponse> : IHandler<TResponse> 
    where TRequest : IRequest<TResponse>
{
    public TResponse Handle(IRequest<TResponse> request)
    {
        return Handle((TRequest)request);
    }

    protected abstract TResponse Handle(TRequest requst);
}

public class GetAgeHandler : Handler<GetAge, int>
{
    protected override int Handle(GetAge request)
    {
        return 20;
    }
}

public class GetNameHandler : Handler<GetName, string>
{
    protected override string Handle(GetName request)
    {
        return "Foo";
    }
}

public class RequestHandler
{
    public Dictionary<Type, IHandler> requestHandlers = new()
    {
        [typeof(GetAge)] = new GetAgeHandler(),
        [typeof(GetName)] = new GetNameHandler()
    };

    public T Handle<T>(IRequest<T> request)
    {
        var handler = requestHandlers[request.GetType()];
        if (handler is IHandler<T> h)
        {
            return h.Handle(request);
        }
        return default;
    }
}

1

There are 1 answers

1
Hayden On

Simply put, the pattern matching fails due generic types being invariant by default. Even if you have Handler<GetAge, int>, it is not considered to be compatible with Handler<IRequest<int>, int>. If the generic types were a covariance or contravariance, then this would be allowed, but in this specific case, it can't be.

One way around this is to define the Handle method like so:

public TResponse Handle<TRequest, TResponse>(TRequest request)
    where TRequest : IRequest<TResponse>
{
    var handler = requestHandlers[typeof(TRequest)];
    if (handler is Handler<TRequest, TResponse> h)
    {
        return h.Handle(request);
    }
    return default;
}

Which you would call it like:

var result = requestHandler.Handle<GetAge, int>(new GetAge());