Search code examples
c#linqlambdanhibernatelinqkit

NHibernate wildcard queryable extension on sublists


I'm creating some extension methods on IQueryable to make wildcard filtering easier. But I'm stumbling into a lot of exceptions when I try to filter a sub list. My example:

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

My wildcard implementation speaks for itself, the method expects a list of values and supports StartsWith, EndsWith and Contains. I have a Filter method that looks like this:

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

And the expressions builders look like:

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

At the end I can use the Filter method like this:

var query = Session.Query<User>();
query = query.Filter(x => x.FirstName, new [] { "Foo*", "*Bar", "*test*" });
return query;

Now I want to make the same extension for the Street property of the Address list. In normal Linq it would compile to query.Where(x => x.Addresses.Any(y => y.Street.*wildcardstuff*)) and the Filter method would be called like query.Filter(x => x.Addresses, x => x.Street, values). But I'm keep getting NotSupported exceptions. The last thing I tried is this post from EF but its not that typed like i want it to be.

The last implementation I tried is this one:

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

but got this exception: System.NotSupportedException: Cannot parse expression 'x => x.Addresses' as it has an unsupported type. Only query sources (that is, expressions that implement IEnumerable) and query operators can be parsed. I tried to make an extension on string with the wildcard logic but this throws also a NotSupported exception, anyone has an idea?


Solution

  • I would make it more universal. Which may simplify creating such extensions.

    query = query.FilterByWildcard(x => x.FirstName, new [] { "Foo*", "*Bar", "*test*" });
    

    And realization:

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