2

Here's my scenario:

There is a collection of objects where each object contains a Dictionary<string, string>. The user can build a set of queries for this collection from another app to obtain a subset by selecting a Key in the Dictionary, an operator such as > or CONTAINS, etc., and a Value. They can also balance parenthesis to create groups of queries and select AND/OR operators to combine the queries.

As an example, let's say I have a collection of Car objects and the Dictionary contains keys for Make, Model, and Year.

My app is getting these queries in the form of a string like so:

"((Make = Honda) AND (Model CONTAINS Civic)) || (Year >= 2015)"

This tells me that from the collection of Car objects that I want cars that have Dictionary keys/values of <Make, Honda> and <Model, anything that contains "Civic"> OR <Year, greater than or equal to 2015>

So, I parse these out and put them into a QueryClass containing three string fields for the Key, the Operator, and the Value. I also keep track of the operator between the queries, and if they are in a group of parentheses or not.

Currently, I have to go through each QueryClass one by one performing the query, checking what the previous operator was, if it's part of a group, etc. and combining collections over and over until it reaches the end. This is tedious and seems like an awful way to do things. If there was a way to build these LINQ queries dynamically or perform SQL statements (what these are essential) on this collection it would be better.

Here's my query class that I'm storing the parsed strings in:

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

    public QueryClass(string pInput)
    {
        var returned = RegexHelpers.SplitKeyValue(pInput); //just returns a string like "Make = Honda" into three parts
        if (returned != null)
        {
            FieldName = returned.Item1;
            Operator = returned.Item2;
            Value = returned.Item3;
        }
    }
}

My parsing class is pretty long so I won't post the whole thing but it returns a List<object> where each element is either:

  • A QueryClass
  • "AND" or "OR"
  • Another List which means it's a group of queries that were grouped by parentheses, containing the two choices above.

Here's an example of the List<object> I get after parsing a string:

enter image description here

I then just loop through each element, determine if the value is a double or string, and execute a LINQ statement on my collection. I'm checking if the operator was "AND" or "OR" (or none if it's just one query), if it's part of a group or not, and combining the results appropriately.

8
  • 1
    Good description but please include the code so we can better assess and try to help you! Commented Aug 14, 2019 at 17:10
  • 2
    Are you aware of Expression trees? From what I know this is basically the technology that makes LINQ to basically anything possible: learn.microsoft.com/en-us/dotnet/csharp/programming-guide/… Commented Aug 14, 2019 at 17:23
  • 1
    Checkout System.Linq.Dynamic nuget library they do a good job in meeting many of these conditions out of box or you surely need ExpessionTrees where you can create Binary expression and method call expression to achieve what you are targeting Commented Aug 14, 2019 at 17:35
  • 1
    Can you provide a little more detail on the class in the collection - specifically, when you say "contains a Dictionary<string, string> what does that mean programmatically? Commented Aug 14, 2019 at 17:51
  • 1
    I would consider using LINQKit that provides helpers for building Expression trees for LINQ queries. Commented Aug 14, 2019 at 17:53

3 Answers 3

2

Here is my implementation of converting your query into a Func. Since I wasn't sure what type was in your collection, I made an interface to represent objects that had an attributes Dictionary<string, string> and processed that.

Basically I added a method to QueryClass to convert it to an Expression. It uses a helper dictionary string->lambda that builds the appropriate comparison Expression for each operator. Then I added a class to convert the List<object> into a Func<IItem,bool> suitable for a LINQ Where filter.

