Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

For Func<T, TResult>, where A extends T, A does not satisfy for T

Okay, let me set the scene: We have a function used within our code that takes a function and does some logging around it and then returns the result. It looks a little something like this.

TResponse LoggedApiCall<TResponse>(Func<BaseRequest, BaseResponse> apiCall, ...)
    where TResponse : BaseResponse;

In use with this I have the four following objects

namespace Name.Space.Base {
    public class BaseRequest {
        ...
    }
}

namespace Name.Space.Base {
    public class BaseResponse {
        ...
    }
}

namespace Some.Other.Name.Space {
    public class Request : BaseRequest {
        ...
    }
}

namespace Name.Space {
    public class Response<TPayload> : BaseResponse {
        ...
    }
}

So, with these I am trying mock LoggedApiCall (using Moq) in order to support some Unit Tests. I am writing a generic method that allows us to pass in a function that meets the base type constraints and a response that also type matches to create a common method to perform .Setup() on the Mock.

It looks like this:

protected IReturnsResult<IService> SetupLoggedApiCall<TRequest, TResponse>(
    Func<TRequest, TResponse> function,
    TResponse response
    ) 
    where TRequest : BaseRequest 
    where TResponse : BaseResponse
{
    var baseFunction = function as Func<BaseRequest, BaseResponse>;
    return _mockService.Setup(service => service.LoggedApiCall<TResponse>(
            baseFunction, /*other parameters *
        ))
        .Returns(response);
    }
}

The reason I am attempting to cast the function is that if I do not, I get the intellisense error

Argument type 'System.Func<TRequest, TResponse>' is not assignable to
parameter type 'System.Func<Name.Space.Base.BaseRequest, Name.Space.Base.BaseResponse>'

This, I find a little bemusing as TRequest and TResponse are constrained as by BaseRequest and BaseResponse respectively but if work around I must, I shall. However, when performing the cast

var baseFunction = function as Func<BaseRequest, BaseResponse>

it resolves as null. This, I also find bemusing due to the afore mentioned constraints on the parameter passed into SetupLoggedApiCall. I did some further digging whilst debugging the code and got the following:

function is Func<TRequest, TResponse>       | true
function is Func<TRequest, BaseResponse>    | true
function is Func<BaseRequest, BaseResponse> | false

As this shows, TResponse continues to satisfy BaseResponse and can be cast to it with no issues. However, as soon as we try and move from TRequest to BaseRequest it fails. Just to make sure I wasn't getting into a situation where imported any wrong types or anything I followed this us with:

typeof(TRequest).BaseType == typeof(BaseRequest) | true

So, can anyone tell me: Given that everything points to TRequest being a BaseRequest this cast fails on the matter of TRequest?

We're going to begin stripping this down and isolating the problem in a new code project (in reality, our code is not quite as simple as it is below, I simplified it to it's core) and see at what point it fails and we'll update if we find anything but any insight would be appreciated.

Update 1

Upon following from the suggestion @EugenePodskal I updated the definition of LoggedApiCall to read

TResponse LoggedApiCall<TRequest, TResponse>(Func<TRequest, TResponse> apiCall, ...)
    where TRequest : BaseRequest where TResponse : BaseResponse

This made SetupLoggedApiCall happy, at least at compile time, in that what was being passed in would be valid however, the call to the mocked service still returns null. I dug back into the proxy object and down to the interceptor for this call and I discovered this:

IService service => service.LoggedApiCall<Request, Response>(, /*other params*/)

That is not a typo. The first parameter simply missing from the interceptor. I guess this shifts the question more to being about Mock than Func but seeing as that the interceptor is a lamba expression anyone able to shed light on what could cause that paramater to simply be missing?

like image 463
Kira Namida Avatar asked Oct 21 '14 09:10

Kira Namida


1 Answers

The topic you need to look at is Covariance and Contravariance in Generics

http://msdn.microsoft.com/en-us/library/dd799517(v=vs.110).aspx

i.e. "In the .NET Framework 4, the Func generic delegates, such as Func, have covariant return types and contravariant parameter types."

It is logical as well, if you think about it..

Func<Apple, AppleProduct> MakeAppleProduct = new Func....;

// Assume the cast is allowed at runtime and doesn't throw.
Func<Fruit, FruitProduct> MakeFruitProduct = (Func<Fruit, FruitProduct>) MakeAppleProduct;

//Returns an instance of AppleProduct
MakeFruitProduct(appleInstance);

//Orange is also a Fruit, and hence we are allowed to pass it? Should it be allowed?
MakeFruitProduct(orangeInstance);

Hence for function parameters, you do not want to allow up cast to base type.

On the other hand, for return values, if the function was originally declared to return instance of AppleProduct, it is 100% (type) safe to say it returns an instance of FuitProduct (base class for AppleProduct)

like image 54
Vikas Gupta Avatar answered Oct 25 '22 03:10

Vikas Gupta