3

I am trying to make a dynamic filtering mechanism with c#.

I have user class with properties like Name, Surname, BirthDate etc. and Also UserInformations list which is a list of a UserInformation object. UserInformation object has attributes Id, Name and Value. And userList from a list of users.

I created a UI to create filter page using https://querybuilder.js.org/demo.html

It generates filter like

{
  "condition": "AND",
  "rules": [
    {
     "id": "name",
      "field": "name",
      "type": "string",
      "input": "text",
      "operator": "equal",
      "value": "huseyin"
    },
    {
      "condition": "OR",
      "rules": [
        {
          "id": "surname",
          "field": "surname",
          "type": "string",
          "input": "text",
          "operator": "equal",
          "value": "34"
        },
        {
          "id": "ix_2_Weight",
          "field": "ix_2_Weight",
          "type": "integer",
          "input": "select",
          "operator": "equal",
          "value": 50
        }
      ]
    }
  ],
  "valid": true
}

I have no problem with filters with attributes of User. However, I am trying to filter informations in UserInformations Array. I am splitting field name if its start with ix and get id of the user information. But i could not find how to compare its value. if it were a static linq, it shoud have been like

users.Where(u=>u.Informations.FirstOrDefault(i=>i.id==2/*id of information in filter*/)?.Value=="something"))

Filter is called with below code

 private async Task<List<User>> FilterUsers(FilterRule rule, List<UserFilterDto> users = null)
        {
            List<User> list = users ?? await GetUsersForFilter();

            var tempList = list.BuildQuery(rule).ToList();
            return tempList;

        }

And Dynamic query part is as below.


  public static IQueryable<User> BuildQuery(this IQueryable<User> queryable, FilterRule filterRule, out string parsedQuery, bool useIndexedProperty = false, string indexedPropertyName = null)
        {
            if (filterRule == null)
            {
                parsedQuery = "";
                return queryable;
            }

            var pe = Expression.Parameter(typeof(UserDetailedDto), "item");

            var expressionTree = BuildExpressionTree(pe, filterRule, useIndexedProperty, indexedPropertyName);
            if (expressionTree == null)
            {
                parsedQuery = "";
                return queryable;
            }

            parsedQuery = expressionTree.ToString();

            var whereCallExpression = Expression.Call(
                typeof(Queryable),
                "Where",
                new[] { queryable.ElementType },
                queryable.Expression,
                Expression.Lambda<Func<UserDetailedDto, bool>>(expressionTree, pe));

            var filteredResults = queryable.Provider.CreateQuery<UserDetailedDto>(whereCallExpression);

            return filteredResults;

        }