public interface IItem {
    Dictionary<string, string> attributes { get; set; }
}

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

    public QueryClass(string pInput) {
        var returned = RegexHelpers.SplitKeyValue(pInput); //just returns a string like "Make = Honda" into three parts
        if (returned != null) {
            FieldName = returned.Item1;
            Operator = returned.Item2;
            Value = returned.Item3;
        }
    }

    static MethodInfo getItemMI = typeof(Dictionary<string, string>).GetMethod("get_Item");
    static Dictionary<string, Func<Expression, Expression, Expression>> opTypes = new Dictionary<string, Func<Expression, Expression, Expression>> {
        { "==", (Expression lhs, Expression rhs) => Expression.MakeBinary(ExpressionType.Equal, lhs, rhs) },
        { ">=", (Expression lhs, Expression rhs) => Expression.MakeBinary(ExpressionType.GreaterThanOrEqual, Expression.Call(lhs, typeof(String).GetMethod("CompareTo", new[] { typeof(string) }), rhs), Expression.Constant(0)) },
        { "CONTAINS",  (Expression lhs, Expression rhs) => Expression.Call(lhs, typeof(String).GetMethod("Contains"), rhs) }
    };
    static MemberInfo attribMI = typeof(IItem).GetMember("attributes")[0];

    public Expression AsExpression(ParameterExpression p) {
        var dictField = Expression.MakeMemberAccess(p, attribMI);
        var lhs = Expression.Call(dictField, getItemMI, Expression.Constant(FieldName));
        var rhs = Expression.Constant(Value);

        if (opTypes.TryGetValue(Operator, out var exprMakerFn))
            return exprMakerFn(lhs, rhs);
        else
            throw new InvalidExpressionException($"Unrecognized operator {Operator}");
    }
}

public class LinqBuilder {
    static Type TItems = typeof(IItem);

    static Expression BuildOneLINQ(object term, ParameterExpression parm) {
        switch (term) {
            case QueryClass qc: // d => d[qc.FieldName] qc.Operator qc.Value
                return qc.AsExpression(parm);
            case List<object> subQuery:
                return BuildLINQ(subQuery, parm);
            default:
                throw new Exception();
        }
    }

    static Expression BuildLINQ(List<object> query, ParameterExpression parm) {
        Expression body = null;
        for (int queryIndex = 0; queryIndex < query.Count; ++queryIndex) {
            var term = query[queryIndex];
            switch (term) {
                case string op:
                    var rhs = BuildOneLINQ(query[++queryIndex], parm);
                    var eop = (op == "AND") ? ExpressionType.AndAlso : ExpressionType.OrElse;
                    body = Expression.MakeBinary(eop, body, rhs);
                    break;
                default:
                    body = BuildOneLINQ(term, parm);
                    break;
            }
        }

        return body;
    }

    public static Func<IItem, bool> BuildLINQ(List<object> query) {
        var parm = Expression.Parameter(TItems, "i");
        return Expression.Lambda<Func<IItem, bool>>(BuildLINQ(query, parm), parm).Compile();
    }
}

Once you have this, you can pass in a List<object> expression and then filter your collection. Given a query q and a collection of IItems cs, you can do:

var ans = cs.Where(LinqBuilder.BuildLINQ(q));
Sign up to request clarification or add additional context in comments.

Comments

2

I would approach this problem little differently, since what you are already having is List<object>, which internally contain a QueryClass containing all the relevant fields containing information, FieldName,Operator and Value, where you are aware which of the binary expressions have to be bundled in a parentheses. Important point is how can you create a Run-time Expression to take care of all kinds of scenarios.

Following is a sample to mimic your scenario:

Sample Class

public class Car
{
    public string Make {get; set;}

    public string Model {get; set;}

    public int Year {get; set;}
}

Query

((c.Make.Equals("Honda") AndAlso c.Model.Contains("Civic")) Or (c.Year >= 2015))

Linqpad Code

void Main()
{
    var cars = new List<Car>();

    Expression<Func<Car,bool>> e1 = c => c.Make.Equals("Honda");

    Expression<Func<Car,bool>> e2 = c => c.Model.Contains("Civic");

    Expression<Func<Car,bool>> e3 = c => c.Year >= 2015;

    var expr1 = Expression.AndAlso(e1.Body,e2.Body);

    var expr2 = e3;

    var finalExpression = Expression.Or(expr1,expr2.Body);

    finalExpression.Dump();
}

Purpose

As it can be seen that I have manually constructed the Expressions and finally Dump the final expression, since in Linqpad it provides a graphical representation of how shall the Expression be constructed dynamically, overall image is too big and deep to be pasted here (you may try yourself using LinqPad), but following are the relevant details:

  1. Create a ParameterExpression, this acts as a lambda parameter representing the Car class object (this is independent of the fields of the Query class)

    var parameterExpression = Expression.Parameter(typeof(Car),"c");

  2. Create MemberExpression to access each relevant field of the Car class, which is used in the Query (this one needs Field property of the Query class)

    var makeMemberAccessExpression = Expression.MakeMemberAccess(parameterExpression, typeof(Car).GetProperty("Make"));
    
    var modelMemberAccessExpression = Expression.MakeMemberAccess(parameterExpression, typeof(Car).GetProperty("Model"));
    
    var yearMemberAccessExpression = Expression.MakeMemberAccess(parameterExpression, typeof(Car).GetProperty("Year"));
    
  3. Completing the Expression:

