0

My plan is to create a query, but the parameters based on a Dictionary. The Dictionary contains string key and bool value. Can be 2 or 3 or more items in the dictionary.

Dictionary<string, bool> items = new Dictionary<string, bool>();
items.Add("CostFree", true);
items.Add("Visible", true);
items.Add("Closed", true);

This is the dictionary I am sending and based on this I want to create dynamically a query like

.Where(e => e.CostFree == true || Visible == true || Closed == true)

but the dictionary can contain 2, 3 or four items.

How can I solve this ? Thanks in advance

abatishchev
  • 98,240
  • 88
  • 296
  • 433
Gabor85
  • 107
  • 1
  • 10
  • Do you need to pass the query into EF? – Andriy Shevchenko Nov 08 '21 at 21:20
  • Perhaps you need to use dictionary keys and values; `item["CostFree"]` will have a value of `true` Or you could use an `enum` for your keys – Peter Smith Nov 08 '21 at 21:24
  • Yes, I have to pass it to EF. Basically I guess the boolean will always true, because I am sending the parameters which are tutned on, so not sure I even need a dictionary. But without dictionary still don't know how to create the query dinamically... – Gabor85 Nov 08 '21 at 21:26
  • Or iterate through your dictionary and count the number of `true` values. – Peter Smith Nov 08 '21 at 21:26
  • 2
    *My plan is to create a query, but the parameters based on a Dictionary* - why? – Caius Jard Nov 08 '21 at 21:41
  • Have a look here: https://dynamic-linq.net/ – Robert Harvey Nov 08 '21 at 21:45
  • Not necessary a dictionary. I am sending the parameters which are true and I need to create a query. I thought that is good with Dictionary... but maybe not – Gabor85 Nov 08 '21 at 21:48

2 Answers2

1

The easy (but inelegant) way of doing this is to chain a series of Union statements. You can use a lookup dictionary with a key matching your strings and a value containing an appropriate predicate.

Here is an example using an extension method:

static public IQueryable<Foo> WithFlags(this IQueryable<Foo> source, string[] flags)
{
    var map = new Dictionary<string, Expression<Func<Foo, bool>>>()
    {
        { "Closed", x => x.Closed },
        { "CostFree", x => x.CostFree },
        { "Visible", x => x.Visible }
    };

    //Start with a query that returns nothing
    var query = source.Where(x => false);

    //For each flag supplied by the caller, add an additional set
    foreach (var flag in flags)
    {
        query = query.Union(query.Where(map[flag]));
    }

    return query;
}

To use:

var results = DbContext.Foo.WithFlags( new string[] { "Closed", "Visible" }).ToList();

The more elegant way to do it is to build a predicate expression containing Or logic. This would be a little involved. I recommend finding a third party toolkit. See this answer.

John Wu
  • 50,556
  • 8
  • 44
  • 80
1

LINQ expressions can be built easily via static methods exposed on System.Linq.Expressions.Expression class. Here is a sample with your needs assuming the entity you are building the expression against named SomeClass

[TestMethod]
public void MyTestMethod()
{
    var testData = new List<SomeClass>()
    {
        new SomeClass() {Id=1, CostFree = false, Closed='N', Visible=false},
        new SomeClass() {Id=2, CostFree = true, Closed='N', Visible=false},     // expect only this one  matching
    };

    var items = new Dictionary<string, object>();
    items.Add("CostFree", true);
    items.Add("Visible", true);
    items.Add("Closed", 'Y');

    // this one will be the "e" in "e => e.CostFree == true || Visible == true || Closed == 'Y'"
    var paramExpression = Expression.Parameter(typeof(SomeClass));

    // lets construct the body ("e.CostFree == true || Visible == true || Closed == 'Y'") part step-by-step
    // the parts consists of binary "equals" expressions combined via logical "or" expression
    var bodyExpression = (Expression)null;
    foreach(var kvp in items)
    {
        // get the named property ("CostFree", ...) reference of paramExpression. this is the left hand side of "equals"
        var propertyExpression = Expression.PropertyOrField(paramExpression, kvp.Key);
        // get the constant with appropriate value to place on right hand side of "equals"
        var constantExpression = Expression.Constant(kvp.Value, kvp.Value.GetType());
        // combine them into "equals"
        var binaryEqualsExpression = Expression.Equal(propertyExpression, constantExpression);

        if (bodyExpression == null)
        {
            bodyExpression = binaryEqualsExpression;
        }
        else
        {
            // combine each "equals" parts with logical "or"
            bodyExpression = Expression.OrElse(bodyExpression, binaryEqualsExpression);
        }
    }

    // now construct the whole lambda...
    var lambdaExpression = Expression.Lambda<Func<SomeClass, bool>>(bodyExpression, paramExpression);
    // ...and make it useable in .Where()
    var compiledExpression = lambdaExpression.Compile();

    // lets execute in on our test data
    var r = testData.Where(compiledExpression);

    // only #2 should match
    Assert.AreEqual(2, r.Single().Id);
}

Update: I changed the solution:

  1. items values are of type object
  2. constantExpression honors the value's type.

This way the dictionary can contain other name-value pairs and the solution still works. The rule of dictionary contents: keys must match SomeClass property names and values must match the given property's type.

cly
  • 661
  • 3
  • 13
  • 1
    You can replace the foreach loop with `Aggregate()` – abatishchev Nov 08 '21 at 22:43
  • This solution makes lot of sense, seems pretty good, thank you! But when I'm implementing it, I have a System.InvalidOperationException at line var binaryEqualsExpression = Expression.Equal(propertyExpression, constantExpression); the error is: System.InvalidOperationException: 'The binary operator Equal is not defined for the types 'System.String' and 'System.Boolean'.' What I make wrong or what I've missed ? – Gabor85 Nov 09 '21 at 00:05
  • I updated the solution, hope left no typos because this time I didnt tried it :) – cly Nov 09 '21 at 00:17
  • The InvalidOperationException above means one of the named properties found in dictionary was not of type bool but instead string. The updated solution handles this if you set the value part of dictionary entry matching the named property's type. – cly Nov 09 '21 at 00:34
  • Thanks good solution! But how we can freely apply && or || between conditions?? – gyousefi Aug 17 '23 at 10:55
  • @gyousefi Add the desired operator to the input and use ````Expression.AndAlso(...)```` instead of ````OrElse```` when your desired operator is ````&&````. Your next question might be about parentheses :) Extend your input with some grouping descriptor and implement the processing to run on these groups. Each result would be an ````Expression```` which can be used as a parameter in the parent group's ````Expression````. Just look around in ````System.Linq.Expressions```` namespace. – cly Aug 17 '23 at 18:14