0

I am dynamically creating a LINQ query based on various search criteria.

As an example, let's say I am searching a Automobiles table, and I have an option to filter by ratings. I have two controls:

  1. Compare type: [At least], [At most], [Less than], [Greater than], and [Equal].
  2. Value: The value to compare the rating against.

So the user could, for example, select the compare type [At least] and the value 3, and my code needs to create a query that limits results to automobile ratings greater than or equal to 3.

I found a great solution given by VinayC in the question How to implement search functionality in C#/ASP.NET MVC. His DynamicWhere() method dynamically creates part of the expression that would produce the correct filter.

My problem is that my primary query type is Automobile but my ratings are in a separate table (Automobile.Ratings). How could I implement this same technique and filter on a type other than my primary query type?

Thanks for any tips.

Community
  • 1
  • 1
Jonathan Wood
  • 65,341
  • 71
  • 269
  • 466
  • 1
    I'm not entirely sure if this will work but I think the best way is to use a simple case or if-else to build up the lambda expression and then just pass that to a single instance of `Where`. – evanmcdonnal Apr 18 '14 at 20:20
  • Is ratings a list of ratings? If so, are you interested in comparing the average of those ratings? – Jeremy Cook Apr 18 '14 at 20:20
  • @JeremyCook: Ratings *is* a list as it's on the many side of the relationship, however, there should only be one and I'm only interested in the first one. (In fact, I have to take some care to handle cases where there are no ratings.) – Jonathan Wood Apr 18 '14 at 20:22
  • @evanmcdonnal: Yes, I could branch and do as you suggest, but I actually have many such conditions and it would be much cleaner if I could do something along the lines of VinayC's `DynamicWhere()` method. – Jonathan Wood Apr 18 '14 at 20:23
  • I have used Pete Montgomery's [Predicate Builder](http://petemontgomery.wordpress.com/2011/02/10/a-universal-predicatebuilder/) to great effect. Its better (imo) than [albahari's](http://www.albahari.com/nutshell/predicatebuilder.aspx) because it handles EF without using a secondary library. – crthompson Apr 18 '14 at 20:25
  • You'll need to show some code then. Your model is not obvious to me at least. – Jeremy Cook Apr 18 '14 at 20:27
  • @JonathanWood what's more ideal; 100 lines of simple declarative code or 20 that no one can understand? Just saying, everyone of these is a 1 liner (2 if you include the `if`). You could have 1000 of them and it would still be pretty easy to work with. – evanmcdonnal Apr 18 '14 at 20:28
  • @JeremyCook: The Ratings table has an FK to the Automobiles table. I don't know why it was set up like that. I'm only interested in one rating for each automobile. If there is more than one rating, it's bad data and I'll just use the first one. – Jonathan Wood Apr 18 '14 at 20:28
  • @evanmcdonnal: The 20 lines of code would be much easier to maintain and less prone to errors. – Jonathan Wood Apr 18 '14 at 20:29
  • I would argue that's not the case. It doesn't matter how short the code is if no one understands it. Given you can't find an example of what you want to do anywhere on SO I'd say it's a pretty safe bet that none of your colleagues will understand it or be able to work with it. – evanmcdonnal Apr 18 '14 at 20:32
  • 1
    @paqogomez I have an implementation of PredicateBuilder [as seen here](http://stackoverflow.com/a/20052700/1159478) that's shorter and simpler still, and also entirely stand alone. – Servy Apr 18 '14 at 20:34
  • Based on all the comments I think that taking the time to integrate and work with a PredicateBuilder (like @Servy's) is the right solution. It makes it very easy to generate dynamic linq queries and they can query on items as deep as you'd like. Read up on Albahari's so you can understand what they're doing, then dont use his. :) – crthompson Apr 18 '14 at 20:50
  • @paqogomez: I appreciate the advice. I was hoping I didn't need to start again from the beginning. But PredicateBuilder definitely sounds worth checking out. – Jonathan Wood Apr 18 '14 at 20:52

3 Answers3

2

Since the number of operations that you have is small, finite, and known at comiple time, you can simply handle it with a switch:

IQueryable<Something> query = GetQuery();

int ratingToComareWith = 1;
string operation = "Equal";

switch (operation)
{
    case ("Equal"):
        query = query.Where(item => item == ratingToComareWith);
        break;
    case ("Less Than"):
        query = query.Where(item => item < ratingToComareWith);
        break;
}
Servy
  • 202,030
  • 26
  • 332
  • 449
  • Yes, this is a simple approach that would work. However, there are 5 conditions and, in my actual application, I have several filters that do this. This all adds up. Implementing something along the lines of VinayC's `DynamicWhere()` method would make my code much clearer and more concise. – Jonathan Wood Apr 18 '14 at 20:24
  • Agreed. I'd suggesting adding a `default` case that throws an invalid operation exception. – Jeremy Cook Apr 18 '14 at 20:25
  • 2
    @JonathanWood When you get to the point where pretty much the entirety of your query is all dynamically defined, and there is nothing about it known at compile time, you're to the point where LINQ really isn't the proper tool for the job. The proper tool for the job is a generation behind, in which you're actually constructing SQL queries. When everything about your LINQ query is dynamic, you're spending way more time building the dynamicism and getting none of the benefits of static typing. – Servy Apr 18 '14 at 20:28
  • @JonathanWood I agree with that last comment. You may as well just build your query as a string and pass it to a `SqlCommand` if you're going to be that dynamic. The code to do that is really simple anyway. – evanmcdonnal Apr 18 '14 at 20:30
  • I appreciate everyone's advice but A) I'm working in a large application and not creating one from scratch, so I need to work within the framework of the existing app, and B) I'm not new to programming. I've been doing it more years than I care to admit. I have a good sense of how I'd like this to work. Guess I still having learning when it comes to LINQ and Expressions. – Jonathan Wood Apr 18 '14 at 20:32
  • @JonathanWood If you know exactly how you want it to work and aren't open to alternatives, and are also an experienced programmer, than simply implement your solution. It's not really all that hard to build that expression dynamically (given that you already have a solution doing 95% of the work), it's just not a terribly good idea. – Servy Apr 18 '14 at 20:37
  • @Servy: I said I knew how I'd like it work, but I do not know how it would be implemented. The answer for the question I posted a link to is how I'd like it to work. To me, that's brilliant. But I don't see how to apply it to a related table. I've already acknowledged I probably have more to learn about expressions and LINQ. I'm not sure what else you want from me. – Jonathan Wood Apr 18 '14 at 20:39
0

Here's an Entity Framework friendly alternative to expression building. Of course you will want to validate column and op to prevent SQL injection.

// User provided values
int value = 3;
string column = "Rating";
string op = "<";
// Dynamically built query
db.Database.SqlQuery<Automobile>(@"select distinct automobile.* from automobile
    inner join ratings on .... 
    where [" + column + "] " + op + " @p0", value);
Jeremy Cook
  • 20,840
  • 9
  • 71
  • 77
  • Yes, that's how we did it in the old days. :) However, the guy that setup our DB access has prevented this type of query. And I can understand why. The compiler cannot check for errors. If a column changes somehow and you miss this query, then your customer will find it as a run-time error in the field. – Jonathan Wood Apr 18 '14 at 20:44
  • @JonathanWood The exact same thing can be said about a query in which you dynamically build the expressions based on strings, as you are proposing be done. Your proposed query would have no more static type checking than this query would, and yours would be quite a lot more work to construct. If you were using LINQ to write a query that could actually be validated at compile time, *then* it adds value. – Servy Apr 18 '14 at 20:46
  • I think we may have been wiser in the old days. You could make column and op enums ... OK, maybe not op :-) But a switch case on an enum like ops has compile time check and is a safe bet. – Jeremy Cook Apr 18 '14 at 20:47
  • 1
    @Servy: To the extent that is true, it is only for a very small portion of the query. Specifically, where I provide the name of the column in the form of a string. The vast majority of my query is still very much subject to verification by the compiler. – Jonathan Wood Apr 18 '14 at 20:47
  • If that is so, sounds like you are writing something rather interesting @JonathanWood ... Something analogous OData's $filters. – Jeremy Cook Apr 18 '14 at 20:49
  • @JeremyCook: Nothing fancy at all. I have statements like `if (!String.IsNullOrWhiteSpace(HotelName)) query = query.Where(h => h.Description.Contains(HotelName.Trim()));`. If the column `Description` was removed or renamed, the compiler would detect that. – Jonathan Wood Apr 18 '14 at 20:51
  • If that is the case, I'm not sure I understand you're problem. Seeing some C#-like pseudo-code that adequately demonstrates what you currently have and what you would like to have may help. It sounds like you are looking for some method that takes some parameters and produces some output. I'm not certain what the parameters are and what the expected output should be. – Jeremy Cook Apr 18 '14 at 21:16
0

Here is a method to build conditions for nested collections or types for linq-to-entities. Restructured for your needs:

        public static Expression GetCondition(Expression parameter, object value, OperatorComparer operatorComparer, params string[] properties)
{
    Expression resultExpression = null;
    Expression childParameter, navigationPropertyPredicate;
    Type childType = null;

    if (properties.Count() > 1)
    {
        //build path
        parameter = Expression.Property(parameter, properties[0]);
        var isCollection = typeof(IEnumerable).IsAssignableFrom(parameter.Type);
        //if it´s a collection we later need to use the predicate in the methodexpressioncall
        if (isCollection)
        {
            childType = parameter.Type.GetGenericArguments()[0];
            childParameter = Expression.Parameter(childType, childType.Name);
        }
        else
        {
            childParameter = parameter;
        }
        //skip current property and get navigation property expression recursivly
        var innerProperties = properties.Skip(1).ToArray();
        navigationPropertyPredicate = GetCondition(childParameter, test, innerProperties);
        if (isCollection)
        {
            //build methodexpressioncall
            var anyMethod = typeof(Enumerable).GetMethods().Single(m => m.Name == "Any" && m.GetParameters().Length == 2);
            anyMethod = anyMethod.MakeGenericMethod(childType);
            navigationPropertyPredicate = Expression.Call(anyMethod, parameter, navigationPropertyPredicate);
            resultExpression = MakeLambda(parameter, navigationPropertyPredicate);
        }
        else
        {
            resultExpression = navigationPropertyPredicate;
        }
    }
    else
    {
       var childProperty = parameter.Type.GetProperty(properties[0]);
       var left = Expression.Property(parameter, childProperty);
       var right = Expression.Constant(value,value.GetType());
       if(!new List<OperatorComparer>    {OperatorComparer.Contains,OperatorComparer.StartsWith}.Contains(operatorComparer))
        {
            navigationPropertyPredicate = Expression.MakeBinary((ExpressionType)operatorComparer,left, right);
        }
        else
        {
            var method = GetMethod(childProperty.PropertyType, operatorComparer); //get property by enum-name from type
            navigationPropertyPredicate = Expression.Call(left, method, right);
        }
        resultExpression = MakeLambda(parameter, navigationPropertyPredicate);
    }
    return resultExpression;
}

private static MethodInfo GetMethod(Type type,OperatorComparer operatorComparer)
{
    var method = type.GetMethod(Enum.GetName(typeof(OperatorComparer),operatorComparer));
    return method;
} 

public enum OperatorComparer
{
    Equals = ExpressionType.Equal,
    Contains,
    StartsWith,
    GreaterThan = ExpressionType.GreaterThan
    ....

}

private static Expression MakeLambda(Expression parameter, Expression predicate)
{
    var resultParameterVisitor = new ParameterVisitor();
    resultParameterVisitor.Visit(parameter);
    var resultParameter = resultParameterVisitor.Parameter;
    return Expression.Lambda(predicate, (ParameterExpression)resultParameter);
}

private class ParameterVisitor : ExpressionVisitor
{
    public Expression Parameter
    {
        get;
        private set;
    }
    protected override Expression VisitParameter(ParameterExpression node)
    {
        Parameter = node;
        return node;
    }
}
    }

You could replace the params string[] with params Expression(Func(T,object)), if you want. Would need some more work to do it that way. You would need to definie nested collections with a syntax like

item => item.nestedCollection.Select(nested => nested.Property)

and rewrite the expression with the help of an expressionvisitor.

Community
  • 1
  • 1
user3411327
  • 1,031
  • 8
  • 14