如何在.NET中创建表达式构建器

问题描述 投票:5回答:4

我已经编写了一些代码来允许在我们的网站上过滤产品,而且我的代码味道非常糟糕。用户可以选择1- *这些过滤器,这意味着我需要特定于WHERE子句。

我想我正在寻找一种构建lambda表达式的方法,所以对于每个过滤器,我都可以“修改”我的WHERE子句 - 但我不确定如何在.NET中执行此操作,并且必须有一种方法。

处于当前状态的代码(实际上是硬编码的,而不是动态的,添加更多过滤器选项会很麻烦)。

public static class AgeGroups
{
    public static Dictionary<string, int> Items = new Dictionary<string, int>(){
        { "Modern (Less than 10 years old)", 1 },
        { "Retro (10 - 20 years old)", 2 },
        { "Vintage(20 - 70 years old)", 3 },
        { "Antique(70+ years old)", 4 }
    };

    public static IQueryable<ProductDTO> FilterAgeByGroup(IQueryable<ProductDTO> query, List<string> filters)
    {
        var values = new List<int>();
        var currentYear = DateTime.UtcNow.Year;
        foreach (var key in filters)
        {
            var matchingValue = Items.TryGetValue(key, out int value);

            if (matchingValue)
            {
                values.Add(value);
            }
        }

        if (Utility.EqualsIgnoringOrder(values, new List<int> { 1 }))
        {
            query = query.Where(x => x.YearManufactured >= currentYear - 10);
        }
        else if (Utility.EqualsIgnoringOrder(values, new List<int> { 2 }))
        {
            query = query.Where(x => x.YearManufactured <= currentYear - 10 && x.YearManufactured >= currentYear - 20);
        }
        else if (Utility.EqualsIgnoringOrder(values, new List<int> { 3 }))
        {
            query = query.Where(x => x.YearManufactured <= currentYear - 20 && x.YearManufactured >= currentYear - 70);
        }
        else if (Utility.EqualsIgnoringOrder(values, new List<int> { 4 }))
        {
            query = query.Where(x => x.YearManufactured <= currentYear - 70);
        }
        else if (Utility.EqualsIgnoringOrder(values, new List<int> { 1, 2}))
        {
            query = query.Where(x => x.YearManufactured >= currentYear - 20);
        }
        else if (Utility.EqualsIgnoringOrder(values, new List<int> { 1, 3 }))
        {
            query = query.Where(x => x.YearManufactured >= currentYear - 10 || (x.YearManufactured <= currentYear - 20 && x.YearManufactured >= currentYear - 70));
        }
        else if (Utility.EqualsIgnoringOrder(values, new List<int> { 1, 4 }))
        {
            query = query.Where(x => x.YearManufactured >= currentYear - 10 ||  x.YearManufactured <= currentYear - 70);
        }
        else if (Utility.EqualsIgnoringOrder(values, new List<int> { 2, 3 }))
        {
            query = query.Where(x => x.YearManufactured <= currentYear - 10 && x.YearManufactured >= currentYear - 70);
        }
        else if (Utility.EqualsIgnoringOrder(values, new List<int> { 2, 4 }))
        {
            query = query.Where(x => (x.YearManufactured <= currentYear - 10 && x.YearManufactured >= currentYear - 20) 
                                     || x.YearManufactured <= currentYear - 70);
        }
        else if (Utility.EqualsIgnoringOrder(values, new List<int> { 1, 2, 3 }))
        {
            query = query.Where(x => x.YearManufactured >= currentYear - 70);
        }
        else if (Utility.EqualsIgnoringOrder(values, new List<int> { 1, 2, 4 }))
        {
            query = query.Where(x => x.YearManufactured >= currentYear - 20 || x.YearManufactured <= currentYear - 70);
        }
        else if (Utility.EqualsIgnoringOrder(values, new List<int> { 2, 3, 4}))
        {
            query = query.Where(x => x.YearManufactured <= currentYear - 10);
        }
        else if (Utility.EqualsIgnoringOrder(values, new List<int> { 1, 3, 4}))
        {
            query = query.Where(x => x.YearManufactured >= currentYear - 10 || x.YearManufactured <= 20);
        }
        else if (Utility.EqualsIgnoringOrder(values, new List<int> { 1, 2, 3, 4 }))
        {
            // all
        }
        return query;
    }
}
c# .net linq iqueryable linq-expressions
4个回答
3
投票

