1

In the code below :

Expression<Func<WorkflowTask, bool>> filterBefore = wt => true;

filterBefore = filterBefore.And(wt => wt.code == "XK");

List<string> sourceLanguages = new List<string>() { "FR", "DE", "NL" };

//HOW TO BUILD OR CONDITIONS DYNAMICALLY BASED ON SOURCE LANGUAGES LIST  ? 
filterBefore = filterBefore.And(wt => wt.SourceLanguages.Contains("FR") || wt.WorkflowTaskContextualInfo.SourceLanguages.Contains("DE"));

I don't know how to build dynamically the OR condition on the SourceLanguages List. That list could contain any number of values (I've hardcoded it here for the sake of example).

wt.WorkflowTaskContextualInfo.SourceLanguages is a string with comma-separated values ("FR, EN" for instance)

The And expression is defined as below :

    public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
    {
        var invokedExpr = Expression.Invoke(expr2, expr1.Parameters.Cast<Expression>());
        return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(expr1.Body, invokedExpr), expr1.Parameters);
    }
Sam
  • 13,934
  • 26
  • 108
  • 194
  • I assume `filterBefore` is being passed to a LINQ method? Are you trying to dynamically do `filterBefore.Or(x).Or(y).Or(z)` for each string in `sourceLanguages`? – Nate Barbettini Jan 08 '20 at 15:57
  • `filterBefore` is passed to the Where clause of a LINQ query indeed. Yes, that's pretty much what I'm trying to do, except I have to ANDed this ORs with the previous filterBefore (2nd line of code) – Sam Jan 08 '20 at 16:00
  • Where does your `And` method come from? That isn't part of `System.Linq.Expressions` – canton7 Jan 08 '20 at 16:05
  • hum indeed, I had not realized that (I didn't write that code, I'm just trying to add new features). I've updated my post with the definition of the `And` method – Sam Jan 08 '20 at 16:09

3 Answers3

4

LINQKit's PredicateBuilder is designed specifically to address this kind of need. But if you feel that's too much overhead, you can craft your own Expression tree with a few simple utilities, as I've described in this answer

First, a general-purpose Expression Replacer:

public class ExpressionReplacer : ExpressionVisitor
{
    private readonly Func<Expression, Expression> replacer;

    public ExpressionReplacer(Func<Expression, Expression> replacer)
    {
        this.replacer = replacer;
    }

    public override Expression Visit(Expression node)
    {
        return base.Visit(replacer(node));
    }
}

Next, a simple utility method to replace one parameter's usage with another parameter in a given expression:

public static T ReplaceParameter<T>(T expr, ParameterExpression toReplace, ParameterExpression replacement)
    where T : Expression
{
    var replacer = new ExpressionReplacer(e => e == toReplace ? replacement : e);
    return (T)replacer.Visit(expr);
}

This is necessary because the lambda parameters in two different expressions are actually different parameters, even when they have the same name. For example, if you want to end up with q => q.first.Contains(first) || q.last.Contains(last), then the q in q.last.Contains(last) must be the exact same q that's provided at the beginning of the lambda expression.

Next we need a general-purpose Join method that's capable of joining Func<T, TReturn>-style Lambda Expressions together with a given Binary Expression generator.

public static Expression<Func<T, TReturn>> Join<T, TReturn>(Func<Expression, Expression, BinaryExpression> joiner, IReadOnlyCollection<Expression<Func<T, TReturn>>> expressions)
{
    if (!expressions.Any())
    {
        throw new ArgumentException("No expressions were provided");
    }
    var firstExpression = expressions.First();
    var otherExpressions = expressions.Skip(1);
    var firstParameter = firstExpression.Parameters.Single();
    var otherExpressionsWithParameterReplaced = otherExpressions.Select(e => ReplaceParameter(e.Body, e.Parameters.Single(), firstParameter));
    var bodies = new[] { firstExpression.Body }.Concat(otherExpressionsWithParameterReplaced);
    var joinedBodies = bodies.Aggregate(joiner);
    return Expression.Lambda<Func<T, TReturn>>(joinedBodies, firstParameter);
}

Now, applying that to your example:

    Expression<Func<WorkflowTask, bool>> codeCriteria = wt => wt.code == "XK";
    var langCriteria = new List<string>() { "FR", "DE", "NL" }
    .Select(lang => (Expression<Func<WorkflowTask, bool>>)(wt => wt.SourceLanguages.Contains(lang)))
    .ToList();

    var filter = Join(Expression.And, new[] { codeCriteria, Join(Expression.Or, langCriteria)});

