10

Manipulate odata filter

How can i manipulate filter in the backend and want the key value pairs of the filter query parameters?

Expression would like below

"?$filter =((Name eq 'John' or Name eq 'Grace Paul') and (Department eq 'Finance and Accounting'))"

As there are 2 filters concatenated & how can i get the values like

Filter 1:
    Key: Name
    Operator: eq
    Value: Name

Operator: or

Filter 2:
    Key: Name
    Operator: eq
    Value: Grace Paul

Operator: and

Filter 3:
    Key: Department
    Operator: eq
    Value: Finance and Accounting

I tried with

  • ODataUriParser, but it doesn't seems to support in ASP.NET core 2.1 web api.
  • Regular Expression - using this stack overflow question, it doesn't seem to work in my case as my 3rd filter contains and in the value & so the regular expression fails.
  • ODataQueryOptions in the method, but it gives the raw text where it cannot be extracted to the key value pairs like mentioned.

I'm using ASP.NET Core 2.1 Web API with OData v4 integration

Is there a way to accomplish the above?

Boghyon Hoffmann
  • 17,103
  • 12
  • 72
  • 170
user7932844
  • 351
  • 1
  • 4
  • 12
  • I'm have a similar question. I'm curious did you find a solution? – Charlie Sep 30 '19 at 08:43
  • Just wondering Why you need to parse the Key pairs? Maybe there is another easier way to achieve the same affect without parsing. E.g (Say you wanted to apply default constraints which restrict access when making a query, you can apply these directly prior, or using multi-tennancy) – johnny 5 Feb 05 '20 at 21:15
  • [Converting ODataQueryOptions into LINQ Expressions in C#](https://d-fens.ch/2017/03/01/converting-odataqueryoptions-into-linq-expressions-in-c/) – Reza Aghaei Feb 05 '20 at 21:20
  • [How to convert an OData query string to .NET expression tree](https://stackoverflow.com/a/42740486/3110834) – Reza Aghaei Feb 05 '20 at 21:22
  • @Pradeep If you need a regex based solution, you should explain the pattern requirements, what should be matched, in what context, etc. Parsing arbitrary queries like this with regex is not a good idea. While .NET provides some cool features, it still does not support recursion. You may get a good-enough solution with regex, but a 100% is unlikely. – Wiktor Stribiżew Feb 07 '20 at 21:19
  • What do you mean that ODataUriParser "doesn't seems to support in ASP.NET core 2.1 web api"? It's available as a .NET Standard 1.1 package, so you should be able to call it from any .NET Core or ASP.NET Core application. – yaakov Feb 08 '20 at 13:50
  • Perhaps this could help: https://www.nuget.org/packages/Microsoft.OData.Core –  Feb 11 '20 at 00:10

2 Answers2

3

You may want to consider defining your own parser and then walking the AST to get desired values. There are plenty tools to do that (see flex or bison for example). But in .net world Irony might be a viable option: it's available in .net standard 2.0 which I had no issues plugging into a .net core 2.1 console test project.

To start off, you normally need to define a grammar. Luckily, Microsoft have been kind enough to supply us with EBNF reference so all we have to do is to adapt it to Irony. I ended up implementing a subset of the grammar above that seems to cater for your example statement (and a bit above and beyond, feel free to cut it down).

using Irony.Parsing;

namespace irony_playground
{
    [Language("OData", "1.0", "OData Filter")]
    public class OData: Grammar
    {
        public OData()
        {
            // first we define some terms
            var identifier = new RegexBasedTerminal("identifier", "[a-zA-Z_][a-zA-Z_0-9]*");
            var string_literal = new StringLiteral("string_literal", "'");
            var integer_literal = new NumberLiteral("integer_literal", NumberOptions.IntOnly);
            var float_literal = new NumberLiteral("float_literal", NumberOptions.AllowSign|NumberOptions.AllowSign) 
                                        | new RegexBasedTerminal("float_literal", "(NaN)|-?(INF)");
            var boolean_literal = new RegexBasedTerminal("boolean_literal", "(true)|(false)");

            var filter_expression = new NonTerminal("filter_expression");
            var boolean_expression = new NonTerminal("boolean_expression");
            var collection_filter_expression = new NonTerminal("collection_filter_expression");
            var logical_expression = new NonTerminal("logical_expression");
            var comparison_expression = new NonTerminal("comparison_expression");
            var variable = new NonTerminal("variable");
            var field_path = new NonTerminal("field_path");
            var lambda_expression = new NonTerminal("lambda_expression");
            var comparison_operator = new NonTerminal("comparison_operator");
            var constant = new NonTerminal("constant");

            Root = filter_expression; // this is where our entry point will be. 

            // and from here on we expand on all terms and their relationships
            filter_expression.Rule = boolean_expression;

            boolean_expression.Rule = collection_filter_expression
                                      | logical_expression
                                      | comparison_expression
                                      | boolean_literal
                                      | "(" + boolean_expression + ")"
                                      | variable;
            variable.Rule = identifier | field_path;

            field_path.Rule = MakeStarRule(field_path, ToTerm("/"), identifier);

            collection_filter_expression.Rule =
                field_path + "/all(" + lambda_expression + ")"
                | field_path + "/any(" + lambda_expression + ")"
                | field_path + "/any()";

            lambda_expression.Rule = identifier + ":" + boolean_expression;

            logical_expression.Rule =
                boolean_expression + (ToTerm("and", "and") | ToTerm("or", "or")) + boolean_expression
                | ToTerm("not", "not") + boolean_expression;

            comparison_expression.Rule =
                variable + comparison_operator + constant |
                constant + comparison_operator + variable;

            constant.Rule =
                string_literal
                | integer_literal
                | float_literal
                | boolean_literal
                | ToTerm("null");

            comparison_operator.Rule = ToTerm("gt") | "lt" | "ge" | "le" | "eq" | "ne";

            RegisterBracePair("(", ")");
        }
    }
}

A bit of a hint: Irony comes with Grammar Explorer tool that allows you to load grammar dlls and debug with them, so I'd suggest you put your class in its own project. Then you would have easier time wrapping your head around the concepts: enter image description here

after you're happy with the grammar, you need to reference it from your project and parse the input string:

class Program
{
    static void Main(string[] args)
    {
        var g = new OData();
        var l = new LanguageData(g);
        var r = new Parser(l);
        var p = r.Parse("((Name eq 'John' or Name eq 'Grace Paul') and (Department eq 'Finance and Accounting'))"); // here's your tree
        // this is where you walk it and extract whatever data you desire 
    }
}

Then, all you have to do is walk the resulting tree and apply your custom logic based on sytax node type. One example how to do that can be found in this SO answer.

Depending on your requirements, you might find this is going to be a total overkill for your purpose, or might actually find that level of control it gives you is exactly right.

timur
  • 14,239
  • 2
  • 11
  • 32
  • for this type of scenario it doesn't make sense to define a grammar and create the AST, OData has already its own parser that allows you to parse your expression. On top of that they have a visitor pattern defined for their grammar. So if you go with your solution basically you are doing a lot of visits to the AST, because they do it by default when you use the nuget package then when you receive the raw values on the backend and binding them to your grammar AST to later visit this new tree to perform the transformation you need. Also will hurt the performance – Zinov Aug 24 '21 at 20:06
-1

I know this is not the solution but sharing with you just in case if it helps you later. This is to match all values on the right side of ':'

/(?<=: )[\w ]+/gm
Dhruvil21_04
  • 1,804
  • 2
  • 17
  • 31
  • What does ':' have to do with this problem. The OP wants the values in that format, the OP has it in OData format which has no ':' – johnny 5 Feb 05 '20 at 21:17