子列表上的 NHibernate 通配符可查询扩展

NHibernate wildcard queryable extension on sublists

我正在 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; }
  ...
}

我的通配符实现不言而喻,该方法需要一个值列表并支持 StartsWithEndsWithContains。我有一个看起来像这样的 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 异常。我尝试的最后一件事是 post from 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;

哪个更短更简洁但有限制。