【问题标题】:How to Parse OData $filter in C#如何在 C# 中解析 OData $filter
【发布时间】:2018-07-04 12:09:19
【问题描述】:

操作 odata 过滤器

我如何在后端操作过滤器并想要过滤器查询参数的键值对?

下面要表达的意思

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

由于连接了 2 个过滤器,我如何获得类似的值

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

我试过了

  • ODataUriParser,但它似乎不支持 ASP.NET core 2.1 web api。
  • 正则表达式 - 使用这个stack overflow question,它在我的情况下似乎不起作用,因为我的第三个过滤器在值 & 中包含 ,因此正则表达式失败。
  • ODataQueryOptions 在方法中,但它会将无法提取的原始文本提供给前面提到的键值对。

我正在使用 ASP.NET Core 2.1 Web API 与 OData v4 集成

有没有办法做到以上几点?

【问题讨论】:

  • 我也有类似的问题。我很好奇你找到解决方案了吗?
  • 只是想知道为什么需要解析密钥对?也许还有另一种更简单的方法可以在不解析的情况下实现相同的效果。例如(假设您想在进行查询时应用限制访问的默认约束,您可以直接应用这些约束,或使用多租户)
  • @Pradeep 如果你需要一个基于正则表达式的解决方案,你应该解释模式要求、应该匹配什么、在什么上下文中等等。用正则表达式解析这样的任意查询不是一个好主意。虽然 .NET 提供了一些很酷的特性,但它仍然不支持递归。您可能会使用正则表达式获得足够好的解决方案,但不太可能达到 100%。

标签: c# regex odata asp.net-core-2.1


【解决方案1】:

您可能需要考虑定义自己的解析器,然后遍历AST 以获得所需的值。有很多工具可以做到这一点(例如,参见 flexbison)。但在 .net 世界中,Irony 可能是一个可行的选择:它在 .net standard 2.0 中可用,我在插入 .net core 2.1 控制台测试项目时没有问题。

首先,您通常需要定义一个语法。幸运的是,微软对supply us with EBNF reference 非常友好,所以我们要做的就是让它适应 Irony。我最终实现了上面语法的一个子集,它似乎可以满足您的示例语句(有点超出预期,请随意削减)。

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("(", ")");
        }
    }
}

一点提示:Irony 带有 Grammar Explorer 工具,允许您加载语法 dll 并使用它们进行调试,所以我建议您将您的类放在自己的项目中。然后,您将更容易理解这些概念:

对语法满意后,需要从项目中引用它并解析输入字符串:

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 
    }
}

然后,您所要做的就是遍历生成的树并根据语法节点类型应用您的自定义逻辑。在this SO answer 中可以找到一个如何做到这一点的示例。

根据您的要求,您可能会发现这对于您的目的来说完全是矫枉过正,或者实际上可能会发现它为您提供的控制水平是完全正确的。

【讨论】:

  • 对于这种类型的场景,定义语法和创建 AST 没有意义,OData 已经有自己的解析器,可以让您解析表达式。最重要的是,他们为他们的语法定义了一个访问者模式。因此,如果您基本上使用您的解决方案,您将对 AST 进行大量访问,因为默认情况下,当您使用 nuget 包时,当您在后端收到原始值并将它们绑定到您的语法 AST 时,它们会默认执行此操作访问这棵新树以执行您需要的转换。也会影响性能
【解决方案2】:

我知道这不是解决方案,但与您分享以防万一以后对您有所帮助。这是为了匹配':'右侧的所有值

/(?<=: )[\w ]+/gm

【讨论】:

  • ':' 与这个问题有什么关系。 OP 想要那种格式的值,OP 有它的 OData 格式,没有':'
猜你喜欢
  • 2014-02-23
  • 2017-02-10
  • 2021-01-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-07-29
  • 1970-01-01
相关资源
最近更新 更多