Generic Linq to Entitiesfilter方法,它接受要过滤的过滤条件和属性

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

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

在这些提供者类的每个提供者类中,有多种方法将接收过滤术语和查询特定属性,并根据属性的相关性返回每个匹配的分数。 大多数方法会一次过滤多个属性,但不是全部。

以下是其中两种方法:

private IQueryable MatchHighRelevanceFields(string searchTerm, IQueryable 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 MatchMediumRelevanceFields(string searchTerm, IQueryable 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 Match(string searchTerm, IQueryable data, Expression<Func> filterProperties) { var results = **build the query for each property in filterProperties** return results; } 

但我真的无法弄明白。 我尝试使用reflection,但它只适用于Linq到Objects,我需要一个Linq to Entities的解决方案。

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

 public static Expression> Compose( this Expression> first, Expression> 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>(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); } } 

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

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

现在我们已经有了这个,我们可以在每个属性选择器上使用Compose将它从属性结果映射到该属性值是否为非null并包含搜索项。 然后,我们可以将所有这些谓词组合在一起,以获得查询的filter:

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

您可以使用表达式树来完成它,但它并不像您想象的那么简单。

 public static IQueryable Match(this IQueryable data, string searchTerm, params Expression>[] 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() { {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>(body, parameter)); return data.Provider.CreateQuery(where); } return data; } public static Expression ReplaceParameters(this Expression exp, IDictionary 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 _map; public ParameterRebinder(IDictionary 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(); 

警告 – 我使用AsQueryable使用linq检查了这个对象但没有针对EF运行它。

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

 public static IQueryable Match( string searchTerm, IQueryable data, params Expression>[] filterProperties) where T : class { var predicates = new List(); 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); 

局限性。

filter只能接受基本表达式。

  • r => r.Name
  • r => r.PropA.Name
  • r => r.PropA.PropB.Name