1

I have to lists

  1. Main list with Comments field
  2. List of keywords to search for.

I want to search the keywords in each Comment field of each record which in SQL would look like below

select * from MainList
where Comment like '%keyword1%'
or Comment like '%keyword2%'
... so on until the last keyword.

So far I have seen examples but usually for single keyword at a time only e.g. LINQ with non-lambda for Any Contains

What I would like is to search each record in MainList for any instance of any of my keyowrds all at once. something like:

var newList = MainList.Where(m => m.Comments.Contains(purposes))

I would prefer doing it in lambda syntax but if not possible, linq is also okay.

OmC'ist
  • 59
  • 1
  • 9

2 Answers2

12

Added extension method which can help in generating such predicate. Usage is simple:

var newList = MainList
   .FilterByItems(keywords, (m, k) => m.Comments.Contains(k), true)
   .ToList();

And implementation:

public static class QueryableExtensions
{
    public static IQueryable<T> FilterByItems<T, TItem>(this IQueryable<T> query, IEnumerable<TItem> items,
        Expression<Func<T, TItem, bool>> filterPattern, bool isOr)
    {
        Expression predicate = null;
        foreach (var item in items)
        {
            var itemExpr = Expression.Constant(item);
            var itemCondition = ExpressionReplacer.Replace(filterPattern.Body, filterPattern.Parameters[1], itemExpr);
            if (predicate == null)
                predicate = itemCondition;
            else
            {
                predicate = Expression.MakeBinary(isOr ? ExpressionType.OrElse : ExpressionType.AndAlso, predicate,
                    itemCondition);
            }
        }

        predicate ??= Expression.Constant(false);
        var filterLambda = Expression.Lambda<Func<T, bool>>(predicate, filterPattern.Parameters[0]);

        return query.Where(filterLambda);
    }

    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(Enumerable.Range(0, lambda.Parameters.Count)
                .ToDictionary(i => (Expression) lambda.Parameters[i], i => toReplace[i])).Visit(lambda.Body);
        }
    }
}
Svyatoslav Danyliv
  • 21,911
  • 3
  • 16
  • 32
  • 2
    How does this work? – Christopher Aug 19 '21 at 17:42
  • 3
    @Chris It takes the `filterPattern` and basically builds up an expression that applies that to each value in the `items` and then or's them together. So this example would create an expression equivalent to `m => m.Comments.Contains(keyword1) || m.Comments.Contains(keyword2) || m.Comments.Contains(keyword3)` where `keywords` contains `keyword1`, `keyword2`, and `keyword3`. – juharr Aug 20 '21 at 01:08
  • I think this is a little complicated because it tries to solve two issues at once. `Contains` for one value works just fine. Isn't the main issue having a dynamic number disjunctions? The `LinqKit` package has a `PredicateBuilder` for that. – Oliver Schimmer Feb 21 '23 at 14:22
  • @OliverSchimmer, are you sure that you want to loop over all items and apply predicate builder? This answer helps to avoid that and avoids additional third party dependency. – Svyatoslav Danyliv Feb 21 '23 at 14:25
  • @SvyatoslavDanyliv You too are iterating over all items in your expression builder. I cannot see how you can build an expression tree without doing so. – Oliver Schimmer Feb 21 '23 at 14:48
  • @OliverSchimmer, I have created extension to do not iterate every time by myself. It is much effective than LINQKit, in which I contribute also. – Svyatoslav Danyliv Feb 21 '23 at 14:53
  • @SvyatoslavDanyliv That wasn't the debate. It does not matter who iterates, the code you provided will still be in your project, and a maintenance nightmare for anyone who stumbles over it. – Oliver Schimmer Feb 21 '23 at 14:54
  • @OliverSchimmer, for which purpose it has to be maintained? It works or not. And it works for years for others. Complex part is 12 lines of simple Expression Tree remapping code. – Svyatoslav Danyliv Feb 21 '23 at 15:01
0

Answering for my and others' reference.

var newList = MainList.Where(m => keywords.Any(k => m.Comments.Contains(k))).ToList();
Enigmativity
  • 113,464
  • 11
  • 89
  • 172
OmC'ist
  • 59
  • 1
  • 9