创建 linq 查询以搜索联系人,就像智能手机一样



假设在我的数据库中我有表

**Table Contact**
Id, FirstName, LastName,   Phone,       Email,           DateCreated
1   Tom        Williams    3052548623   tom@gmail.com    2013-12-21 14:51:08
etc...

我想使用户能够搜索输入字符串的联系人。假设用户输入:

tom       -> TRUE
tom wil   -> TRUE
wil tom   -> TRUE
tom XX    -> FALSE
t w 3 @   -> TRUE
wil 305   -> TRUE

(True 表示搜索找到客户 Tom,False 表示未找到)

我将在数据库中的不同表中执行这种类型的搜索。如果我不必为特定表构建查询,那就太好了。


我正在考虑采取的方法是每次找到一个或多个空格时拆分搜索字符串。然后我将创建 n 个搜索,然后执行相交?

你可以做类似的事情,假设你只想在字符串属性中进行搜索(所以如果我们将 Phone 视为字符串属性,你的示例将起作用)。

它当然可以用数字属性来做(但会变得更复杂)。

帮助程序静态类中类似的方法

public static Expression<Func<T, bool>> BuildPredicateForFilter<T>(string filterString)
{
//first, split search by space, removing white spaces, and putting this to upper case
var filters = filterString.Split(new []{" "}, StringSplitOptions.RemoveEmptyEntries).Select(m => m.ToUpper());
var parameter = Expression.Parameter(typeof (T), "m");
//get string.Contains() method
var containsMethod = typeof (string).GetMethod("Contains");
//get string.ToUpper() method
var toUpperMethod = typeof (string).GetMethod("ToUpper", new Type[]{});
//find all the string properties of your class
var properties = typeof(T).GetProperties().Where(m => m.PropertyType == typeof(string));
//for all the string properties, build a "m.<PropertyName>.ToUpper() expression
var members = properties.Select(p => Expression.Call(Expression.Property(parameter, p), toUpperMethod));
Expression orExpression = null;
//build the expression
foreach (var filter in filters)
{
Expression innerExpression = null;
foreach (var member in members)
{
innerExpression = innerExpression == null
? (Expression)Expression.Call(member, containsMethod, Expression.Constant(filter))
: Expression.OrElse(innerExpression, Expression.Call(member, containsMethod, Expression.Constant(filter)));
}
orExpression = orExpression == null
? innerExpression
: Expression.AndAlso(orExpression, innerExpression);
}
return Expression.Lambda<Func<T, bool>>(orExpression, new[]{parameter});
}

用法:

var result = <yourSource>.Where(Helper.BuildPredicateForFilter<TableName>("tom XX"));

例如,对于"tom XX",orExpression 将如下所示

((((m.FirstName.ToUpper().Contains("TOM") OrElse m.LastName.ToUpper().Contains("TOM")) OrElse m.Phone.ToUpper().Contains("TOM")) OrElse m.Email.ToUpper().Contains("TOM")) AndAlso (((m.FirstName.ToUpper().Contains("XX") OrElse m.LastName.ToUpper().Contains("XX")) OrElse m.Phone.ToUpper().Contains("XX")) OrElse m.Email.ToUpper().Contains("XX")))

编辑

或者您可以将方法更改为

public static IQueryable<T> FilterFor(this IQueryable<T> queryable, string filterString) {
//same
var predicate = Expression.Lambda<Func<T, bool>>(orExpression, new[]{parameter});
return queryable.Where(predicate);
}

那么用法就很简单

<yourSource>.FilterFor("tom XX");

因此,我们在这里要做的是在给定值的类型中的所有字段中搜索,执行Contains搜索。 我们可以编写一种方法来执行此操作。

首先我们需要使用PredicateBuilder,因为我们将动态生成许多我们想要一起 OR 的表达式。 以下是我对能够做到这一点的PredicateBuilder的定义:

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

这将使用以下帮助程序方法/类将一个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 Expression Replace(this Expression expression,
Expression searchEx, Expression replaceEx)
{
return new ReplaceVisitor(searchEx, replaceEx).Visit(expression);
}

我们将用来解决此问题的另一个工具是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);
}

多亏了所有这些工具,剩下的实际上非常简单。 我们将接受一个查询、一个要搜索的字符串和一系列选择器,每个选择器选择一个要搜索的字段。 然后我们初始化一个过滤器,遍历每个选择器,使用Compose将每个选择器转换为对相关搜索文本执行Contains检查的谓词,然后将其 OR 添加到现有过滤器。

public static IQueryable<T> AnyFieldContains<T>(
this IQueryable<T> query,
string searchText,
params Expression<Func<T, string>>[] fieldSelectors)
{
var filter = PredicateBuilder.False<T>();
foreach (var selector in fieldSelectors)
{
filter = filter.Or(selector.Compose(
value => value.Contains(searchText)));
}
return query.Where(filter);
}

现在我们已经有了所有这些,我们可以拆分您拥有的输入,对于每个表达式,我们可以调用此方法。 然后,您只需为需要搜索的字段提供选择器:

IQueryable<Foo> query = db.Foo;
string searchText = "wil tom";
var searchExpressions = searchText.Split(' ');
foreach (var expression in searchExpressions)
{
query = query.AnyFieldContains(expression,
foo => foo.FirstName,
foo => foo.LastName,
foo => foo.Phone);
}
var result = query.Any();

如果你真的确定要搜索每个字段(我不确定你是否是,很可能许多表都有不应该搜索的字段,或者有需要你做一些工作才能将它们转换为值得搜索的适当字符串的字段),那么你可以使用反射来生成所有选择器, 而不是明确输入您想要搜索的内容。 我们可以简单地创建一个额外的重载,这样如果没有提供选择器,它将使用"一切":

public static IQueryable<T> AnyFieldContains<T>(
this IQueryable<T> query,
string searchText)
{
return AnyFieldContains(query, searchText,
typeof(T).GetProperties()
.Select(prop => CreateSelector<T>(prop))
.ToArray());
}
private static Expression<Func<T, string>> CreateSelector<T>(PropertyInfo prop)
{
var param = Expression.Parameter(typeof(T));
Expression body = Expression.Property(param, prop);
if (prop.PropertyType == typeof(decimal?))
body = Expression.Call(body, typeof(SqlFunctions)
.GetMethod("StringConvert", new[] { typeof(decimal?) }));
else if (prop.PropertyType == typeof(double?))
body = Expression.Call(body, typeof(SqlFunctions)
.GetMethod("StringConvert", new[] { typeof(double?) }));
return Expression.Lambda<Func<T, string>>(body, param);
}

最新更新