diff --git a/src/NSubstitute/Core/CallSpecification.cs b/src/NSubstitute/Core/CallSpecification.cs
index e0d1ff15..c5984206 100644
--- a/src/NSubstitute/Core/CallSpecification.cs
+++ b/src/NSubstitute/Core/CallSpecification.cs
@@ -59,7 +59,7 @@ private static Type[] ParameterTypes(MethodInfo info)
return info.GetParameters().Select(p => p.ParameterType).ToArray();
}
- internal static bool TypesAreAllEquivalent(Type[] aArgs, Type[] bArgs)
+ internal static bool TypesAreAllEquivalent(Type[] aArgs, Type[] bArgs, AreAssignableInclusionType assignableInclusionType = AreAssignableInclusionType.Include)
{
if (aArgs.Length != bArgs.Length) return false;
for (var i = 0; i < aArgs.Length; i++)
@@ -79,21 +79,31 @@ internal static bool TypesAreAllEquivalent(Type[] aArgs, Type[] bArgs)
if (first.IsGenericType && second.IsGenericType
&& first.GetGenericTypeDefinition() == second.GetGenericTypeDefinition())
{
+ var genericArgumentsInclusionType = assignableInclusionType switch
+ {
+ AreAssignableInclusionType.Exclude => AreAssignableInclusionType.Exclude,
+ AreAssignableInclusionType.ExcludeGenericArguments => AreAssignableInclusionType.Exclude,
+ _ => AreAssignableInclusionType.Include
+ };
+
// both are the same generic type. If their GenericTypeArguments match then they are equivalent
- if (!TypesAreAllEquivalent(first.GenericTypeArguments, second.GenericTypeArguments))
+ if (!TypesAreAllEquivalent(first.GenericTypeArguments, second.GenericTypeArguments, genericArgumentsInclusionType))
{
return false;
}
continue;
}
+ var includeAreAssignable = assignableInclusionType == AreAssignableInclusionType.Include
+ || assignableInclusionType == AreAssignableInclusionType.ExcludeGenericArguments;
+
var areAssignable = first.IsAssignableFrom(second) || second.IsAssignableFrom(first);
var areAnyTypeAssignable = typeof(Arg.AnyType).IsAssignableFrom(first) ||
typeof(Arg.AnyType).IsAssignableFrom(second);
var areByRefAnyTypeAssignable = first.IsByRef && second.IsByRef &&
(typeof(Arg.AnyType).IsAssignableFrom(first.GetElementType()) ||
typeof(Arg.AnyType).IsAssignableFrom(second.GetElementType()));
- var areEquivalent = areAssignable || areAnyTypeAssignable || areByRefAnyTypeAssignable;
+ var areEquivalent = (includeAreAssignable && areAssignable) || areAnyTypeAssignable || areByRefAnyTypeAssignable;
if (!areEquivalent) return false;
}
return true;
@@ -102,7 +112,12 @@ internal static bool TypesAreAllEquivalent(Type[] aArgs, Type[] bArgs)
private static bool AreEquivalentDefinitions(MethodInfo a, MethodInfo b)
{
return a.IsGenericMethod == b.IsGenericMethod
- && TypesAreAllEquivalent([a.ReturnType], [b.ReturnType])
+
+ // 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)
+
&& a.Name.Equals(b.Name, StringComparison.Ordinal);
}
@@ -175,4 +190,25 @@ private bool HasDifferentNumberOfArguments(ICall call)
{
return _argumentSpecifications.Length != call.GetOriginalArguments().Length;
}
+
+ ///
+ /// Specifies whether an assignability check between types should be included for type equivalence checks.
+ ///
+ internal enum AreAssignableInclusionType
+ {
+ ///
+ /// Include the check
+ ///
+ Include,
+
+ ///
+ /// Exclude the check
+ ///
+ Exclude,
+
+ ///
+ /// Exclude the check for generic arguments, but include it for the current type
+ ///
+ ExcludeGenericArguments,
+ }
}
\ No newline at end of file
diff --git a/tests/NSubstitute.Acceptance.Specs/GenericArguments.cs b/tests/NSubstitute.Acceptance.Specs/GenericArguments.cs
index aa173e13..e8f6ab9d 100644
--- a/tests/NSubstitute.Acceptance.Specs/GenericArguments.cs
+++ b/tests/NSubstitute.Acceptance.Specs/GenericArguments.cs
@@ -1,7 +1,7 @@
+using NUnit.Framework;
using System.Collections;
using System.Globalization;
using System.Reflection;
-using NUnit.Framework;
namespace NSubstitute.Acceptance.Specs;
@@ -180,6 +180,24 @@ public void Returns_works_with_AnyType_for_ref_parameter_with_AnyType_generic_ar
Assert.That(result, Is.True);
}
+ ///
+ /// See https://github.com/nsubstitute/NSubstitute/issues/974.
+ ///
+ [Test]
+ public void Returns_works_with_mismatching_generic_return_types()
+ {
+ ISomethingWithGenerics something = Substitute.For();
+
+ something
+ .SomeFunction(Arg.Any())
+ .Returns(Substitute.For>());
+
+ something
+ .SomeFunction(Arg.Any())
+ .Returns(Substitute.For>());
+ }
+
+
[Test]
public void Callback_allows_access_to_method_call()
{
@@ -210,4 +228,4 @@ static ICollection CreateSubstitute(int count)
Assert.That(result.Count, Is.EqualTo(7));
}
-}
\ No newline at end of file
+}