filter will now have the equivalent of wt => wt.code == "XK" && (wt.SourceLanguages.Contains("FR") || wt.SourceLanguages.Contains("DE") || wt.SourceLanguages.Contains("NL"))

StriplingWarrior
  • 151,543
  • 27
  • 246
  • 315
  • Thanks for the effort ! Quick question... would that work with NHibernate ? I'm not sure I understand all of it so there's no point in coding it if my ORM doesn't support this. – Sam Jan 08 '20 at 16:28
  • 1
    @Sam: I have no way to test it, but the Expression tree you produce this way will be identical to what you'd produce by manually constructing the expression in code (e.g. `wt => wt.SourceLanguages.Contains(lang1) || w.SourceLanguages.Contains(lang2)...`). So I have every reason to believe it'll work. – StriplingWarrior Jan 08 '20 at 17:13
  • BTW, remember to include special logic for when there are no languages. The Join method will throw an exception if it's passed an empty collection, whereas I'm guessing having no languages selected in your case means you'll want to avoid adding that criteria to the query at all. – StriplingWarrior Jan 08 '20 at 17:15
  • yes indeed, I already have some logic to prevent adding the filter if there are no languages selected. If it translates to a regular OR filter, then it should work because when I hardcode the filter this way it works. I'll give it a try tomorrow. thanks ! – Sam Jan 08 '20 at 21:10
  • 1
    in the end I have used PredicateBuilder as per your recommendation. It's working like a charm :) Thanks for your help ! – Sam Jan 09 '20 at 08:44
2

I would put the required languages in an array or list.

var required = new string[]{ "FR", "DE" };

Then you can query with

wt => required.Any(r => wt.SourceLanguages.Contains(r))

or, the other way round

wt => wt.SourceLanguages.Any(sl => required.Contains(sl))
Olivier Jacot-Descombes
  • 104,806
  • 13
  • 138
  • 188
  • This would work indeed and it answers the question. But I'm out of luck because I use NHibernate and it crashes at runtime with the the "Not supported method" error. I guess it has to be written in another way for NH to handle it and convert it to SQL correctly. – Sam Jan 08 '20 at 16:18
  • Yeah, this would totally make sense with LINQ to Objects, but I think most ORMs aren't smart enough to translate that expression correctly. – StriplingWarrior Jan 08 '20 at 16:20
0

I did not feel like importing a whole library, and the sample given seemed a bit of a stretch so I think i found an easier solution using BinaryExpression Update(Expression left, LambdaExpression? conversion, Expression right).

The samples below accepts a list of string, and constructs a chain of OR expressions. Each OR expressions calls a entity framework's LIKE method. In the end the whole ordeal gets translated nicely to SQL - so if you're on the job of making dynamic filters like i did - this should help you out.

private static Expression<Func<TMeta, bool>> GetMetaKeyFilterPredicateExpression<TMeta>(List<string> metaKeyNames)
    where TMeta : IMetaKeyValuePair
{
    var parameter = Expression.Parameter(typeof(TMeta));
    var property = Expression.Property(Expression.Convert(parameter, typeof(IMetaKeyValuePair)),
        propertyName: nameof(IMetaKeyValuePair.MetaKey));
    Expression body = null!;

    BinaryExpression? predicateExpression = null;

    foreach (var metaKeyName in metaKeyNames)
    {
        var likeExpression = Expression.Call(typeof(DbFunctionsExtensions),
            nameof(DbFunctionsExtensions.Like),
            null,
            Expression.Constant(EF.Functions),
            property,
            Expression.Constant(metaKeyName)
        );

        predicateExpression =
            predicateExpression == null
                ? Expression.Or(likeExpression, Expression.Constant(false))
                : predicateExpression.Update(predicateExpression, null, likeExpression);
    }

    body = (Expression?)predicateExpression ?? Expression.Constant(true);

    var expr = Expression.Lambda<Func<TMeta, bool>>(body: body, parameter);
    return expr;
}
var expr = GetMetaKeyFilterPredicateExpression<TMeta>(metaKeyNames);
qMetaKeyValuePairs = qMetaKeyValuePairs.Where(expr);
sommmen
  • 6,570
  • 2
  • 30
  • 51