IQueryable<T> 高级 where 的扩展方法
IQueryable<T> extension method for advanced where
我正在尝试为 IQueryable<T>
添加扩展方法,以便将特定位置附加到 IQueryable
。
假设用户正在搜索名称等于 "Matt"
的数据
public IEnumerable<Employees> Search(String name)
{
var qList = _context.Employees;
if(!String.isNullOrEmpty(name))
qList = qList.Where(emp => emp.Name == name);
return qList.ToList();
}
现在,如果用户正在搜索名称以 "Matt"
开头的数据 - 他可能会尝试在特定文本框中写入 "Matt%**"
或 "Matt*"
之类的内容。预测这个我可以做到:
public IEnumerable<Employees> Search(String name)
{
var qList = _context.Employees;
if(!String.isNullOrEmpty(name))
{
string extractedName = name.Replace("%","").Replace("*","");
if(name.EndsWith("%") || name.EndsWith("*");
qList = qList.Where(emp => emp.Name.StartsWith(extractedName));
}
return qList.ToList();
}
此处将有很多 IF 语句来预测所有可能性,但还可以。我能做到。但我不喜欢的是每次执行此类功能时都重复我的代码。
如何为
制作扩展方法
IQueryable<T>
哪个会一直为我做这个检查?
public static IQueryable<T> MyAdvancedSearch<T>(this IQueryable<T> qList, String searchText)
{
if(!String.isNullOrEmpty(searchText))
/// ... Problem starts here.. because I cannot make this "Where":
qList.Where(prop=>prop.??? == searchText);
/// ... I dont know how to tell my method which field should be filtered
}
更新
我已经设法创建了一个看起来不错的扩展方法:
public static IQueryable<T> Filter<T>(this IQueryable<T> qList, Func<T,string> property, string text )
{
if (!String.IsNullOrEmpty(text))
{
if ((text.StartsWith("%") || text.StartsWith("*")) && (text.EndsWith("*") || text.EndsWith("*")))
qList = qList.Where(e => property(e).Contains(text));
else if (text.StartsWith("%") || text.StartsWith("*"))
qList = qList.Where(e => property(e).EndsWith(text));
else if (text.EndsWith("%") || text.EndsWith("*"))
qList = qList.Where(e => property(e).StartsWith(text));
else
qList = qList.Where(e => property(e) == text);
}
return qList;
}
但是当我尝试执行 qList.Tolist() 时,我收到错误消息:
InvalidOperationException: The LINQ expression 'DbSet<Employee>()
.Where(o => Invoke(__property_0, o)
== __text_1)' could not be translated.
InvalidOperationException: The LINQ expression 'DbSet<Employee>() .Where(o => Invoke(__property_0, o) == __text_1)'
could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'.
See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.
这应该可以解决问题
public static IQueryable<T> MyAdvancedSearch<T>(this IQueryable<T> qList, Expression<Func<T, string>> property, String text)
{
Expression<Func<T, bool>> expression = null;
var propertyName = GetPropertyName(property);
var parameter = Expression.Parameter(typeof(T), "x");
var left = propertyName.Split('.').Aggregate((Expression)parameter, Expression.Property);
var right = Expression.Constant(text, typeof(string));
Expression body = null;
if (!string.IsNullOrEmpty(text))
{
if ((text.StartsWith("%") || text.StartsWith("*")) && (text.EndsWith("%") || text.EndsWith("*")))
body = Expression.Call(left, "Contains", Type.EmptyTypes, Expression.Constant(text, typeof(string)));
else if (text.StartsWith("%") || text.StartsWith("*"))
body = Expression.Call(left, "EndsWith", Type.EmptyTypes, Expression.Constant(text, typeof(string)));
else if (text.EndsWith("%") || text.EndsWith("*"))
body = Expression.Call(left, "StartsWith", Type.EmptyTypes, Expression.Constant(text, typeof(string)));
else
body = Expression.MakeBinary(ExpressionType.Equal, left, right);
}
expression = Expression.Lambda<Func<T, bool>>(body, parameter);
qList = qList.Where(expression);
return qList;
}
public static string GetPropertyName<TClass, TProperty>(Expression<Func<TClass, TProperty>> property)
{
MemberExpression member = property.Body as MemberExpression;
PropertyInfo info = member.Member as PropertyInfo;
return info.Name;
}
public static IQueryable<T> MyAdvancedSearch<T>(this IQueryable<T> qList, Func<T, string> func, String searchText)
{
qList.Where(prop => func(prop) == searchText);
return qList;
}
然后你可以这样称呼它:
var qList = _context.Employees.MyAdvancedSearch((emp) => emp.Name, extractedname)
该代码看起来您正在使用 Entity Framework,因此 LINQ 语句试图在幕后为您创建一个 SQL 查询。但是它无法从 'property(e)' 函数调用创建 SQL 语句。您可以先将 IQueryable 转换为列表以将数据提取到内存中。然后,LINQ 将停止尝试将查询转换为 SQL。不过不确定这样做对性能的影响。
以上使用 Func<>
的答案将不起作用,因为 Entity Framework(或任何 ORM)无法将委托分解为其组成部分以将其转换为 SQL。 IQueryable<>
是围绕 Expression
API 设计的,这意味着您需要将 Func<,>
替换为 Expression<,>
。
但是,您不能像调用委托那样简单地调用 Expression
,因此切换类型不能单独解决此问题t/won。相反,您可以 重写 表达式,使用辅助方法和 ExpressionVisitor
将您的逻辑转换为 Entity Framework 可以 解构并转换为SQL。我们的辅助方法将采用两个参数:
Expression<Func<TEntity, TProperty>>
表示有问题的 属性,
- 代表过滤子句的
Expression<TProperty, bool>
。
(TProperty
在我们的例子中将解析为 string
)
我们将创建一个新的 Expression<Func<TEntity, bool>>
,方法是将对第二个(过滤)表达式的 TProperty
(string
)参数的引用替换为 body 第一个 (属性) 表达式:
public static class QueryExtensions
{
public static IQueryable<T> Filter<T>(
this IQueryable<T> qList,
Expression<Func<T, string>> property,
string text)
{
var filterText = text?.Trim('%', '*');
if (string.IsNullOrWhiteSpace(filterText))
return qList;
var matchEnd = text.StartsWith("*") || text.StartsWith("%");
var matchStart = text.EndsWith("*") || text.EndsWith("%");
if (matchEnd && matchStart)
return qList.Where( TranslateFilter( property, e => e.Contains( filterText ) ) );
if (matchEnd)
return qList.Where( TranslateFilter( property, e => e.EndsWith( filterText ) ) );
if (matchStart)
return qList.Where( TranslateFilter( property, e => e.StartsWith( filterText ) ) );
return qList.Where( TranslateFilter( property, e => e == text ) );
}
private static Expression<Func<TEntity, bool>> TranslateFilter<TEntity, TProperty>(
Expression<Func<TEntity, TProperty>> prop,
Expression<Func<TProperty, bool>> filter)
{
var newFilterExpression = new Visitor<TEntity>(prop).Visit(filter);
return (Expression<Func<TEntity, bool>>) newFilterExpression;
}
private class Visitor<TEntity> : ExpressionVisitor
{
private readonly ParameterExpression _parameter;
private readonly Expression _body;
public Visitor(LambdaExpression prop)
{
_parameter = prop.Parameters[0];
_body = prop.Body;
}
// return the body of the property expression any time we encounter
// the parameter expression of the filter expression
protected override Expression VisitParameter(ParameterExpression node) => _body;
public override Expression Visit(Expression node)
{
if (node is LambdaExpression lamda)
{
// Visit the body of the filter lambda, replacing references to the string
// parameter with the body of the property expression
var newBody = this.Visit(lamda.Body);
// construct a new lambda expression with the new body and the original parameter
return Expression.Lambda<Func<TEntity, bool>>(newBody, _parameter);
}
return base.Visit(node);
}
}
}
与上面的答案不同,这将适用于 Entity Framework 支持的任何“特殊”功能——无需手动编写 starts/ends 等构建表达式的逻辑.
虽然我们通过扩展方法将它限制为 string
,但上面的方法可以与任何类型的属性一起使用,例如 int
。以下是一个示例,可以让您接受针对整列的文本过滤器,例如 >= 5
或 != 33
:
public static IQueryable<T> FilterNumber<T>(
this IQueryable<T> qList,
Expression<Func<T, int>> property,
string filterQuery)
{
if (string.IsNullOrEmpty(filterQuery))
return qList;
var match = Regex.Match(filterQuery.Trim(), "^(?<sym><=|<|>|>=|={1,2}|!=|<>)?\s?(?<num>\d+)$");
if (!match.Success)
throw new Exception();
var number = int.Parse(match.Groups["num"].Value);
// no symbol, assume exact match
if (!match.Groups["sym"].Success)
return qList.Where(TranslateFilter(property, c => c == number));
return match.Groups["sym"].Value switch
{
"==" => qList.Where(TranslateFilter(property, c => c == number)),
"=" => qList.Where(TranslateFilter(property, c => c == number)),
"!=" => qList.Where(TranslateFilter(property, c => c != number)),
"<>" => qList.Where(TranslateFilter(property, c => c != number)),
"<" => qList.Where(TranslateFilter(property, c => c < number)),
"<=" => qList.Where(TranslateFilter(property, c => c <= number)),
">" => qList.Where(TranslateFilter(property, c => c > number)),
_ => qList.Where(TranslateFilter(property, c => c >= number))
};
}
最终对我有用的版本:
public static IQueryable<T> MyAdvancedSearch<T>(this IQueryable<T> qList, Expression<Func<T, string>> property, String searchExpression)
{
if (string.IsNullOrEmpty(searchExpression))
return qList;
Expression<Func<T, bool>> expression = null;
var propertyName = GetPropertyName(property);
var parameter = Expression.Parameter(typeof(T), "x");
var left = propertyName.Split('.').Aggregate((Expression)parameter, Expression.Property);
var right = Expression.Constant(searchExpression, typeof(string));
var pattern = new Regex("[*%]");
string searchText = pattern.Replace(searchExpression, "");
Expression body = null;
if ((searchExpression.StartsWith("%") || searchExpression.StartsWith("*")) && (searchExpression.EndsWith("*") || searchExpression.EndsWith("*")))
body = Expression.Call(left, "Contains", Type.EmptyTypes, Expression.Constant(searchText, typeof(string)));
else if (searchExpression.StartsWith("%") || searchExpression.StartsWith("*"))
body = Expression.Call(left, "EndsWith", Type.EmptyTypes, Expression.Constant(searchText, typeof(string)));
else if (searchExpression.EndsWith("%") || searchExpression.EndsWith("*"))
body = Expression.Call(left, "StartsWith", Type.EmptyTypes, Expression.Constant(searchText, typeof(string)));
else
body = Expression.MakeBinary(ExpressionType.Equal, left, right);
expression = Expression.Lambda<Func<T, bool>>(body, parameter);
qList = qList.Where(expression);
return qList;
}
public static string GetPropertyName<TClass, TProperty>(Expression<Func<TClass, TProperty>> property)
{
MemberExpression member = property.Body as MemberExpression;
var info = member.Member as PropertyInfo;
return info.Name;
}
我正在尝试为 IQueryable<T>
添加扩展方法,以便将特定位置附加到 IQueryable
。
假设用户正在搜索名称等于 "Matt"
public IEnumerable<Employees> Search(String name)
{
var qList = _context.Employees;
if(!String.isNullOrEmpty(name))
qList = qList.Where(emp => emp.Name == name);
return qList.ToList();
}
现在,如果用户正在搜索名称以 "Matt"
开头的数据 - 他可能会尝试在特定文本框中写入 "Matt%**"
或 "Matt*"
之类的内容。预测这个我可以做到:
public IEnumerable<Employees> Search(String name)
{
var qList = _context.Employees;
if(!String.isNullOrEmpty(name))
{
string extractedName = name.Replace("%","").Replace("*","");
if(name.EndsWith("%") || name.EndsWith("*");
qList = qList.Where(emp => emp.Name.StartsWith(extractedName));
}
return qList.ToList();
}
此处将有很多 IF 语句来预测所有可能性,但还可以。我能做到。但我不喜欢的是每次执行此类功能时都重复我的代码。
如何为
制作扩展方法IQueryable<T>
哪个会一直为我做这个检查?
public static IQueryable<T> MyAdvancedSearch<T>(this IQueryable<T> qList, String searchText)
{
if(!String.isNullOrEmpty(searchText))
/// ... Problem starts here.. because I cannot make this "Where":
qList.Where(prop=>prop.??? == searchText);
/// ... I dont know how to tell my method which field should be filtered
}
更新 我已经设法创建了一个看起来不错的扩展方法:
public static IQueryable<T> Filter<T>(this IQueryable<T> qList, Func<T,string> property, string text )
{
if (!String.IsNullOrEmpty(text))
{
if ((text.StartsWith("%") || text.StartsWith("*")) && (text.EndsWith("*") || text.EndsWith("*")))
qList = qList.Where(e => property(e).Contains(text));
else if (text.StartsWith("%") || text.StartsWith("*"))
qList = qList.Where(e => property(e).EndsWith(text));
else if (text.EndsWith("%") || text.EndsWith("*"))
qList = qList.Where(e => property(e).StartsWith(text));
else
qList = qList.Where(e => property(e) == text);
}
return qList;
}
但是当我尝试执行 qList.Tolist() 时,我收到错误消息:
InvalidOperationException: The LINQ expression 'DbSet<Employee>()
.Where(o => Invoke(__property_0, o)
== __text_1)' could not be translated.
InvalidOperationException: The LINQ expression 'DbSet<Employee>() .Where(o => Invoke(__property_0, o) == __text_1)'
could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'.
See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.
这应该可以解决问题
public static IQueryable<T> MyAdvancedSearch<T>(this IQueryable<T> qList, Expression<Func<T, string>> property, String text)
{
Expression<Func<T, bool>> expression = null;
var propertyName = GetPropertyName(property);
var parameter = Expression.Parameter(typeof(T), "x");
var left = propertyName.Split('.').Aggregate((Expression)parameter, Expression.Property);
var right = Expression.Constant(text, typeof(string));
Expression body = null;
if (!string.IsNullOrEmpty(text))
{
if ((text.StartsWith("%") || text.StartsWith("*")) && (text.EndsWith("%") || text.EndsWith("*")))
body = Expression.Call(left, "Contains", Type.EmptyTypes, Expression.Constant(text, typeof(string)));
else if (text.StartsWith("%") || text.StartsWith("*"))
body = Expression.Call(left, "EndsWith", Type.EmptyTypes, Expression.Constant(text, typeof(string)));
else if (text.EndsWith("%") || text.EndsWith("*"))
body = Expression.Call(left, "StartsWith", Type.EmptyTypes, Expression.Constant(text, typeof(string)));
else
body = Expression.MakeBinary(ExpressionType.Equal, left, right);
}
expression = Expression.Lambda<Func<T, bool>>(body, parameter);
qList = qList.Where(expression);
return qList;
}
public static string GetPropertyName<TClass, TProperty>(Expression<Func<TClass, TProperty>> property)
{
MemberExpression member = property.Body as MemberExpression;
PropertyInfo info = member.Member as PropertyInfo;
return info.Name;
}
public static IQueryable<T> MyAdvancedSearch<T>(this IQueryable<T> qList, Func<T, string> func, String searchText)
{
qList.Where(prop => func(prop) == searchText);
return qList;
}
var qList = _context.Employees.MyAdvancedSearch((emp) => emp.Name, extractedname)
该代码看起来您正在使用 Entity Framework,因此 LINQ 语句试图在幕后为您创建一个 SQL 查询。但是它无法从 'property(e)' 函数调用创建 SQL 语句。您可以先将 IQueryable 转换为列表以将数据提取到内存中。然后,LINQ 将停止尝试将查询转换为 SQL。不过不确定这样做对性能的影响。
以上使用 Func<>
的答案将不起作用,因为 Entity Framework(或任何 ORM)无法将委托分解为其组成部分以将其转换为 SQL。 IQueryable<>
是围绕 Expression
API 设计的,这意味着您需要将 Func<,>
替换为 Expression<,>
。
但是,您不能像调用委托那样简单地调用 Expression
,因此切换类型不能单独解决此问题t/won。相反,您可以 重写 表达式,使用辅助方法和 ExpressionVisitor
将您的逻辑转换为 Entity Framework 可以 解构并转换为SQL。我们的辅助方法将采用两个参数:
Expression<Func<TEntity, TProperty>>
表示有问题的 属性,- 代表过滤子句的
Expression<TProperty, bool>
。
(TProperty
在我们的例子中将解析为 string
)
我们将创建一个新的 Expression<Func<TEntity, bool>>
,方法是将对第二个(过滤)表达式的 TProperty
(string
)参数的引用替换为 body 第一个 (属性) 表达式:
public static class QueryExtensions
{
public static IQueryable<T> Filter<T>(
this IQueryable<T> qList,
Expression<Func<T, string>> property,
string text)
{
var filterText = text?.Trim('%', '*');
if (string.IsNullOrWhiteSpace(filterText))
return qList;
var matchEnd = text.StartsWith("*") || text.StartsWith("%");
var matchStart = text.EndsWith("*") || text.EndsWith("%");
if (matchEnd && matchStart)
return qList.Where( TranslateFilter( property, e => e.Contains( filterText ) ) );
if (matchEnd)
return qList.Where( TranslateFilter( property, e => e.EndsWith( filterText ) ) );
if (matchStart)
return qList.Where( TranslateFilter( property, e => e.StartsWith( filterText ) ) );
return qList.Where( TranslateFilter( property, e => e == text ) );
}
private static Expression<Func<TEntity, bool>> TranslateFilter<TEntity, TProperty>(
Expression<Func<TEntity, TProperty>> prop,
Expression<Func<TProperty, bool>> filter)
{
var newFilterExpression = new Visitor<TEntity>(prop).Visit(filter);
return (Expression<Func<TEntity, bool>>) newFilterExpression;
}
private class Visitor<TEntity> : ExpressionVisitor
{
private readonly ParameterExpression _parameter;
private readonly Expression _body;
public Visitor(LambdaExpression prop)
{
_parameter = prop.Parameters[0];
_body = prop.Body;
}
// return the body of the property expression any time we encounter
// the parameter expression of the filter expression
protected override Expression VisitParameter(ParameterExpression node) => _body;
public override Expression Visit(Expression node)
{
if (node is LambdaExpression lamda)
{
// Visit the body of the filter lambda, replacing references to the string
// parameter with the body of the property expression
var newBody = this.Visit(lamda.Body);
// construct a new lambda expression with the new body and the original parameter
return Expression.Lambda<Func<TEntity, bool>>(newBody, _parameter);
}
return base.Visit(node);
}
}
}
与上面的答案不同,这将适用于 Entity Framework 支持的任何“特殊”功能——无需手动编写 starts/ends 等构建表达式的逻辑.
虽然我们通过扩展方法将它限制为 string
,但上面的方法可以与任何类型的属性一起使用,例如 int
。以下是一个示例,可以让您接受针对整列的文本过滤器,例如 >= 5
或 != 33
:
public static IQueryable<T> FilterNumber<T>(
this IQueryable<T> qList,
Expression<Func<T, int>> property,
string filterQuery)
{
if (string.IsNullOrEmpty(filterQuery))
return qList;
var match = Regex.Match(filterQuery.Trim(), "^(?<sym><=|<|>|>=|={1,2}|!=|<>)?\s?(?<num>\d+)$");
if (!match.Success)
throw new Exception();
var number = int.Parse(match.Groups["num"].Value);
// no symbol, assume exact match
if (!match.Groups["sym"].Success)
return qList.Where(TranslateFilter(property, c => c == number));
return match.Groups["sym"].Value switch
{
"==" => qList.Where(TranslateFilter(property, c => c == number)),
"=" => qList.Where(TranslateFilter(property, c => c == number)),
"!=" => qList.Where(TranslateFilter(property, c => c != number)),
"<>" => qList.Where(TranslateFilter(property, c => c != number)),
"<" => qList.Where(TranslateFilter(property, c => c < number)),
"<=" => qList.Where(TranslateFilter(property, c => c <= number)),
">" => qList.Where(TranslateFilter(property, c => c > number)),
_ => qList.Where(TranslateFilter(property, c => c >= number))
};
}
最终对我有用的版本:
public static IQueryable<T> MyAdvancedSearch<T>(this IQueryable<T> qList, Expression<Func<T, string>> property, String searchExpression)
{
if (string.IsNullOrEmpty(searchExpression))
return qList;
Expression<Func<T, bool>> expression = null;
var propertyName = GetPropertyName(property);
var parameter = Expression.Parameter(typeof(T), "x");
var left = propertyName.Split('.').Aggregate((Expression)parameter, Expression.Property);
var right = Expression.Constant(searchExpression, typeof(string));
var pattern = new Regex("[*%]");
string searchText = pattern.Replace(searchExpression, "");
Expression body = null;
if ((searchExpression.StartsWith("%") || searchExpression.StartsWith("*")) && (searchExpression.EndsWith("*") || searchExpression.EndsWith("*")))
body = Expression.Call(left, "Contains", Type.EmptyTypes, Expression.Constant(searchText, typeof(string)));
else if (searchExpression.StartsWith("%") || searchExpression.StartsWith("*"))
body = Expression.Call(left, "EndsWith", Type.EmptyTypes, Expression.Constant(searchText, typeof(string)));
else if (searchExpression.EndsWith("%") || searchExpression.EndsWith("*"))
body = Expression.Call(left, "StartsWith", Type.EmptyTypes, Expression.Constant(searchText, typeof(string)));
else
body = Expression.MakeBinary(ExpressionType.Equal, left, right);
expression = Expression.Lambda<Func<T, bool>>(body, parameter);
qList = qList.Where(expression);
return qList;
}
public static string GetPropertyName<TClass, TProperty>(Expression<Func<TClass, TProperty>> property)
{
MemberExpression member = property.Body as MemberExpression;
var info = member.Member as PropertyInfo;
return info.Name;
}