NSubstitute icon indicating copy to clipboard operation
NSubstitute copied to clipboard

Difference in Received() check between array params and new C# 13 params

Open dusk0r opened this issue 2 months ago • 0 comments

C# 13 allows using params with many collection types. https://devblogs.microsoft.com/dotnet/csharp13-calling-methods-is-easier-and-faster/

Example:

void DoSomething2(params IEnumerable<object> values) {}

DoSomething2(new object(), new object());

The compiler will automatically create the required collection. This causes an inconsistency between checking with classic array params and the new collections params:

public interface ITest
{
    void DoSomething(params object[] args);
    void DoSomething2(params IEnumerable<object> values); // valid since C# 13
}

public class Test
{
    public static void Run()
    {
        var obj = new Object();
        
        var substitute = Substitute.For<ITest>();
        substitute.DoSomething(obj);
        substitute.DoSomething2(obj);
 
        // Works
        substitute.Received().DoSomething(obj);
        // Fails 
        substitute.Received().DoSomething2(obj);
        // Works, but verbose
        substitute.Received()
            .DoSomething2(Arg.Is<IEnumerable<object>>(args => args.SequenceEqual(new List<object> { obj })));
        
        /*
         * NSubstitute.Exceptions.ReceivedCallsException: Expected to receive a call matching:
         *       DoSomething2(<>z__ReadOnlySingleElementList<Object>)
         * Actually received no matching calls.
         * Received 1 non-matching call (non-matching arguments indicated with '*' characters):
         *       DoSomething2(*<>z__ReadOnlySingleElementList<Object>*)
         */
    }
}

I wouldn't call it a bug but this is certainly unexpected. Is there a simpler way to do the check?

dusk0r avatar Sep 16 '25 14:09 dusk0r