如何在C#中解析OData $filter

10

操作odata过滤器

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

表达式应该像下面这样

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

由于有两个连接的筛选器,我该如何获取值?

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问题,但在我的第三个筛选器中包含and,所以正则表达式失败了。

在方法中使用ODataQueryOptions,但它提供的是原始文本,无法提取到像上述那样的键值对。

我正在使用ASP.NET Core 2.1 Web API与OData v4集成,有没有办法实现上述功能?


我有一个类似的问题。不知道你找到解决方法了吗? - Charlie
将ODataQueryOptions转换为C#中的LINQ表达式 - Reza Aghaei
如何将OData查询字符串转换为.NET表达式树 - Reza Aghaei
@Pradeep 如果你需要一个基于正则表达式的解决方案,你应该解释一下模式要求,需要匹配什么,在什么上下文中等等。使用正则表达式解析任意查询并不是一个好主意。虽然.NET提供了一些很酷的功能,但它仍然不支持递归。你可能会得到一个足够好的正则表达式解决方案,但100%的准确率是不太可能的。 - Wiktor Stribiżew
你说ODataUriParser在ASP.NET Core 2.1 Web API中“似乎不支持”是什么意思?它作为一个.NET Standard 1.1包是可用的,因此你应该能够从任何.NET Core或ASP.NET Core应用程序中调用它。 - yaakov
显示剩余2条评论
2个回答

3
你可能需要考虑定义自己的解析器,然后遍历AST以获取所需值。有许多工具可以做到这一点(例如flexbison)。但在.net世界中,Irony可能是一个可行的选择:它在.net standard 2.0中可用,我没有在.net core 2.1控制台测试项目中遇到任何问题。
首先,你通常需要定义语法。幸运的是,微软很好地为我们提供了EBNF参考资料,所以我们所要做的就是将其适应于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 
    }
}

然后,您需要遍历生成的树,并根据语法节点类型应用自定义逻辑。如何实现可以在这个SO答案中找到一个例子。

根据您的要求,您可能会发现这对于您的目的来说是一种过度设计,或者实际上可能会发现它给了您恰当的控制级别。


对于这种情况,定义语法并创建AST没有意义,因为OData已经有了自己的解析器,可以让您解析表达式。除此之外,他们还为其语法定义了访问者模式。因此,如果您使用自己的解决方案,基本上会对AST进行大量访问,因为当您使用nuget包时,它们默认执行此操作,然后在后端接收原始值并将其绑定到语法AST以稍后访问此新树来执行所需的转换。这也会影响性能。 - Zinov

-2

我知道这不是解决方案,但如果以后有帮助的话,与您分享一下。这是为了匹配“:”右侧的所有值。

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

冒号(:)与这个问题有什么关系?提问者想要以那种格式显示数值,但是现在它们是在OData格式下且没有冒号。 - johnny 5

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接