我最近自己遇到过这个问题。通过SO的另一个问题的帮助我找到了http://www.albahari.com/nutshell/predicatebuilder.aspx。基本上你想构建一个谓词并将其传递给查询的where子句。

public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> where1, 
     Expression<Func<T, bool>> where2)
{
    InvocationExpression invocationExpression = Expression.Invoke(where2, 
         where1.Parameters.Cast<Expression>());
    return Expression.Lambda<Func<T, bool>>(Expression.OrElse(where1.Body, 
         invocationExpression), where1.Parameters);
}

public static IQueryable<ProductDTO> FilterAgeByGroup(IQueryable<ProductDTO> query,  
   List<string> filters, int currentYear)
{
    var values = new HashSet<int>();
    //Default value
    Expression<Func<ProductDTO, bool>> predicate = (ProductDTO) => false;

    foreach (var key in filters)
    {
        var matchingValue = Items.TryGetValue(key, out int value);

        if (matchingValue)
        {
            values.Add(value);
        }
    }

    if (values.Count == 0)
        return query;

    if (values.Contains(1))
    {
        predicate = predicate.Or(x => x.YearManufactured >= currentYear - 10);
    }

    if (values.Contains(2))
    {
        predicate = predicate.Or(x => x.YearManufactured <= currentYear - 10 && 
            x.YearManufactured >= currentYear - 20);
    }

    if (values.Contains(3))
    {
        predicate = predicate.Or(x => x.YearManufactured <= currentYear - 20 && 
            x.YearManufactured >= currentYear - 70);
    }

    if (values.Contains(4))
    {
        predicate = predicate.Or(x => x.YearManufactured <= currentYear - 70);
    }

    return query.Where(predicate);
}

3
投票

看起来你面临着组合爆炸。您可以使用修改后的Items集合静态声明简单案例:

static Dictionary<string, Expression<Func<int, int, bool>>> Items
   = new Dictionary<string, Expression<Func<int, int, bool>>>
{
    {
      "Modern (Less than 10 years old)",
      (yearManufactured, currentYear) => yearManufactured >= currentYear - 10
    },
    {
      "Retro (10 - 20 years old)",
      (yearManufactured, currentYear) => yearManufactured <= currentYear - 10 && yearManufactured >= currentYear - 20
    },
    {
      "Vintage(20 - 70 years old)",
      (yearManufactured, currentYear) => yearManufactured <= currentYear - 20 && yearManufactured >= currentYear - 70
    },
    {
      "Antique(70+ years old)",
      (yearManufactured, currentYear) => yearManufactured <= currentYear - 70
    }
};

现在,您可以将简单案例与Linq Expression OrElse动态组合。试试这段代码:

public static IQueryable<ProductDTO> FilterAgeByGroup(
  IQueryable<ProductDTO> query, List<string> filters)
{
    var conditions = new List<Expression>();
    foreach (var key in filters)
        if (Items.TryGetValue(key, out Expression<Func<int, int, bool>> value))
            conditions.Add(value);

    // return as is if there no conditions
    if (!conditions.Any())
        return query;

    var x = Expression.Parameter(typeof(ProductDTO), "x");
    var yearManufactured = Expression.PropertyOrField(x, "YearManufactured");
    var currentYear = Expression.Constant(DateTime.UtcNow.Year);
    var body = conditions.Aggregate(
        (Expression) Expression.Constant(false), // ignore item by default
        (c, n) => Expression.OrElse(c, Expression.Invoke(n, yearManufactured, currentYear)));

    var lambda = Expression.Lambda<Func<ProductDTO, bool>>(body, x);
    return query.Where(lambda);
}

1
投票

使用LINQKit,您可以轻松组合谓词。此外,没有理由将filters List转换为另一个List只是为了处理它们一旦你可以组合它们,你只需添加传入的每个过滤器。

