Skip to content

Fix return type equivalency checking - #977

Closed
JMolenkamp wants to merge 2 commits into
nsubstitute:mainfrom
JMolenkamp:fix-974
Closed

Fix return type equivalency checking#977
JMolenkamp wants to merge 2 commits into
nsubstitute:mainfrom
JMolenkamp:fix-974

Conversation

@JMolenkamp

Copy link
Copy Markdown
Contributor

Do not use areAssignable when comparing generic argument types in an equivalency check for return types.

Closes #974

@dtchepak dtchepak left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @JMolenkamp !

@Romfos this looks ok to me, can you please take a look as well when you get a chance?

Comment on lines +116 to +119
// Exclude the assignable check for generic arguments, generic types generally are not equivalent when a generic argument is not
// exactly the same, even though the types themselves may be assignable.
// See https://github.com/nsubstitute/NSubstitute/issues/974.
&& TypesAreAllEquivalent([a.ReturnType], [b.ReturnType], AreAssignableInclusionType.ExcludeGenericArguments)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: do you know if there needs to be any additional handling for generics with co/contravariance?

Comment on lines +186 to +200
[Test]
public void Returns_works_with_mismatching_generic_return_types()
{
ISomethingWithGenerics something = Substitute.For<ISomethingWithGenerics>();

something
.SomeFunction(Arg.Any<ICloneable>())
.Returns(Substitute.For<ICollection<ICloneable>>());

something
.SomeFunction(Arg.Any<string>())
.Returns(Substitute.For<ICollection<string>>());
}


Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: might be worth adding the specific case from #974 as well:

    [Test]
    public void Issue974()
    {
        var sub = Substitute.For<ISender>();

        sub.Send(Arg.Any<NonGenericRequest>()).Returns(Substitute.For<IResult>());
        sub.Send(Arg.Any<GenericRequest>()).Returns(Substitute.For<IResult<int>>());

        sub.Send(new NonGenericRequest());
        sub.Send(new GenericRequest());
    }

    public interface IResult { }
    public interface IResult<T> : IResult { }
    public interface IRequest<TResponse> { }
    public class NonGenericRequest : IRequest<IResult> { }
    public class GenericRequest : IRequest<IResult<int>> { }
    public interface ISender {
        Task<TResponse> Send<TResponse>(IRequest<TResponse> request);
    }

@zvirja

zvirja commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

@JMolenkamp Thanks a lot for your contribution! I created another PR inspired by your one which is a bit simpler. We shall publish it somewhere soon 😊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Configuring two returns on the same generic method with inheritance-related type arguments throws InvalidCastException

3 participants