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;

        }
4
  • 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 Commented Apr 29, 2019 at 0: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 Commented Apr 29, 2019 at 0:24
  • @Alen.Toma Thanks for your comment however for complex filters string manipulation can be serious headache. Commented Apr 29, 2019 at 10:55
  • using SQL query builders is more appropriate in your case Commented May 10, 2019 at 8:48

1 Answer 1

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.

Sign up to request clarification or add additional context in comments.

Comments

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.