在表达式中使用函数

本文关键字:函数 表达式 | 更新日期: 2023-09-27 18:02:03

Background

有一个通过测试的示例,但管道中发生了错误,我不确定为什么。我想弄清楚发生了什么,但我是表达式构造的新手,不想做出任何假设。

这适用于搜索筛选机制。它使用 ServiceStack 的 PredicateBuilder 实现。我基本上有一个我传入的值列表,我希望它构造一个表达式树。我以前只和Func<T<bool>>一起做过这件事,但意识到我需要结束Expression<Func<T<bool>>>。无赖。

目标

从可重用搜索过滤器类型构建的搜索过滤器,该过滤器由 Func s 和 Expression s 构建而成,允许我从对象传入字段名称以及我应该匹配的值,并最终得到我们可以对其运行 Where() 语句的内容。

守则/问题

我正在尝试的通用"可为空的布尔"过滤器 - 设置可接受的项目并返回一个旨在帮助过滤的 func:

public class NullableBoolFilter : IGenericSearchFilter<bool?>
{
    public Func<bool?, bool> GetFilterFunc(string valuesToProcess)
    {
        var acceptableValues = new List<bool?>();
        if (string.IsNullOrWhiteSpace(valuesToProcess))
        {
            // all values acceptable
            acceptableValues = new List<bool?>{true, false, null};
        }
        else
        {
            if (!valuesToProcess.Contains("0") && !valuesToProcess.Contains("1"))
            {
                throw new ArgumentException("Invalid Nullable boolean filter attribute specified");
            }
            if (valuesToProcess.Contains("0"))
            {
                acceptableValues.Add(false);
            }
            if (valuesToProcess.Contains("1"))
            {
                acceptableValues.Add(true);
            }
        }
        Func<bool?, bool> returnFunc = delegate(bool? item) { return acceptableValues.Any(x=>x == item); };
        return returnFunc;
    }
}

然后我有另一个过滤器,它继承自NullableBoolFilter并尝试使用 Func:

public class ClaimsReportIsMDLFilter : NullableBoolFilter, ISearchFilter<vSEARCH_ClaimsReport>
{
    public Expression<Func<vSEARCH_ClaimsReport, bool>> GetExpression(string valuesToProcess)
    {
        var theFunc = base.GetFilterFunc(valuesToProcess);
        Expression<Func<vSEARCH_ClaimsReport, bool>> mdlMatches = item => theFunc(item.IsMDL);
        var predicate = PredicateBuilder.False<vSEARCH_ClaimsReport>();
        predicate = predicate.Or(mdlMatches);
        return predicate;
    }
}

以下测试通过:

public class ClaimsReportIsMDLFilterTests
{
    // ReSharper disable InconsistentNaming
    private readonly vSEARCH_ClaimsReport ItemWithMDL = new vSEARCH_ClaimsReport { IsMDL = true };
    private readonly vSEARCH_ClaimsReport ItemWithoutMDL = new vSEARCH_ClaimsReport { IsMDL = false };
    private readonly vSEARCH_ClaimsReport ItemWithNullMDL = new vSEARCH_ClaimsReport { IsMDL = null };
    // ReSharper restore InconsistentNaming
    [Fact]
    public void WithSearchValueOf1_HidesNonMDLAndNull()
    {
        var sut = this.GetCompiledExpressionForValues("1");
        sut.Invoke(ItemWithMDL).Should().BeTrue();
        sut.Invoke(ItemWithoutMDL).Should().BeFalse();
        sut.Invoke(ItemWithNullMDL).Should().BeFalse();
    }
    private Func<vSEARCH_ClaimsReport, bool> GetCompiledExpressionForValues(string searchValue)
    {
        return new ClaimsReportIsMDLFilter().GetExpression(searchValue).Compile();
    }
}

问题所在

当我实际尝试运行它时,我收到错误:

类型为"vSEARCH_ClaimsReport"的变量"参数"从范围"引用,但未定义

对我来说,为什么会发生这种情况是有道理的——在评估它时,我没有一个真正的对象可以传递到Func中。但是,我很困惑为什么我的测试可能会通过,但这在实际使用中没有。