a.) c => c.Make.Equals("Honda") we create as follows: (this one needs Value property of the QueryClass)

var makeConstantExpression = Expression.Constant("Honda");

var makeEqualExpression = Expression.Equal(makeMemberAccessExpression, makeConstantExpression);

b.) c.Model.Contains("Civic") can be represented as follows here we need supply the MethodInfo for the string Contains method and create a MethodCallEXpression

var modelConstantExpression = Expression.Constant("Civic");

var stringContainsMethodInfo = typeof(string).GetMethod("Contains", new[] { typeof(string) });

var modelContainsMethodExpression = Expression.Call(modelMemberAccessExpression, stringContainsMethodInfo, modelConstantExpression);

c.) c.Year >= 2015 can simply projected as:

var yearConstantExpression = Expression.Constant(2015);

var yearGreaterThanEqualExpression = Expression.GreaterThanOrEqual(yearMemberAccessExpression, yearConstantExpression);
  1. Combining all together to form the Composite Expression:

Expressions a.) and b.) are combined together as follows:

((c.Make.Equals("Honda") AndAlso c.Model.Contains("Civic"))

var firstExpression = Expression.AndAlso(makeEqualExpression,modelContainsMethodExpression);

Expression c.) is independent:

c.Year >= 2015
var secondExpression = yearGreaterThanEqualExpression;
  1. Final Combined Expression and Creation a Func Delegate

    // Expressions combined via Or (||)
    var finalCombinedExpression = Expression.Or(firstExpression,secondExpression);
    
    // Create Lambda Expression
    var lambda = Expression.Lambda<Func<Car,bool>>(finalCombinedExpression, parameterExpression);
    
    // Create Func delegate via Compilation
    var func = lambda.Compile();
    

func delegate thus can be used in any of the where clause in the Linq, which expects Func<Car,bool>

Design Suggestions

  1. Using the explanation above and values from the Query Class placeholders or directly from the Dictionary it is feasible to create any number of Expressions to be used dynamically in the code, to be compiled and used as a Func delegate
  2. Binary expressions like Equal, GreaterThan, LessThan, LessThanOrEqual,GreaterThanOrEqual are all exposed by the Expression trees to be used directly, For method like Contains available by default you need Reflection to get theb MethodInfo, similarly it can be done for the Static methods, just that there's object expressions
  3. All Expressions expect values to be supplied Left to Right in the correct order, it cannot be random or incorrect order, else it will fail at run-time.
  4. In your case, since you have few queries combined in parentheses and few independent, I would recommend creating multiple List<Expression>, where each List is combined in parentheses using AndAlso or OrElse and each list can thus be combined using And / Or

By this approach you shall be able to construct very complex requirements at runtime using Linq Expressions.

Comments

1

You should be able to use Linq Expressions (System.Linq.Expressions) and leverage predicates to handle your filtering.

public IQueryable<Car> GetCars(Expression<Func<Car, bool>> filter)
{
   return context.Cars.Where(filter);
}

That said, the challenge will be to build your predicate expressions based off of your custom QueryClass object. To handle the filter on each Dictionary you can create a method to handle each:

public Expression<Func<Car, bool>> GetModelFilter(QueryClass modelQuery)
{
    return modelQuery.Operator == "CONTAINS"? car => car.Model.Contains(modelQuery.Value) : car => car.Model == modelQuery.Value;
}

Considering you have a limited amount of filters, the above may be acceptable. However, when dealing with a large set this can also be done more dynamically, using reflection or a dynamic predicate builder but for simplicity you can follow the above.

HTH

1 Comment

Your construction of the Expression tree is not correct as its manual if else, this shall be able to construct the Expression tree automatically, whether Binary or Method Call Expression

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.