2

Is it possible to pass a method group as an argument in C#?

I am using a lot of mocks. I have the following:

public static List<IInvocation> InvocationsWithName<T>(
    this Mock<T> mock, string methodName)
    where T: class
{
    var invocations = mock.Invocations
        .Where(i => i.Method.Name == methodName)
        .ToList();

    return invocations;
}

I use it like this:

var invocations = myInterface
    .InvocationsWithName(nameof(MyInterface.MyMethod));

It works. But I would prefer to use it like this:

var invocations = myInterface
    .InvocationsWithName(MyInterface.MyMethod);

To do that, I need to define my InvocationsWithName extension method in a way that accepts MyInterface.MyMethod as the argument. But I don't know if/how that can be done.

In practice, MyMethod might be a single method, or it might be a method group with multiple members. Ideally, the solution would work in both cases.

Ivan Gechev
  • 706
  • 3
  • 9
  • 21
William Jockusch
  • 26,513
  • 49
  • 182
  • 323
  • 3
    just use a `Func` instead of a methodName – MakePeaceGreatAgain Oct 18 '22 at 14:45
  • @MakePeaceGreatAgain When I do that and try to call it, I get a compile error: "cannot convert from 'method group' to 'string'" – William Jockusch Oct 18 '22 at 15:30
  • 1
    @WilliamJockusch The second parameter of your method `InvocationsWithName` is a string. If you want to pass something else than a string, you have to change the method. I can't think of a defintion, that would accept methods with arbitrary signatures as a parameter. – Jürgen Röhr Oct 18 '22 at 16:14

2 Answers2

2

Your description is a bit vague, but it screams for expressions:

static List<IInvocation> InvocationsWithName<TInvocations, TClass, TMethod>(
    this TInvocations mock, TClass @class, Expression<Func<TClass, TMethod>> selector)
    where TInvocations : class, IInvocationsContainer
    where TClass : class
{
    var invocations = mock.Invocations
        .Where(i => i.Method.Name == ((MemberExpression)selector.Body).Member.Name).ToList();
    return invocations;
}

public static void Main()
{
    // called like this
    var invocations = InvocationsWithName(new InvocationsContainer(), new C(), c => c.F);
}

Where C is the class that contains the function who's name you want to get:

class C
{
    public int F { get; set; }
}
Peter Csala
  • 17,736
  • 16
  • 35
  • 75
Blindy
  • 65,249
  • 10
  • 91
  • 131
  • That appears to make calling the extension method more complicated. My goal is the highest possible ease of calling in the cases I frequently use. – William Jockusch Oct 18 '22 at 15:26
  • Perhaps, but safer. You can't give this a random string without a compiler error now. – Blindy Oct 18 '22 at 17:26
  • @WilliamJockusch actually calling this is pretty easy. In your calling code you jst have this lambda: `c => c.F`. Providing just a string may introduce lots of errors, e.g. the member may not exist. – MakePeaceGreatAgain Oct 19 '22 at 07:15
0

Based on Blindy's solution I would suggest a slightly modified version:

public static List<IInvocation> InvocationsByMethodSelector<T>(this Mock<T> mock, Expression<Action<T>> methodSelector)
where T : class
    => mock.Invocations
        .Where(i => i.Method.MethodHandle == ((MethodCallExpression)methodSelector.Body).Method.MethodHandle)
        .ToList();

Rather than performing string comparison (...Name == ...Name) here we comparing two RuntimeMethodHandle structs (...MethodHandle == ...MethodHandle)

In case of Blindy's solution the parameter passing looks like this

myInterface.InvocationsWithName(m => m.MyMethod);

We can't use this because in that case we need to use the Expression<Func<T, Action>> for methodSelector parameter.

Here the Expression's Body is an UnaryExpression. It does have a Method property, but it is null in our case. Its Operand property contains the necessary information, but we can't cast that from Expression to InstanceMethodCallExpressionN, because that class is internal.

So, we need to use MethodCallExpression instead which requires us to do the parameter passing like this:

myInterface.InvocationsByMethodSelector(m => m.MyMethod());
Peter Csala
  • 17,736
  • 16
  • 35
  • 75