public static IQueryable<User> BuildQuery(this IQueryable<User> queryable, FilterRule filterRule, out string parsedQuery, bool useIndexedProperty = false, string indexedPropertyName = null)
        {
            if (filterRule == null)
            {
                parsedQuery = "";
                return queryable;
            }

            var pe = Expression.Parameter(typeof(UserDetailedDto), "item");

            var expressionTree = BuildExpressionTree(pe, filterRule, useIndexedProperty, indexedPropertyName);
            if (expressionTree == null)
            {
                parsedQuery = "";
                return queryable;
            }

            parsedQuery = expressionTree.ToString();

            var whereCallExpression = Expression.Call(
                typeof(Queryable),
                "Where",
                new[] { queryable.ElementType },
                queryable.Expression,
                Expression.Lambda<Func<UserDetailedDto, bool>>(expressionTree, pe));

            var filteredResults = queryable.Provider.CreateQuery<UserDetailedDto>(whereCallExpression);

            return filteredResults;

        }

        private static Expression BuildExpressionTree(ParameterExpression pe, FilterRule rule, bool useIndexedProperty = false, string indexedPropertyName = null)
        {

            if (rule.Rules != null && rule.Rules.Any())
            {
                var expressions =
                    rule.Rules.Select(childRule => BuildExpressionTree(pe, childRule, useIndexedProperty, indexedPropertyName))
                        .Where(expression => expression != null)
                        .ToList();

                var expressionTree = expressions.First();

                var counter = 1;
                while (counter < expressions.Count)
                {
                    expressionTree = rule.Condition.ToLower() == "or"
                        ? Expression.Or(expressionTree, expressions[counter])
                        : Expression.And(expressionTree, expressions[counter]);
                    counter++;
                }

                return expressionTree;
            }
            if (rule.Field != null)
            {
                Type type;

                switch (rule.Type)
                {
                    case "integer":
                        type = typeof(int);
                        break;
                    case "long":
                        type = typeof(long);
                        break;
                    case "double":
                        type = typeof(double);
                        break;
                    case "string":
                        type = typeof(string);
                        break;
                    case "date":
                    case "datetime":
                        type = typeof(DateTime);
                        break;
                    case "boolean":
                        type = typeof(bool);
                        break;
                    default:
                        throw new Exception($"Unexpected data type {rule.Type}");
                }

                Expression propertyExp = null;

                if (rule.Field.StartsWith("ix"))
                {
                    long informationId =long.Parse(rule.Field.Split("_")[1]);

                  ????
????

                } else if (useIndexedProperty)
                {
                    propertyExp = Expression.Property(pe, indexedPropertyName, Expression.Constant(rule.Field));
                }
                else
                {
                    propertyExp = Expression.Property(pe, rule.Field);
                }

                Expression expression;

                if (propertyExp.Type.Name.Contains("ICollection") || propertyExp.Type.Name.Contains("List"))
                {
                    // Rule Field is a Collection
                    expression = BuildCollectionExpression(pe, rule);
                }
                else
                {

                    switch (rule.Operator.ToLower())
                    {
                        case "in":
                            expression = In(type, rule.Value, propertyExp);
                            break;
                        case "not_in":
                            expression = NotIn(type, rule.Value, propertyExp);
                            break;
                        case "equal":
                            expression = Equals(type, rule.Value, propertyExp);
                            break;
                        case "not_equal":
                            expression = NotEquals(type, rule.Value, propertyExp);
                            break;
                        case "between":
                            expression = Between(type, rule.Value, propertyExp);
                            break;
                        case "not_between":
                            expression = NotBetween(type, rule.Value, propertyExp);
                            break;
                        case "less":
                            expression = LessThan(type, rule.Value, propertyExp);
                            break;
                        case "less_or_equal":
                            expression = LessThanOrEqual(type, rule.Value, propertyExp);
                            break;
                        case "greater":
                            expression = GreaterThan(type, rule.Value, propertyExp);
                            break;
                        case "greater_or_equal":
                            expression = GreaterThanOrEqual(type, rule.Value, propertyExp);
                            break;
                        case "begins_with":
                            expression = BeginsWith(type, rule.Value, propertyExp);
                            break;
                        case "not_begins_with":
                            expression = NotBeginsWith(type, rule.Value, propertyExp);
                            break;
                        case "contains":
                            expression = Contains(type, rule.Value, propertyExp);
                            break;
                        case "not_contains":
                            expression = NotContains(type, rule.Value, propertyExp);
                            break;
                        case "ends_with":
                            expression = EndsWith(type, rule.Value, propertyExp);
                            break;
                        case "not_ends_with":
                            expression = NotEndsWith(type, rule.Value, propertyExp);
                            break;
                        case "is_empty":
                            expression = IsEmpty(propertyExp);
                            break;
                        case "is_not_empty":
                            expression = IsNotEmpty(propertyExp);
                            break;
                        case "is_null":
                            expression = IsNull(propertyExp);
                            break;
                        case "is_not_null":
                            expression = IsNotNull(propertyExp);
                            break;
                        default:
                            throw new Exception($"Unknown expression operator: {rule.Operator}");
                    }

                }
                return expression;


            }
            return null;

        }
  • Why dont you build Where stats of type string and convert it back to linq using Dynamic.linq library. it much easer to handle where you write code of type string and convert it back to code – Alen.Toma Apr 29 '19 at 00:23
  • Similiar to `string expression ="x.Person.FirstName.EndsWith(\"n\") AND (x.Person.FirstName.Contains(\"a\") OR x.Person.FirstName.StartsWith(\"a\"))";` And using Dynamic.linq – Alen.Toma Apr 29 '19 at 00:24
  • @Alen.Toma Thanks for your comment however for complex filters string manipulation can be serious headache. – Huseyin Simsek Apr 29 '19 at 10:55
  • using SQL query builders is more appropriate in your case – amd May 10 '19 at 08:48

