-1

I have this project in c# where I have several possible types of conditions, let's say they are title and level just to put it simply.

I'm trying to make the query but I'm not sure if it's built in the correct way.

Cases:

  • Title is given, I have to go for all the titles that match that one.
  • Level is given, it's the same as title

so I have

query = query.Where(x => x.Title == title);

and later on

query = query.Where(x => x.Level == level)

My question is, how does this translate? Like this:

from * myTable WHERE Title = title and Level = level

or like this (this is the way I need it)

from * myTable WHERE Title = title or Level = level

I have to do some validations and I can't create the query in the same place,

1 Answers1

1

In general the same series of operations on IEnumerable<T> and IQueryable<T> with the same contents should produce the same output... give or take case sensitive comparisons and so on.

Consider the following:

int[] data = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var query = data.Where(i => i > 3);
query = query.Where(i => i < 8);

When you enumerate over query (which is of type IEnumerable<int>) the operations are applied in sequence. Values which fail the first Where do not get evaluated by the second, so can never reach the result set:

Value First Where Second Where Output
1 Fail -- No
2 Fail -- No
3 Fail -- No
4 Pass Pass Yes
5 Pass Pass Yes
6 Pass Pass Yes
7 Pass Pass Yes
8 Pass Fail No
9 Pass Fail No
10 Pass Fail No

This is equivalent to:

query = data.Where(i => i > 3 && i < 8);

The equivalent SQL WHERE would then be:

WHERE i > 3 AND i < 8

While this works in the case of restrictive filters (where each term must applied), it doesn't do much for permissive filters (where any term may apply). Instead we have to look at other ways to do query composition.

For IEnumerable<T> we can simply use function composition:

string title = "A Title";
int level = 1;

Func<recordType, bool> predicate = x => x.Title == title;
predicate = x => predicate(x) || x.Level == level;

var query = data.Where(predicate);

Unfortunately this isn't quite as simple for IQueryable<T> since the predicate type is a LINQ Expression of type Expression<Func<T, bool>>, and we can't create a suitable expression tree from a Func<T, bool>. Instead we have to do lambda expression composition.

The simplest point to start at is implementing an OrElse expression compositor that takes two lambda expressions and invokes them as parameters to an OrElse expression:

public static Expression<Func<T, bool>> OrElse<T>(Expression<Func<T, bool>> left, Expression<Func<T, bool>> right)
{
    if (left is null)
        return right;
    
    if (right is null)
        return left;
    
    var parm = Expression.Parameter(typeof(T), "row");
    var result = Expression.Lambda<Func<T, bool>>
    (
        Expression.OrElse
        (
            Expression.Invoke(left, parm),
            Expression.Invoke(right, parm)
        ),
        parm
    );
    return result;
}

(This could be an extension method, but I think it's bad form to design extensions that explicitly permit targets.)

We can then use this to compose a predicate expression from optional parts:

// 'Book' in this case is a placeholder for your record type.
Expression<Func<Book, bool>> MakePermissiveFilter(string title, int? level)
{
    Expression<Func<Book, bool>> result = null;
    
    if (!string.IsNullOrEmpty(title))
        result = OrElse(result, b => b.Title == title);
    
    if (level is not null)
        result = OrElse(result, b => b.Level == level);
    
    // if nothing selected return a default 'always true' predicate
    if (result is null)
        result = b => true;
    
    return result;
}

Now we can invoke that to generate the required filter expression for your Where clause:

// 'query' previously defined with select and optional ordering
query = query.Where(MakePermissiveFilter(text, level));

This will work for any compliant IQueryable<T>: LinqToObjects, LinqToSQL, Entity Framework (all versions) and so on.


The resultant predicate is slightly inelegant, but SQL generation will reduce the noise in most cases. You might end up with a WHERE 1 = 1 in there if the filter terms are empty.

We could go a lot further and unwrap the supplied lamdba expressions, replace their parameters and build a new lambda, but it's a fair amount of work for relatively little gain.

Corey
  • 15,524
  • 2
  • 35
  • 68