问题

  • 为什么我的测试可能通过,但仍然收到此错误?
  • 我到底应该如何开始尝试解决这个问题?
  • 有没有一种非常简单的方法可以将该Func变成我可以传递字段的Expression
  • 我是否需要放弃通用过滤器的想法,并让每个类根据传入的输入手动将表达式添加到PredicateBuilder?这是可行的,但似乎工作量可以减少更多。

在表达式中使用函数

为什么我的测试可能会通过 [...]

因为您的测试只是将表达式编译到它表示的代码中并调用它。它不需要实际解析表达式树并查看它所代表的代码正在做什么,它只需要运行它并确保输出是正确的。

为什么可能 [...]我仍然收到此错误?

因为当你实际使用它时,它不仅仅是执行代码;而是查看表达式树,试图确定代码在做什么,以便它可以被转换为其他内容,而不是它可以作为 C# 代码运行。

您的表达式除了呼叫代表外什么都不做。 遍历表达式树的人无法看到委托内部并知道它在做什么。 知道您正在调用另一种方法并不能翻译成另一种语言。

我到底应该如何开始尝试解决这个问题?

您需要从一开始就生成一个Expression,而不是生成一个Func然后创建一个调用它的Expression

有没有一种远程简单的方法可以将该 Func 转换为我可以将字段传递到的表达式?

不。 需要提取函数的 IL 代码,将其反编译为 C# 代码,然后生成Expression对象来表示该代码。 这几乎不会发生。


你几乎需要GetFilterFunc返回一个Expression,才能让它工作。 幸运的是,鉴于您所拥有的,这很容易做到。 您只需更改方法签名并将最后两行替换为以下内容:

return item => acceptableValues.Any(x => x == item);

瞧。 lambda 可以根据上下文编译为Expression对象,而不是委托,因此,如果该方法的返回类型是Expression<Func<bool?,bool>>则您将获得。

现在,在GetExpression中使用它. 首先,PredicateBuilder并没有真正做任何事情。 在表达式中添加 OR FALSE 不会改变任何有意义的内容。 所有这些都可以去。 留给我们的只是使用Expression<Func<bool?,bool>>并通过拉出布尔属性将其更改为Expression<Func<vSEARCH_ClaimsReport, bool>>。 要做到这一点,表达式的工作比委托要多一些。 我们不只是调用表达式,还需要做更多的工作来组成它们。 我们需要编写一个方法来执行此操作:

public static Expression<Func<TFirstParam, TResult>>
    Compose<TFirstParam, TIntermediate, TResult>(
    this Expression<Func<TFirstParam, TIntermediate>> first,
    Expression<Func<TIntermediate, TResult>> second)
{
    var param = Expression.Parameter(typeof(TFirstParam), "param");
    var newFirst = first.Body.Replace(first.Parameters[0], param);
    var newSecond = second.Body.Replace(second.Parameters[0], newFirst);
    return Expression.Lambda<Func<TFirstParam, TResult>>(newSecond, param);
}

这依赖于使用以下方法将一个表达式的所有实例替换为另一个表达式:

public static Expression Replace(this Expression expression,
    Expression searchEx, Expression replaceEx)
{
    return new ReplaceVisitor(searchEx, replaceEx).Visit(expression);
}
internal class ReplaceVisitor : ExpressionVisitor
{
    private readonly Expression from, to;
    public ReplaceVisitor(Expression from, Expression to)
    {
        this.from = from;
        this.to = to;
    }
    public override Expression Visit(Expression node)
    {
        return node == from ? to : base.Visit(node);
    }
}

这样做是将第二个表达式参数的所有实例替换为第一个表达式的主体,从而有效地将该表达式内联到第二个表达式中。 其余的只是用一个新的单个参数替换所有参数,然后将其包装回 lambda 中。

现在我们有了这个,我们的方法非常简单:

public Expression<Func<vSEARCH_ClaimsReport, bool>> GetExpression(
    string valuesToProcess)
{
    Expression<Func<vSEARCH_ClaimsReport, bool?>> selector = 
        item => item.IsMDL;
    return selector.Compose(base.GetFilterFunc(valuesToProcess));
}