在表达式中使用Func?

时间:2014-05-06 15:14:18

标签: c# lambda delegates func ormlite-servicestack

背景

我有一个测试通过的例子,但是在管道中发生的错误,我不知道为什么。我想知道发生了什么,但我是Expression构建的新手,不想做任何假设。

这是针对搜索过滤机制。它使用ServiceStack的PredicateBuilder实现。我基本上有一个我传入的值列表,我希望它构造一个表达式树。我之前使用Func<T<bool>>完成了此操作,但意识到我需要结束Expression<Func<T<bool>>>。长号。

目标

搜索由可重复使用的搜索过滤器类型构建的过滤器,这些过滤器类型由FuncExpression构建,允许我从对象传入字段名称以及我应该匹配的值最后我们可以对Where()语句进行操作。

代码/问题

我正在尝试的通用“可空bool”过滤器 - 设置可接受的项目并返回一个有助于过滤的函数:

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'类型的变量'param',但未定义

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

问题

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

1 个答案:

答案 0 :(得分:3)

  

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

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

  

为什么[...]我仍会收到此错误?

因为当你实际使用它时,它不只是执行代码;而是通过表达式树来查看代码正在做什么以便它可以被翻译成其他东西,而不是它可以作为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));
}