是否可以创建一个将方法组作为参数的通用方法

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

我正在尝试创建一个允许传入“方法组”的方法。基本上这是为了测试我想确保调用了一个方法的位置。 FakeItEasy 已经允许使用以下代码

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);

为了更简洁的代码,我更愿意编写类似

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

基本上与 nameof 的工作方式相同。使用一个函数,所以我希望按如下方式调用该方法

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

“方法组”是一个只存在于语言语法中的概念,不存在与方法组相对应的运行时类型。

在我回答问题之前,你可以在调用站点这样做:

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

我看不出有任何理由 s => s.Create 会比 nameof(s.Create) 更好或更具可读性。

现在是有趣的部分。方法组可转换为兼容的委托类型 as governed by an impossibly complicated set of magic rules.

所以我们要寻找一个签名为:

的方法
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

我们正在使用 Expressions,所以我们必须创建一个 ExpressionVisitor 来遍历 Expression 树并查找适当类型的方法调用。为简单起见,我们假设您始终只使用一个 x => x.Foo 表达式调用该方法。假设我们有一个访问者 class 执行此操作,我们方法的主体很简单:

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;
}

表达式 s => s.Create 在编译期间用一些神奇的编译器生成的代码进行了丰富,这些代码将其转换为适当的委托类型。但是方法组本身作为 MethodInfo 类型的 ConstantExpression 嵌套在其中的某处。我们的访问者 class 将遍历树,找到这样的表达式并保存它的名称。

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);
    }
}

现在我们可以测试一下:

var fake = new Fake<IDummyFactory>();

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

var result = fake.FakedObject.Create();

Console.WriteLine(result);

不幸的是这不起作用。至少从 C#9 开始,委托类型是语言中的第二个 class 公民,对它们的泛型类型推断不起作用。所以上面的 ReturnForMethod 调用不会编译,因为编译器无法推断出返回 string 且不带参数的函数实际上是 Func<string>。所以调用站点必须如下所示:

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

这使得这个解决方案的吸引力大打折扣。好消息是,如果我正确理解这些建议,在 C#10 中(或者可能是 C#11,如果该功能错过了 C#10 的发布 window),这将在没有显式类型签名的情况下工作。

您可以找到完整的工作演示 here