我正在IQueryable
上创建一些扩展方法,以简化通配符过滤。但当我尝试筛选子列表时,我会遇到很多异常。我的例子:
public class User {
public string FirstName { get; set; }
public string LastName { get; set; }
public IReadOnlyList<Address> Addresses { get; set; }
...
}
public class Address {
public string Street { get; set; }
...
}
我的通配符实现不言自明,该方法需要一个值列表,并支持StartsWith
、EndsWith
和Contains
。我有一个Filter
方法,看起来像这样:
public static IQueryable<T> Filter<T>(this IQueryable<T> query, Expression<Func<T, string>> property,
IList<string> values)
{
if (values == null || values.Count == 0)
return query;
Expression<Func<T, bool>> condition;
if (values.Count == 1)
condition = GetBooleanExpressionFromString(property, values.First()).Expand();
else
condition = GetBooleanExpressionFromStringList(property, values).Expand();
return query.Where(condition);
}
表达式构建器看起来像:
private static Expression<Func<T, bool>> GetBooleanExpressionFromStringList<T>(
Expression<Func<T, string>> expression,
IList<string> values)
{
var predicate = PredicateBuilder.New<T>();
foreach (var value in values)
{
var parsedValue = value.Replace("*", string.Empty);
if (value.StartsWith("*") && value.EndsWith("*"))
predicate.Or(x => expression.Invoke(x).Contains(parsedValue));
else if (value.StartsWith("*"))
predicate.Or(x => expression.Invoke(x).EndsWith(parsedValue));
else if (value.EndsWith("*"))
predicate.Or(x => expression.Invoke(x).StartsWith(parsedValue));
else predicate.Or(x => expression.Invoke(x) == parsedValue);
}
return predicate;
}
public static Expression<Func<T, bool>> GetBooleanExpressionFromString<T>(
Expression<Func<T, string>> expression,
string value)
{
var parsedValue = value.Replace("*", string.Empty);
if (value.StartsWith("*") && value.EndsWith("*"))
return x => expression.Invoke(x).Contains(parsedValue);
if (value.StartsWith("*"))
return x => expression.Invoke(x).EndsWith(parsedValue);
if (value.EndsWith("*"))
return x => expression.Invoke(x).StartsWith(parsedValue);
return x => expression.Invoke(x) == parsedValue;
}
最后,我可以使用Filter
方法,如下所示:
var query = Session.Query<User>();
query = query.Filter(x => x.FirstName, new [] { "Foo*", "*Bar", "*test*" });
return query;
现在,我想对Address
列表的Street
属性进行相同的扩展。在正常的Linq中,它将编译为query.Where(x => x.Addresses.Any(y => y.Street.*wildcardstuff*))
,并且Filter
方法将像query.Filter(x => x.Addresses, x => x.Street, values)
一样被调用。但我一直收到NotSupported异常。我尝试的最后一件事是EF的这篇文章,但它并不像我希望的那样打字。
我尝试的最后一个实现是:
public static IQueryable<T> FilterList<T, U>(this IQueryable<T> query, Expression<Func<T, IEnumerable<U>>> innerList, Expression<Func<U, string>> property,
IList<string> values)
{
if (values == null || values.Count == 0)
return query;
Expression<Func<U, bool>> condition;
if (values.Count == 1)
condition = GetBooleanExpressionFromString(property, values.First()).Expand();
else
condition = GetBooleanExpressionFromStringList(property, values).Expand();
return query.Where(i => innerList.Invoke(i).Any(j => condition.Invoke(j)));
}
但得到了此异常:System.NotSupportedException:无法解析表达式"x=>x.Addresses',因为它具有不受支持的类型。只能解析查询源(即实现IEnumerable的表达式(和查询运算符。我试图用通配符逻辑对字符串进行扩展,但这也引发了一个NotSupported异常,有人知道吗?
我会让它更通用。这可以简化创建此类扩展。
query = query.FilterByWildcard(x => x.FirstName, new [] { "Foo*", "*Bar", "*test*" });
以及实现:
public static class QueryExtensions
{
public static IQueryable<T> FilterByWildcard<T>(this IQueryable<T> query, Expression<Func<T, string>> prop, IEnumerable<string> items)
{
return query.FilterBy(items, prop, s =>
{
var pattern = s.Trim('*');
if (s.StartsWith("*"))
if (s.EndsWith("*"))
return e => e.Contains(pattern);
else
return e => e.StartsWith(pattern);
else if (s.EndsWith("*"))
return e => e.EndsWith(pattern);
else
return e => e == s;
});
}
public static IQueryable<T> FilterBy<T, TProp, TItem>(this IQueryable<T> query,
IEnumerable<TItem> items,
Expression<Func<T, TProp>> prop,
Func<TItem, Expression<Func<TProp, bool>>> operationSelector, bool isOr = true)
{
var param = prop.Parameters[0];
Expression predicate = null;
foreach (var item in items)
{
var operation = operationSelector(item);
var body = ExpressionReplacer.GetBody(operation, prop.Body);
if (predicate == null)
{
predicate = body;
}
else
{
predicate = Expression.MakeBinary(isOr ? ExpressionType.OrElse : ExpressionType.AndAlso, predicate,
body);
}
}
if (predicate == null)
return query.Where(e => 1 == 2);
var lambda = Expression.Lambda<Func<T, bool>>(predicate, param);
return query.Where(lambda);
}
class ExpressionReplacer : ExpressionVisitor
{
readonly IDictionary<Expression, Expression> _replaceMap;
public ExpressionReplacer(IDictionary<Expression, Expression> replaceMap)
{
_replaceMap = replaceMap ?? throw new ArgumentNullException(nameof(replaceMap));
}
public override Expression Visit(Expression exp)
{
if (exp != null && _replaceMap.TryGetValue(exp, out var replacement))
return replacement;
return base.Visit(exp);
}
public static Expression Replace(Expression expr, Expression toReplace, Expression toExpr)
{
return new ExpressionReplacer(new Dictionary<Expression, Expression> {{toReplace, toExpr}}).Visit(expr);
}
public static Expression Replace(Expression expr, IDictionary<Expression, Expression> replaceMap)
{
return new ExpressionReplacer(replaceMap).Visit(expr);
}
public static Expression GetBody(LambdaExpression lambda, params Expression[] toReplace)
{
if (lambda.Parameters.Count != toReplace.Length)
throw new InvalidOperationException();
return new ExpressionReplacer(lambda.Parameters.Zip(toReplace)
.ToDictionary(e => (Expression)e.First, e => e.Second)).Visit(lambda.Body);
}
}
}
这个答案正在修复我自己的方法,但Svyatoslav Danyliv的答案更具未来性。
public static IQueryable<T> FilterList<T, U>(this IQueryable<T> query, Expression<Func<T, IEnumerable<U>>> innerList, Expression<Func<U, string>> property,
IList<string> values)
{
if (values == null || values.Count == 0)
return query;
Expression<Func<U, bool>> condition;
if (values.Count == 1)
condition = GetBooleanExpressionFromString(property, values.First());
else
condition = GetBooleanExpressionFromStringList(property, values);
Expression<Func<T, bool>> finalCondition = t => innerList.Invoke(t).Any(j => condition.Invoke(j));
return query.Where(finalCondition.Expand());
}
然后像这样使用它。
var query = Session.Query<User>();
query = query.FilterList(x => x.Addresses, y => y.Street, new [] { "Foo*", "*Bar", "*test*" });
return query;
它更短、更干净,但受到限制。