Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamically generate predicate at runtime

Tags:

c#

linq

Not sure if this is entirely possible; I've found some stuff about expression and predicate builders, but so far nothing that lets you run arbitrary queries without knowing them in advance.

Basically, I have a collection of objects from a large SQL database, and I'm building a webpage (ASP.NET MVC 4) to allow a user to display and filter these objects. The queries the users will be entering will vary in complexity. The simplest and neatest way to let them enter these queries is something like the way the Visual Studio TFS plugin lets you search work items: a table of conditions, where you can keep adding rows. You select "and" or "or" for the join condition, then select a field, enter a value, and select whether you want things that do or do not match it:

1. show items where [Field] [is|is not] [value]
2.         [and|or] [Field] [is|is not] [value]
3.         [and|or] [Field] [is|is not] [value]
etc...

What's the simplest way to turn that into something LINQ-ish that I can stick a .ToList() on the end of? The only solution I've come up with so far has involved a rather large and ugly switch block with cases to match the various fields and tack on a .Where(), but to allow the user to select "or" for a condition then I end up doing something like this:

  • While condition is AND:
    • use big switch to match the field
    • query = query.Where(ThisField == value);
  • When you hit a condition that is OR:
    • append current results to temporary list
    • new query from full unfiltered list
    • use big switch to match the field
    • query = fullList.Where(ThisField == value);
    • continue as before
  • When you run out of conditions, append your current result set to the temporary list you've been using all along, and return that list.

This seems less elegant than I'd like.

like image 687
anaximander Avatar asked Dec 12 '25 13:12

anaximander


2 Answers

You can do it like this:

class Program
{
    public enum Operator
    {
        And,
        Or
    }

    public class Condition
    {
        public Operator Operator { get; set; }
        public string FieldName { get; set; }
        public object Value { get; set; }
    }

    public class DatabaseRow
    {
        public int A { get; set; }
        public string B { get; set; }
    }

    static void Main(string[] args)
    {
        var conditions = new List<Condition>
        {
            new Condition { Operator = Operator.And, FieldName = "A", Value = 1 },
            new Condition { Operator = Operator.And, FieldName = "B", Value = "Asger" },
            new Condition { Operator = Operator.Or, FieldName = "A", Value = 2 },
        };

        var parameter = Expression.Parameter(typeof (DatabaseRow), "x");
        var currentExpr = MakeExpression(conditions.First(), parameter);
        foreach (var condition in conditions.Skip(1))
        {
            var nextExpr = MakeExpression(condition, parameter);
            switch (condition.Operator)
            {
                case Operator.And:
                    currentExpr = Expression.And(currentExpr, nextExpr);
                    break;
                case Operator.Or:
                    currentExpr = Expression.Or(currentExpr, nextExpr);
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        var predicate = Expression.Lambda<Func<DatabaseRow, bool>>(currentExpr, parameter).Compile();

        var input = new[]
        {
            new DatabaseRow {A = 1, B = "Asger"},
            new DatabaseRow {A = 2, B = "Hans"},
            new DatabaseRow {A = 3, B = "Grethe"}
        };

        var results = input.Where(predicate).ToList();
    }

    static BinaryExpression MakeExpression(Condition condition, ParameterExpression parameter)
    {
        return Expression.Equal(
            Expression.MakeMemberAccess(parameter, typeof (DatabaseRow).GetMember(condition.FieldName)[0]),
            Expression.Constant(condition.Value));
    }
}

This assumes you have a class as a model of the database row with the correct types. You can then parse your conditions into a list of the typed condition shown above via regex and the provided code can convert that to an expression tree. The resulting expression can either be compiled and run (as shown) or converted to SQL (just stuffing the predicate into the IQueryable.Where instead).

like image 115
asgerhallas Avatar answered Dec 15 '25 06:12

asgerhallas


You can use PredicateBuilder from LINQKit to do this. Using its And() and Or() extension methods, you can build an expression tree for your query. Then you can use that expression tree as the condition of your Where(). You will also need to either call AsExpandable() or your query, or call Expand() on the created expression.

like image 34
svick Avatar answered Dec 15 '25 04:12

svick