1 Answers1

2

if it were a static LINQ, it should have been like

u => u.Informations.FirstOrDefault(i => i.id == 2)?.Value == "something"))

Operator ?. is not supported in expression trees, so it's better to build something like this instead:

u => u.Informations.Any(i => i.id == 2 && i.Value == "something"))

In order to do that, extract the raw expression building in a separate method:

private static Expression BuildCondition(FilterRule rule, Expression propertyExp)
{
    Type type;

    switch (rule.Type)
    {
        case "integer":
            type = typeof(int);
            break;
        case "long":
            type = typeof(long);
            break;
        case "double":
            type = typeof(double);
            break;
        case "string":
            type = typeof(string);
            break;
        case "date":
        case "datetime":
            type = typeof(DateTime);
            break;
        case "boolean":
            type = typeof(bool);
            break;
        default:
            throw new Exception($"Unexpected data type {rule.Type}");
    }

    Expression expression;

    switch (rule.Operator.ToLower())
    {
        case "in":
            expression = In(type, rule.Value, propertyExp);
            break;
        case "not_in":
            expression = NotIn(type, rule.Value, propertyExp);
            break;
        case "equal":
            expression = Equals(type, rule.Value, propertyExp);
            break;
        case "not_equal":
            expression = NotEquals(type, rule.Value, propertyExp);
            break;
        case "between":
            expression = Between(type, rule.Value, propertyExp);
            break;
        case "not_between":
            expression = NotBetween(type, rule.Value, propertyExp);
            break;
        case "less":
            expression = LessThan(type, rule.Value, propertyExp);
            break;
        case "less_or_equal":
            expression = LessThanOrEqual(type, rule.Value, propertyExp);
            break;
        case "greater":
            expression = GreaterThan(type, rule.Value, propertyExp);
            break;
        case "greater_or_equal":
            expression = GreaterThanOrEqual(type, rule.Value, propertyExp);
            break;
        case "begins_with":
            expression = BeginsWith(type, rule.Value, propertyExp);
            break;
        case "not_begins_with":
            expression = NotBeginsWith(type, rule.Value, propertyExp);
            break;
        case "contains":
            expression = Contains(type, rule.Value, propertyExp);
            break;
        case "not_contains":
            expression = NotContains(type, rule.Value, propertyExp);
            break;
        case "ends_with":
            expression = EndsWith(type, rule.Value, propertyExp);
            break;
        case "not_ends_with":
            expression = NotEndsWith(type, rule.Value, propertyExp);
            break;
        case "is_empty":
            expression = IsEmpty(propertyExp);
            break;
        case "is_not_empty":
            expression = IsNotEmpty(propertyExp);
            break;
        case "is_null":
            expression = IsNull(propertyExp);
            break;
        case "is_not_null":
            expression = IsNotNull(propertyExp);
            break;
        default:
            throw new Exception($"Unknown expression operator: {rule.Operator}");
    }

    return expression;
}

use it inside current branch for direct members, and use the following for "indexed" branch:

if (rule.Field.StartsWith("ix_"))
{
    var tokens = rule.Field.Split("_");
    var infoParameter = Expression.Parameter(typeof(UserInformation), "i");
    var infoCondition = Expression.AndAlso(
        Expression.Equal(
            Expression.Property(infoParameter, nameof(UserInformation.Id)),
            Expression.Constant(long.Parse(tokens[1]))),
        BuildCondition(rule, Expression.Property(infoParameter, tokens[2])));
    return Expression.Call(
        typeof(Enumerable), nameof(Enumerable.Any), new[] { infoParameter.Type },
        Expression.Property(pe, nameof(User.Informations)),
        Expression.Lambda(infoCondition, infoParameter));
}

As a side note, the C# logical && and || operators are represented by Expression.AndAlso and Expression.OrElse.

Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343