通过比较返回 lambda 表达式的扩展方法

Posted

技术标签:

【中文标题】通过比较返回 lambda 表达式的扩展方法【英文标题】:Extension method returning lambda expression through compare 【发布时间】:2012-05-22 21:52:26 【问题描述】:

我正在为我们这个庞大的项目创建一个更精细的过滤系统。主要谓词之一是能够通过字符串参数传递比较。这以以下形式表示:“>50”或“5-10”或“

我有什么(举例说明)

视图模型:

TotalCost (string) (value: "<50")
Required (string) (value: "5-10")

EF 型号:

TotalCost (double)
Required(double)

我想使用的表达方式:

model => model.Where(field => field.TotalCost.Compare(viewModel.TotalCost) && field.Required.Compare(viewModel.Required));

我想收到的表达:

model => model.Where(field => field.TotalCost < 50 && field.Required > 5 && field.Required < 10);

或类似的东西

但是...我不知道从哪里开始。我把范围缩小到了

public static Expression Compare<T>(this Expression<Func<T, bool>> value, string compare)

这可能甚至不正确,但这就是我所拥有的一切。比较构建器不是问题,这很容易。困难的部分实际上是返回表达式。我从未尝试过将表达式作为函数值返回。所以基本上我需要保留的是字段并返回一个比较表达式,差不多。

有什么帮助吗? :x

更新:

唉,这并不能解决我的问题。可能是因为我过去 23 小时一直在工作,但我对如何将其变成扩展方法一无所知。正如我所说,我想要的......基本上是一种写​​作方式:

var ex = new ExTest();
var items = ex.Repo.Items.Where(x => x.Cost.Compare("<50"));

我塑造该功能的方式(可能完全错误)是

public static Expression<Func<decimal, bool>> Compare(string arg)

    if (arg.Contains("<"))
        return d => d < int.Parse(arg);

    return d => d > int.Parse(arg);

它首先缺少要比较的“this -something- value”,而且我还没有设法弄清楚如何让它能够获得表达式输入......至于 ReSharper,它建议我将其转换为布尔值...

此刻我的脑袋里满是绒毛……

更新 2:

我设法找到一种方法,让一段代码在控制台应用程序的内存存储库中工作。不过,我还没有尝试使用实体框架。

public static bool Compare(this double val, string arg)
    
        var arg2 = arg.Replace("<", "").Replace(">", "");
        if (arg.Contains("<"))
            return val < double.Parse(arg2);

        return val > double.Parse(arg2);
    

但是,我非常怀疑这就是我所追求的

更新 3:

对,坐下来再次查看 lambda 表达式后,在最后一个答案之前,我想出了类似于以下内容的东西,它不符合“Compare()”的确切要求,但它是一个“重载” -ish' Where 方法:

public static IQueryable<T> WhereExpression<T>(this IQueryable<T> queryable, Expression<Func<T, double>> predicate, string arg)
    
        var lambda =
            Expression.Lambda<Func<T, bool>>(Expression.LessThan(predicate.Body, Expression.Constant(double.Parse(50.ToString()))));

        return queryable.Where(lambda);
    

然而,尽管在我看来,一切看起来都合乎逻辑,但我得到了运行时异常:

