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

1

9

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?

Omaromara answered 21/10, 2014 at 9:42 Comment(11)
I don't see how this is a duplicate - the linked question is a clarification of some details regarding the variance of Func while this question actually calls for an introduction/explanation of variance in general. The context is the same, but the question is not. The answer to the other question will likely not help the asker of this one.Armijo
@AntP The question linked is "why can't I cast Action<Derived> to Action<Base>", and this question is essentially asking "why can't I cast Func<Derived, X> to Func<Base, X>".Coelenterate
@Coelenterate Sure, if you like to oversimplify things. The other question and its answers already assume comprehension of the concept of variance, so the answers to that question are useless when applied to this one. E.g. "Why can't I cast Func<Derived, X> to Func<Base, X>" can't be properly answered with "You've got the covariance and contravariance the wrong way round."Armijo
Thank you for your responses and I'm getting to grips with what yo have presented. However, the issue I am finding is not that I cannot case Func<Derived, X> to Func<Base, X>, it's casting Func<DerivedA, DerivedB> to Func<BaseA, BaseB> where it works for B (as show by the successful is on Func<DerivedA, DerivedB> to Func<DerivedA, BaseB>. What I don't understand is why it you work for Out but not In.Omaromara
Func<T1, T2> is contravariant in T1 so if you try to assign to a variable of type Func<D1, B2> then D1 needs to be a subtype of T1. In your example, your argument type is a base type of the argument type so it is unsafe.Menorrhagia
Right, so to qualify: In the definition of Func<T1, T2> T1 is contravariant but T2 is not hence why we can upcast T2 successfully? Meaning this is the nature of Func.Omaromara
Func<T, TResult> can return anything that is more specialized(derived) than TResult type, but it can't return anything less derived(like base class), because the calling code, when it executes Func<T, TResult> MUST get TResult or SomeClass: TResult in return, otherwise the delegate just cannot be considered a valid Func<T, TResult> - compiler cannot guarantee that call of Func<T, TBase> will return valid TResult derived value.Orgeat
Yes, Funcs are contravariant in their argument types and covariant in their return type.Menorrhagia
Also, what preventing you from making the method signature like TResponse LoggedApiCall<TResponse, TRequest>(Func<TRequest, TResponse> apiCall, ...) where...?Orgeat
Lee: Thank you. =) EugenePodskal: That would have been my next recourse of action though this has brought forward a conversation about other problems with this approach so we might be replacing it anyway. Thank you all for you input.Omaromara
possible duplicate of Variance in Func<> argumentsMaishamaisie
K
2

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)

Kaspar answered 31/10, 2014 at 2:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.