Alex Krupka
Alex Krupka

Reputation: 730

Is it possible to create a generic method that take a method group as an argument

I'm trying to create a method that would allow a "method group" to be passed in. Basically this is for testing where I want to make sure that a method was called. FakeItEasy already allows for the following code

public static IAnyCallConfigurationWithReturnTypeSpecified<ReturnType> ReturnForMethod<SUT, ReturnType>(
          this Fake<SUT> fake,
           string methodName, ReturnType returnObj)
      {
          var call = fake.AnyCall().WithReturnType<ReturnType>().Where(s => s.Method.Name == methodName);

          call.Returns(returnObj);
          return call;
      }

//Called using 
new Fake<IDummyFactory>().ReturnForMethod(nameof(IDummyFactory.Create), testStr);

For cleaner code I'd prefer to write something like

public static IAnyCallConfigurationWithReturnTypeSpecified<ReturnType> ReturnForMethodF<SUT ,ReturnType>(
            this Fake<SUT> fake,
            MethodGroup method, ReturnType returnObj)

Basically the same way nameof works. Using a function so I would want the method called as follows

new Fake<IDummyFactory>().ReturnForMethod(s=> s.Create, testStr); --NoInvoke as just want method group

Upvotes: 0

Views: 257

Answers (1)

V0ldek
V0ldek

Reputation: 10573

"Method group" is a concept that exists only in the language's syntax, there is no runtime type that corresponds to a method group.

Before I answer the question, you can just do this at callsite:

new Fake<IDummyFactory>().ReturnForMethod(nameof(s.Create), testStr);

I don't see any reason s => s.Create would be better or more readable than nameof(s.Create).

Now for the fun part. Method groups are convertible to compatible delegate types as governed by an impossibly complicated set of magic rules.

So we would be looking for a method with a signature of:

public static IAnyCallConfigurationWithReturnTypeSpecified<TReturnType> ReturnForMethod<TSUT, TDelegate, TReturnType>(
    this Fake<TSUT> fake,
    Expression<Func<TSUT, TDelegate>> methodGroupExpression,
    TReturnType returnObject) where TSUT : class 
                              where TDelegate : Delegate

We're working with Expressions, so we will have to create an ExpressionVisitor that traverses the Expression tree and looks for method calls on the appropriate type. For simplicity we are going to assume that you are always calling the method with just a single x => x.Foo expression. Assuming we have a visitor class that does this, the body of our method is straightforward:

public static IAnyCallConfigurationWithReturnTypeSpecified<TReturnType> ReturnForMethod<TSUT, TDelegate, TReturnType>(
    this Fake<TSUT> fake,
    Expression<Func<TSUT, TDelegate>> methodGroupExpression,
    TReturnType returnObject) where TSUT : class 
                                where TDelegate : Delegate
{
    var visitor = new ExtractMethodNameExpressionVisitor<TSUT>();
    var methodName = visitor.ExtractMethodName(methodGroupExpression);

    if (methodName is null)
    {
        throw new InvalidOperationException();
    }

    var call = fake.AnyCall().WithReturnType<TReturnType>().Where(s => s.Method.Name == methodName);

    call.Returns(returnObject);
    return call;
}

The expression s => s.Create is enriched with some magical compiler generated code during compilation, which converts it to the appropriate delegate type. But the method group itself is nested somewhere in it as a ConstantExpression of type MethodInfo. Our visitor class will traverse the tree, find such an expression and save its name.

class ExtractMethodNameExpressionVisitor<T> : ExpressionVisitor
{
    private string? _methodName;

    public string? ExtractMethodName(Expression expression)
    {
        _methodName = null;

        this.Visit(expression);
        return _methodName;
    }

    protected override Expression VisitConstant(ConstantExpression node)
    {
        if (node.Type == typeof(MethodInfo) && 
            node.Value is MethodInfo methodInfo &&
            methodInfo.DeclaringType == typeof(T))
        {
            _methodName = methodInfo.Name;
        }

        return base.VisitConstant(node);
    }
}

And now we can test this:

var fake = new Fake<IDummyFactory>();

fake.ReturnForMethod(s => s.Create, "testStr");

var result = fake.FakedObject.Create();

Console.WriteLine(result);

The unfortunate part is that this doesn't work. As of C#9 at least, delegate types are kind of a second-class citizen in the language, and generic type inference for them does not work. So the above ReturnForMethod call does not compile, because the compiler cannot infer that a function returning string and taking no arguments is actually Func<string>. So the callsite must look like this:

fake.ReturnForMethod<IDummyFactory, Func<string>, string>(s => s.Create, "testStr");

which makes this solution much less appealing. The good news is that, if I understand the proposals correctly, in C#10 (or maybe C#11 if the feature misses C#10's release window) this would work without the explicit type signature.

You can find the full working demo here.

Upvotes: 3

Related Questions