【问题标题】:Generic Linq to Entities filter method that accepts filter criteria and properties to be filtered接受过滤条件和要过滤的属性的通用 Linq to Entities 过滤方法
【发布时间】:2014-08-26 14:38:39
【问题描述】:

我在 SO 中查看了许多通用 linq 过滤问题及其答案,但没有一个能满足我的需求,所以我认为我应该创建一个问题。

我创建了许多我称之为“过滤器提供程序”的类,为我的模型中的每个实体类创建一个,以便为我的应用程序提供简单的搜索。我不想进入像 Lucene.Net 这样更高级的解决方案,因为具有匹配分数的基本过滤就足够了。

在这些提供程序类中的每一个中,都有多个方法将接收过滤条件并查询特定属性,并根据属性的相关性为每个匹配项返回一个分数。大多数方法会一次过滤多个属性,但不是全部。

这里有两种方法:

private IQueryable<Retailer> MatchHighRelevanceFields(string searchTerm, IQueryable<Retailer> retailers)
{
    var results = retailers.Where(r =>
        (r.CompanyName != null && r.CompanyName.ToUpper().Contains(searchTerm))
        || (r.TradingName != null && r.TradingName.ToUpper().Contains(searchTerm))
    );

    return results;
}

private IQueryable<Retailer> MatchMediumRelevanceFields(string searchTerm, IQueryable<Retailer> retailers)
{
    var results = retailers.Where(r =>
        (r.Address.Street != null && r.Address.Street.ToUpper().Contains(searchTerm))
        || (r.Address.Complement != null && r.Address.Complement.ToUpper().Contains(searchTerm))
    );

    return results;
}

这些方法在每个提供程序类中都被复制得令人作呕,我希望我可以将它们替换为一个可以接收要包含在查询中的属性的方法。

类似:

public static IQueryable<T> Match<T>(string searchTerm, IQueryable<T> data, Expression<Func<T, string>> filterProperties)
{
    var results = **build the query for each property in filterProperties**

    return results;
}

但我真的想不通。我尝试使用反射,但它只适用于 Linq to Objects,我需要 Linq to Entities 的解决方案。