public static class AgeGroups {
    public static Dictionary<string, int> Items = new Dictionary<string, int>(){
        { "Modern (Less than 10 years old)", 1 },
        { "Retro (10 - 20 years old)", 2 },
        { "Vintage(20 - 70 years old)", 3 },
        { "Antique(70+ years old)", 4 }
    };

    public static IQueryable<ProductDTO> FilterAgeByGroup(IQueryable<ProductDTO> query, List<string> filters) {
        var currentYear = DateTime.UtcNow.Year;

        var pred = PredicateBuilder.New<ProductDTO>();
        foreach (var fs in filters) {
            if (Items.TryGetValue(fs, out var fv)) {
                switch (fv) {
                    case 1:
                        pred = pred.Or(p => currentYear-p.YearManufactured < 10);
                        break;
                    case 2:
                        pred = pred.Or(p => 10 <= currentYear-p.YearManufactured && currentYear-p.YearManufactured  <= 20);
                        break;
                    case 3:
                        pred = pred.Or(p => 20 <= currentYear-p.YearManufactured && currentYear-p.YearManufactured  <= 70);
                        break;
                    case 4:
                        pred = pred.Or(p => 70 <= currentYear-p.YearManufactured);
                        break;
                }
            }
        }

        return query.Where(pred);
    }
}

1
投票

我建议动态构建你要传递给Where的表达式(如AlexAndreev's answer;但不使用任何编译器生成的表达式,只使用System.Linq.Expressions.Expression中的工厂方法。

首先使用每个标准的最小和最大年龄值元组定义原始字典:

// using static System.Linq.Expressions.Expression

public static Dictionary<string, (int code, int? min, int? max)> Items = new Dictionary<string, (int code, int? min, int? max)>(){
    { "Modern (Less than 10 years old)", (1, null, 10) },
    { "Retro (10 - 20 years old)", (2, 10, 20) },
    { "Vintage(20 - 70 years old)", (3, 20, 70) },
    { "Antique(70+ years old)", (4, 70, null) }
};

然后,您可以动态构建谓词,根据传入的过滤器及其匹配条件添加条件:

public static IQueryable<ProductDTO> FilterAgeByGroup(
    IQueryable<ProductDTO> query, 
    List<string> filters)
{
    var criteria = filters
        .Select(filter => {
            Items.TryGetValue(filter, out var criterion);
            return criterion; // if filter is not in Items.Keys, code will be 0
        })
        .Where(criterion => criterion.code > 0) // excludes filters not matched in Items
        .ToList();

    if (!criteria.Any()) { return query; }

    var type = typeof(ProductDTO);
    var x = Parameter(t);

    // creates an expression that represents the number of years old this ProductDTO is:
    // 2019 - x.YearManufactured
    var yearsOldExpr = Subtract(
        Constant(DateTime.UtcNow.Year),
        Property(x, t.GetProperty("YearManufactured"))
    );

    var filterExpressions = new List<Expression>();

    foreach (var criterion in criteria) {
        Expression minExpr = null;
        if (criterion.min != null) {
            // has to be at least criteria.min years old; eqivalent of:
            // 2019 - x.YearManufactured >= 10
            minExpr = GreaterThanOrEqual(
                yearsOldExpr,
                Constant(criterion.min)
            );
        }

        Expression maxExpr = null;
        if (criterion.max != null) {
            // has to be at least criteria.max years old; equivalent of
            // 2019 - x.YearManufactured <= 20
            maxExpr = LessThanOrEqual(
                yearsOldExpr,
                Constant(criterion.max)
            )
        }

        if (minExpr != null && maxExpr != null) {
            filterExpressions.Add(AndAlso(minExpr, maxExpr));
        } else {
            filterExpressions.Add(minExpr ?? maxExpr); // They can't both be null; we've already excluded that possibility above
        }
    }

    Expression body = filterExpressions(0);
    foreach (var filterExpression in filterExpressions.Skip(1)) {
        body = OrElse(body, filterExpression);
    }
    return query.Where(
        Lambda<Func<ProductDTO, bool>>(body, x)
    );
}
© www.soinside.com 2019 - 2024. All rights reserved.