System.ArgumentException was unhandled
  Message=Incorrect number of parameters supplied for lambda declaration
  Source=System.Core
  StackTrace:
       at System.Linq.Expressions.Expression.ValidateLambdaArgs(Type delegateType, Expression& body, ReadOnlyCollection`1 parameters)
       at System.Linq.Expressions.Expression.Lambda[TDelegate](Expression body, String name, Boolean tailCall, IEnumerable`1 parameters)
       at System.Linq.Expressions.Expression.Lambda[TDelegate](Expression body, Boolean tailCall, IEnumerable`1 parameters)
       at System.Linq.Expressions.Expression.Lambda[TDelegate](Expression body, ParameterExpression[] parameters)

这显然是罪魁祸首:

var lambda =
                Expression.Lambda<Func<T, bool>>(Expression.LessThan(predicate.Body, Expression.Constant(double.Parse(50.ToString()))));

我非常接近解决方案。如果我能摆脱这个错误,我相信 EF 应该能够将它翻译成 SQL。否则……好吧,最后一个响应可能会消失。

【问题讨论】:

我认为,您的 update2 部分不会针对 SQL Server (EF) 执行。你试过了吗? 是的,刚刚做到了。正如我所想的那样。 【参考方案1】:

要生成将被转换为 SQL (eSQL) 的表达式,您应该手动生成 Expression。这是GreaterThan过滤器创建的示例,其他过滤器可以用类似的技术制作。

static Expression<Func<T, bool>> CreateGreaterThanExpression<T>(Expression<Func<T, decimal>> fieldExtractor, decimal value)

    var xPar = Expression.Parameter(typeof(T), "x");
    var x = new ParameterRebinder(xPar);
    var getter = (MemberExpression)x.Visit(fieldExtractor.Body);
    var resultBody = Expression.GreaterThan(getter, Expression.Constant(value, typeof(decimal)));
    return Expression.Lambda<Func<T, bool>>(resultBody, xPar);


private sealed class ParameterRebinder : ExpressionVisitor

    private readonly ParameterExpression _parameter;

    public ParameterRebinder(ParameterExpression parameter)
     this._parameter = parameter; 

    protected override Expression VisitParameter(ParameterExpression p)
     return base.VisitParameter(this._parameter); 

这里是使用示例。 (假设我们有 StackEntites EF 上下文和 TestEntity 实体的实体集 TestEnitities)

static void Main(string[] args)

    using (var ents = new StackEntities())
    
        var filter = CreateGreaterThanExpression<TestEnitity>(x => x.SortProperty, 3);
        var items = ents.TestEnitities.Where(filter).ToArray();
    

更新: 为了创建复杂的表达式,您可以使用如下代码: (假设已经做了CreateLessThanExpressionCreateBetweenExpression函数)

static Expression<Func<T, bool>> CreateFilterFromString<T>(Expression<Func<T, decimal>> fieldExtractor, string text)

    var greaterOrLessRegex = new Regex(@"^\s*(?<sign>\>|\<)\s*(?<number>\d+(\.\d+)0,1)\s*$");
    var match = greaterOrLessRegex.Match(text);
    if (match.Success)
    
        var number = decimal.Parse(match.Result("$number"));
        var sign = match.Result("$sign");
        switch (sign)
        
            case ">":
                return CreateGreaterThanExpression(fieldExtractor, number);
            case "<":
                return CreateLessThanExpression(fieldExtractor, number);
            default:
                throw new Exception("Bad Sign!");
        
    

    var betweenRegex = new Regex(@"^\s*(?<number1>\d+(\.\d+)0,1)\s*-\s*(?<number2>\d+(\.\d+)0,1)\s*$");
    match = betweenRegex.Match(text);
    if (match.Success)
    
        var number1 = decimal.Parse(match.Result("$number1"));
        var number2 = decimal.Parse(match.Result("$number2"));
        return CreateBetweenExpression(fieldExtractor, number1, number2);
    
    throw new Exception("Bad filter Format!");

【讨论】:

这似乎更有用。我再次更新了原始问题,使用我自己的部分解决方案......好吧,仍然无法正常工作。 我现在已经在过滤方法中实现了这个功能。不过,EF 有一些问题,首先“getter”不能在它前面加上 (MemberExpression)。只会例外。其次,数字正则表达式(这是最容易修复的)略有偏差,应该是 (?\d+(\.\d*)?),你帖子中的那个说 requires 此处为“.”。它不满足于非十进制值。但除此之外(y) 关于正则表达式:没错。使用 0,1 量词更新答案(与 ? 相同)。 关于MemberExpression - 尝试了这段代码,就像一个魅力。 var filter = CreateFilterFromString&lt;TestEnitity&gt;(x =&gt; x.SortProperty, "&gt;2"); 能否举个例子,什么时候引发异常? 我相信这个问题是由实体框架引发的。在通用存储库测试中,它对我来说也很好。【参考方案2】:

C# 编译器的一个乍一看神奇的功能可以为您完成繁重的工作。你可能知道你可以这样做:

Func<decimal, bool> totalCostIsUnder50 = d => d < 50m;

也就是说,使用 lambda 表达式来分配 Func。但是你知道你可以这样做:

Expression<Func<decimal, bool>> totalCostIsUnder50Expression = d => d < 50m;

也就是说,使用一个 lambda 表达式来分配一个 Expression 来表达一个 Func?挺好看的。

如你所说

比较生成器不是问题,这很简单。艰难的 部分实际上是返回表达式

我假设您可以在这里填写空白;假设我们将 `"

Expression<Func<decimal, bool>> TotalCostCheckerBuilder(string criterion)

    // Split criterion into operator and value

    // when operator is < do this:
    return d => d < value;

    // when operator is > do this:
    return d => d > value;

    // and so on

最后,要将Expressions 和&amp;&amp; 组合在一起(并且还有Expression),请执行以下操作:

var andExpression = Expression.And(firstExpression, secondExpression);

【讨论】:

不确定它是否适用于这种情况。用 cmets 更新了原始帖子。【参考方案3】:

困难的部分实际上是返回表达式。

将字符串转换为更结构化的结构,如枚举和类,以定义属性、运算符和过滤器:

Enum Parameter
    TotalCost
    Required
End Enum

Enum Comparator
    Less
    More
    Equals
End Enum

Class Criterion
    Public ReadOnly Parameter As Parameter
    Public ReadOnly Comparator As Comparator
    Public ReadOnly Value As Double

    Public Sub New(Parameter As Parameter, Comparator As Comparator, Value As Double)
        Me.Parameter = Parameter
        Me.Comparator = Comparator
        Me.Value = Value
    End Sub
End Class

然后定义一个创建表达式的函数:

Function CreateExpression(Criteria As IEnumerable(Of Criterion)) As Expression(Of Func(Of Field, Boolean))
    Dim FullExpression = PredicateBuilder.True(Of Field)()

    For Each Criterion In Criteria
        Dim Value = Criterion.Value

        Dim TotalCostExpressions As New Dictionary(Of Comparator, Expression(Of Func(Of Field, Boolean))) From 
            Comparator.Less, Function(Field) Field.TotalCost < Value,
            Comparator.More, Function(Field) Field.TotalCost > Value,
            Comparator.Equals, Function(Field) Field.TotalCost = Value
        

        Dim RequiredExpressions As New Dictionary(Of Comparator, Expression(Of Func(Of Field, Boolean))) From 
            Comparator.Less, Function(Field) Field.Required < Value,
            Comparator.More, Function(Field) Field.Required > Value,
            Comparator.Equals, Function(Field) Field.Required = Value
        

        Dim Expressions As New Dictionary(Of Parameter, IDictionary(Of Comparator, Expression(Of Func(Of Field, Boolean)))) From 
            Parameter.TotalCost, TotalCostExpressions,
            Parameter.Required, RequiredExpressions

        Dim Expression = Expressions(Criterion.Parameter)(Criterion.Comparator)

        FullExpression = Expression.And(Expression)
    Next

    Return FullExpression
End Function

PredicateBuilder 取here 需要用AND 运算符组合两个表达式。

用法:

Function Usage() As Integer

    Dim Criteria = 
        New Criterion(Parameter.TotalCost, Comparator.Less, 50),
        New Criterion(Parameter.Required, Comparator.More, 5),
        New Criterion(Parameter.Required, Comparator.Less, 10)

    Dim Expression = CreateExpression(Criteria)
End Function

它将创建与示例中提供的完全一样的表达式

field => field.TotalCost < 50 && field.Required > 5 && field.Required < 10

【讨论】:

以上是关于通过比较返回 lambda 表达式的扩展方法的主要内容,如果未能解决你的问题,请参考以下文章

使用Lambda表达式扩展List.Sort()

为拓展方法传递lambda表达式

编写高质量代码改善C#程序的157个建议——建议27:在查询中使用Lambda表达式

Lambda表达式

Lambda表达式动态组装查询条件

lambda表达式之进化