C# 接受要筛选的筛选条件和属性的通用Linq to Entities筛选方法
我在SO中研究了许多通用linq过滤问题及其答案,但没有一个能满足我的需要,所以我认为应该创建一个问题 我已经创建了许多我称之为“过滤器提供者”的类,在我的模型中每个实体类一个,为我的应用程序提供一个简单的搜索。我不想使用像Lucene.Net这样更高级的解决方案,因为使用匹配分数的基本过滤就足够了 在这些提供程序类中的每一个类中都有多个方法,这些方法将接收筛选条件并查询特定属性,并根据属性的相关性为每个匹配返回分数。大多数方法会一次过滤多个属性,但不是全部 以下是其中两种方法:C# 接受要筛选的筛选条件和属性的通用Linq to Entities筛选方法,c#,entity-framework,generics,linq-to-entities,C#,Entity Framework,Generics,Linq To Entities,我在SO中研究了许多通用linq过滤问题及其答案,但没有一个能满足我的需要,所以我认为应该创建一个问题 我已经创建了许多我称之为“过滤器提供者”的类,在我的模型中每个实体类一个,为我的应用程序提供一个简单的搜索。我不想使用像Lucene.Net这样更高级的解决方案,因为使用匹配分数的基本过滤就足够了 在这些提供程序类中的每一个类中都有多个方法,这些方法将接收筛选条件并查询特定属性,并根据属性的相关性为每个匹配返回分数。大多数方法会一次过滤多个属性,但不是全部 以下是其中两种方法: private
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;
}
private IQueryable MatchHighRelevanceFields(字符串搜索词,IQueryable零售商)
{
var结果=零售商。其中(r=>
(r.CompanyName!=null&&r.CompanyName.ToUpper().Contains(searchTerm))
||(r.TradingName!=null&&r.TradingName.ToUpper().Contains(搜索术语))
);
返回结果;
}
专用IQueryable MatchMediumRelevanceFields(字符串搜索术语,IQueryable零售商)
{
var结果=零售商。其中(r=>
(r.Address.Street!=null和&r.Address.Street.ToUpper().Contains(searchTerm))
||(r.Address.complete!=null&&r.Address.complete.ToUpper().Contains(searchTerm))
);
返回结果;
}
这些方法在每个提供程序类中都是自然复制的,我希望我可以将它们替换为一个方法,该方法将接收要包含在查询中的属性
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;
}
比如:
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;
}
公共静态IQueryable匹配(字符串搜索项、IQueryable数据、表达式筛选器属性)
{
var results=**为filterProperties中的每个属性生成查询**
返回结果;
}
但我真的搞不懂。我尝试过使用反射,但它只适用于Linq to对象,我需要一个Linq to实体的解决方案。您可以使用它来构建查询
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;
}
限制
筛选器只能接受基本表达式
r=>r.Name
r=>r.PropA.Name
r=>r.PropA.PropB.Name
- 所以要解决这个问题,我们首先需要一些拼图。第一个难题是一个方法,它可以采用一个计算值的表达式,然后采用另一个计算新值的表达式,该表达式采用第一个函数返回的相同类型,并创建一个新表达式,该表达式表示将第一个函数的结果作为参数传递给第二个函数的结果。这使我们能够
编写表达式:
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 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);
}
}
你可以用表达式树来实现,但它并不像你想象的那么简单
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);
}
警告-我使用AsQueryable与linq to对象检查了这一点,但没有对EF运行它。尝试使用类似于所有表达式的表达式
这非常脆弱,无法支持多种不同类型的选择器表达式,并且很容易由于试图解析未知表达式的字符串表示而导致语法中断。@Servy,我是根据OP示例代码构建的,如果您能提供另一个我可以测试的示例,那将是greatI所做的。我在我的回答中发布了它。@Servy,你说的示例用法可能会破坏这种方法,任何不仅仅是属性的访问器的东西,比如说,item=>“foo”+item.property
,闭包item=>someclosedVervariable
,或者使用EntityFunctions
item=>EntityFunctions.Left(item.Something,5)
@Servy这太棒了。但是我遇到的一个问题是value.Contains(…)
。这就是OP的搜索逻辑,对吗?我想将搜索词中的任何单词与属性值中的任何单词进行匹配。我在它自己的方法中有这样的功能,它将值拆分为数组并进行比较,但这在EF中不起作用,因为它不理解该方法。我该怎么做?如果你想的话,你可以带我去聊天
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);
}
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();