【问题讨论】:

    标签: c# entity-framework generics linq-to-entities


    【解决方案1】:

    所以要解决这个问题,我们首先需要一些拼图。第一个拼图是一个方法,它可以接受一个计算值的表达式,然后另一个表达式计算一个与第一个返回的类型相同的新值,并创建一个表示传递第一个结果的新表达式函数作为第二个的参数。这让我们可以Compose 表达式:

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

    我们还需要一个工具来帮助我们将OR 两个谓词表达式组合在一起:

    public static class PredicateBuilder
    {
        public static Expression<Func<T, bool>> True<T>() { return f => true; }
        public static Expression<Func<T, bool>> False<T>() { return f => false; }
    
        public static Expression<Func<T, bool>> Or<T>(
            this Expression<Func<T, bool>> expr1,
            Expression<Func<T, bool>> expr2)
        {
            var secondBody = expr2.Body.Replace(
                expr2.Parameters[0], expr1.Parameters[0]);
            return Expression.Lambda<Func<T, bool>>
                  (Expression.OrElse(expr1.Body, secondBody), expr1.Parameters);
        }
    
        public static Expression<Func<T, bool>> And<T>(
            this Expression<Func<T, bool>> expr1,
            Expression<Func<T, bool>> expr2)
        {
            var secondBody = expr2.Body.Replace(
                expr2.Parameters[0], expr1.Parameters[0]);
            return Expression.Lambda<Func<T, bool>>
                  (Expression.AndAlso(expr1.Body, secondBody), expr1.Parameters);
        }
    }
    

    现在我们有了这个,我们可以在每个属性选择器上使用Compose 来将它从属性结果映射到该属性值是否为非空并包含搜索词。然后,我们可以将所有这些谓词 OR 在一起,为您的查询获取过滤器:

    public static IQueryable<T> Match<T>(
        IQueryable<T> data,
        string searchTerm,
        IEnumerable<Expression<Func<T, string>>> filterProperties)
    {
        var predicates = filterProperties.Select(selector =>
                selector.Compose(value => 
                    value != null && value.Contains(searchTerm)));
        var filter = predicates.Aggregate(
            PredicateBuilder.False<T>(),
            (aggregate, next) => aggregate.Or(next));
        return data.Where(filter);
    }
    

    【讨论】:

    • @Servy 这很棒。我遇到的一个问题是value.Contains(...)。那是OP的搜索逻辑,对吗?我想将搜索词中的任何单词与属性值中的任何单词进行匹配。我在它自己的方法中有这个,它将值拆分为数组并进行比较,但这在 EF 中不起作用,因为它不理解该方法。我该怎么做?如果你愿意,可以带去聊天
    【解决方案2】:

    您可以使用表达式树来做到这一点,但它并不像您想象的那么简单。

    public static IQueryable<T> Match<T>(this IQueryable<T> data, string searchTerm,
                                             params Expression<Func<T, string>>[] filterProperties)
    {
        var parameter = Expression.Parameter(typeof (T), "source");
    
        Expression body = null;
    
        foreach (var prop in filterProperties)
        {
            // need to replace all the expressions with the one parameter (gist taken from Colin Meek blog see link on top of class)
    
            //prop.body should be the member expression
            var propValue =
                prop.Body.ReplaceParameters(new Dictionary<ParameterExpression, ParameterExpression>()
                    {
                        {prop.Parameters[0], parameter}
                    });
    
    
            // is null check
            var isNull = Expression.NotEqual(propValue, Expression.Constant(null, typeof(string)));
    
            // create a tuple so EF will parameterize the sql call
            var searchTuple = Tuple.Create(searchTerm);
            var matchTerm = Expression.Property(Expression.Constant(searchTuple), "Item1");
            // call ToUpper
            var toUpper = Expression.Call(propValue, "ToUpper", null);
            // Call contains on the ToUpper
            var contains = Expression.Call(toUpper, "Contains", null, matchTerm);
            // And not null and contains
            var and = Expression.AndAlso(isNull, contains);
            // or in any additional properties
            body = body == null ? and : Expression.OrElse(body, and);
        }
    
        if (body != null)
        {
            var where = Expression.Call(typeof (Queryable), "Where", new[] {typeof (T)}, data.Expression,
                                        Expression.Lambda<Func<T, bool>>(body, parameter));
            return data.Provider.CreateQuery<T>(where);
        }
        return data;
    }
    
     public static Expression ReplaceParameters(this Expression exp, IDictionary<ParameterExpression, ParameterExpression> map)
    {
        return new ParameterRebinder(map).Visit(exp);
    }
    

    现在你需要一个表达式访问器来让所有表达式都使用一个参数

    //http://blogs.msdn.com/b/meek/archive/2008/05/02/linq-to-entities-combining-predicates.aspx
    public class ParameterRebinder : ExpressionVisitor
    {
        private readonly IDictionary<ParameterExpression, ParameterExpression> _map;
    
        public ParameterRebinder(IDictionary<ParameterExpression, ParameterExpression> map)
        {
            _map = map;
        }
    
        protected override Expression VisitParameter(ParameterExpression node)
        {
            if (_map.ContainsKey(node))
            {
                return _map[node];
            }
            return base.VisitParameter(node);
        }
    }
    

    会像这样使用它

    var matches = retailers.Match("7", r => r.Address.Street, x => x.Address.Complement).ToList();
    

    警告 - 我使用 linq 对使用 AsQueryable 的对象进行了检查,但没有针对 EF 运行它。

    【讨论】:

      【解决方案3】:

      您可以使用Linq.Dynamic 来构建查询。

      public static IQueryable<T> Match<T>(
          string searchTerm, 
          IQueryable<T> data, 
          params Expression<Func<T, string>>[] filterProperties) where T : class
      {
          var predicates = new List<string>();
          foreach (var prop in filterProperties)
          {
              var lambda = prop.ToString();
              var columnName = lambda.Substring(lambda.IndexOf('.') + 1);
              var predicate = string.Format(
                  "({0} != null && {0}.ToUpper().Contains(@0))", columnName);
              predicates.Add(predicate);
          }
      
          var filter = string.Join("||", predicates);
          var results = data.Where(filter, searchTerm);
          return results;
      }
      

      用法。

      var retailers = Match(
          "asd", db.Retailers, r => r.CompanyName, r => r.TradingName);
      
      var retailers = Match(
          "asd", db.Retailers, r => r.Address.Street, r => r.Address.Complement);
      

      限制。

      过滤器只能接受基本表达式。

      • r =&gt; r.Name
      • r =&gt; r.PropA.Name
      • r =&gt; r.PropA.PropB.Name

      【讨论】:

      • 这是非常脆弱的,不能支持许多不同类型的选择器表达式,并且由于尝试解析未知表达式的字符串表示而很容易破坏其语法.
      • @Servy,我是根据OP示例代码构建的,如果你能提供另一个我可以测试的例子,那就太好了
      • 我做到了。我在我的答案中发布了它。
      • @Servy,您所说的示例用法可能会破坏此方法
      • 任何不只是属性访问器的东西,比如item =&gt; "foo" + item.Property,使用闭包item =&gt; someClosedOverVariable,或使用EntityFunctions item =&gt; EntityFunctions.Left(item.Something, 5) .
      【解决方案4】:
      猜你喜欢
      • 1970-01-01
      • 2014-05-07
      • 2012-03-08
      • 2011-06-10
      • 2023-03-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-08-28
      相关资源
      最近更新 更多