【问题标题】:How to parse OData $filter with regular expression in C#?如何在 C# 中使用正则表达式解析 OData $filter?
【发布时间】:2014-02-23 01:48:43
【问题描述】:

您好,我想知道在 C# 中解析 OData $filter 字符串的最佳方法是什么,例如

/API/organisations?$filter="name eq 'Facebook' 或 name eq 'Twitter' 和订阅者 gt '30'"

应返回名称为 Facebook 或 Twitter 且订阅人数超过 30 人的所有组织。我已经研究了很多,但找不到任何不围绕 WCF 的解决方案。我正在考虑使用正则表达式并将它们分组,所以我有一个列表 过滤器类,例如:

Filter
    Resource: Name
    Operator: Eq
    Value: Facebook
Filter
    Resource: Name
    Operator: Eq
    Value: Twitter
Filter
    Resource: Subscribers
    Operator: gt
    Value: 30

但我不知道如何处理 AND / OR。

【问题讨论】:

  • 考虑使用实际的解析器工具包并按照规范工作,而不是在一堆 RE 中磕磕绊绊。我发现的文档提到了带有过滤器表达式语法的“规范 OData 规范”。

标签: c# regex odata


【解决方案1】:

在 .NET 中,有一个可用的库可以为您执行此操作。编写自己的正则表达式可能会丢失一些极端情况。

使用 NuGet,引入 Microsoft.Data.OData。然后,你可以这样做:

using Microsoft.Data.OData.Query;

var result = ODataUriParser.ParseFilter(
  "name eq 'Facebook' or name eq 'Twitter' and subscribers gt 30",
  model,
  type);

result 这里将采用 AST 的形式表示过滤子句。

(要获取 modeltype 输入,您可以使用以下内容解析 $metadata 文件:

using Microsoft.Data.Edm;
using Microsoft.Data.Edm.Csdl;

IEdmModel model = EdmxReader.Parse(new XmlTextReader(/*stream of your $metadata file*/));
IEdmEntityType type = model.FindType("organisation");

)

【讨论】:

  • Jen,您或其他人能解释一下如何动态生成 EDM 吗?我的第一个想法是使用反射生成一个,但IEdmModel 有大量成员要实现,我希望避免必须了解每个成员的作用。
  • 有没有办法不使用 nuGet 只下载 C++ 源代码?是否有 Microsoft.Data.OData 的源存储库?
  • 请注意:此解决方案使用 Odata V 3.0。 (Microsoft.Data.OData)。较新的 OData V 4.0 需要 Microsoft.OData.Core
  • 有人能解释一下模型中的 XmlTextReader 参数应该给出什么吗?
【解决方案2】:

我认为您应该使用访问者模式提供的接口遍历 AST。

假设你有一个代表过滤器的类

public class FilterValue
{
    public string ComparisonOperator { get; set; }
    public string Value { get; set; }
    public string FieldName { get; set; }
    public string LogicalOperator { get; set; }
}

那么,我们如何将 OData 参数附带的过滤器“提取”到您的类中?

FilterClause 对象有一个 Expression 属性,它是一个从 QueryNode 继承的 SingleValueNode。 QueryNode 具有接受 QueryNodeVisitor 的 Accept 方法。

    public virtual T Accept<T>(QueryNodeVisitor<T> visitor);

是的,所以您必须实现自己的 QueryNodeVisitor 并完成您的工作。下面是一个未完成的示例(我不会覆盖所有可能的访问者)。

public class MyVisitor<TSource> : QueryNodeVisitor<TSource>
    where TSource: class
{ 
    List<FilterValue> filterValueList = new List<FilterValue>();
    FilterValue current = new FilterValue();
    public override TSource Visit(BinaryOperatorNode nodeIn)
    {
        if(nodeIn.OperatorKind == Microsoft.Data.OData.Query.BinaryOperatorKind.And 
            || nodeIn.OperatorKind == Microsoft.Data.OData.Query.BinaryOperatorKind.Or)
        {
            current.LogicalOperator = nodeIn.OperatorKind.ToString();
        }
        else
        {
            current.ComparisonOperator = nodeIn.OperatorKind.ToString();
        }
        nodeIn.Right.Accept(this);
        nodeIn.Left.Accept(this);
        return null;
    }
    public override TSource Visit(SingleValuePropertyAccessNode nodeIn)
    {
        current.FieldName = nodeIn.Property.Name;
        //We are finished, add current to collection.
        filterValueList.Add(current);
        //Reset current
        current = new FilterValue();
        return null;
    }

    public override TSource Visit(ConstantNode nodeIn)
    {
        current.Value = nodeIn.LiteralText;
        return null;
    }

}

然后,开火:)

MyVisitor<object> visitor = new MyVisitor<object>();
options.Filter.FilterClause.Expression.Accept(visitor);

当它遍历树时你的

visitor.filterValueList

应包含所需格式的过滤器。我确信需要做更多的工作,但如果你能做到这一点,我想你可以解决。

【讨论】:

  • 请注意:如果您使用的是 Odata V4.0,就像我正在做的那样,您需要改用 Microsoft.OData.Core.UriParser.TreeNodeKinds.BinaryOperatorKind.And
  • 感谢现实生活中的例子!但是为什么return current as TSource?这在任何地方都有用吗?我试图在所有被覆盖的 Visit(...) 方法中返回 null,它也能正常工作。是否有更详细的帮助来覆盖这些访问?我没能用谷歌搜索出来,这个答案似乎是实现QueryNodeVisitor的唯一可谷歌来源@
  • 其实我也不知道。但是签名说 return T 所以我就这么做了。
  • 如果需要,您可以将 Type T 选择为类似于 List 的内容,然后您不必直接公开内部 filterValueList。
【解决方案3】:

根据 Jen S 所说,您可以遍历 FilterClause 返回的 AST 树。

例如,您可以从控制器的查询选项中检索 FilterClause:

public IQueryable<ModelObject> GetModelObjects(ODataQueryOptions<ModelObject> queryOptions)        
    {
        var filterClause = queryOptions.Filter.FilterClause;

然后您可以使用如下代码遍历生成的 AST 树(借用自 this article):

var values = new Dictionary<string, object>();
TryNodeValue(queryOptions.Filter.FilterClause.Expression, values);

调用的函数是这样的:

public void TryNodeValue(SingleValueNode node, IDictionary<string, object> values)
    {
        if (node is BinaryOperatorNode )
        {
            var bon = (BinaryOperatorNode)node;
            var left = bon.Left;
            var right = bon.Right;

            if (left is ConvertNode)
            {
                var convLeft = ((ConvertNode)left).Source;

                if (convLeft is SingleValuePropertyAccessNode && right is ConstantNode)
                    ProcessConvertNode((SingleValuePropertyAccessNode)convLeft, right, bon.OperatorKind, values);
                else
                    TryNodeValue(((ConvertNode)left).Source, values);                    
            }

            if (left is BinaryOperatorNode)
            {
                TryNodeValue(left, values);
            }

            if (right is BinaryOperatorNode)
            {
                TryNodeValue(right, values);
            }

            if (right is ConvertNode)
            {
                TryNodeValue(((ConvertNode)right).Source, values);                  
            }

            if (left is SingleValuePropertyAccessNode && right is ConstantNode)
            {
                ProcessConvertNode((SingleValuePropertyAccessNode)left, right, bon.OperatorKind, values);
            }
        }
    }

    public void ProcessConvertNode(SingleValuePropertyAccessNode left, SingleValueNode right, BinaryOperatorKind opKind, IDictionary<string, object> values)
    {            
        if (left is SingleValuePropertyAccessNode && right is ConstantNode)
        {
            var p = (SingleValuePropertyAccessNode)left;

            if (opKind == BinaryOperatorKind.Equal)
            {
                var value = ((ConstantNode)right).Value;
                values.Add(p.Property.Name, value);
            }
        }
    }

然后您可以浏览列表字典并检索您的值:

 if (values != null && values.Count() > 0)
        {
            // iterate through the filters and assign variables as required
            foreach (var kvp in values)
            {
                switch (kvp.Key.ToUpper())
                {
                    case "COL1":
                        col1 = kvp.Value.ToString();
                        break;
                    case "COL2":
                        col2 = kvp.Value.ToString();
                        break;
                    case "COL3":
                        col3 = Convert.ToInt32(kvp.Value);
                        break;
                    default: break;
                }
            }
        }

这个例子相当简单,因为它只考虑“eq”评估,但就我的目的而言,它运行良好。 YMMV。 ;)

【讨论】:

    【解决方案4】:

    感谢@Stinky Buffalo 的回答。 在字典中添加重复键时,我会更改您的代码并解决错误。

    示例:

    CreateDate%20gt%202021-05-22T00:00:00Z%20and%20CreateDate%20lt%202021-05-26T00:00:00Z%20
    

    还有:

    BookRequestType%20eq%20%27BusDomestic%27%20or%20BookRequestType%20eq%20%27TrainDomestic%27%20or%20BookRequestType%20eq%20%27FlightDomestic%27%20
    

    以下代码对我来说效果很好:

    首先安装Install-Package Microsoft.Data.OData -Version 5.8.4 包。

    然后创建名为“ODataHelper”的类,然后复制以下代码:

        public class ODataHelper<T> where T : class
    {
        private static readonly TextInfo TextInfo = new CultureInfo("en-US", false).TextInfo;
    
        public static Dictionary<string, Tuple<object, ODataOperatorType>> ODataUriParser(
            ODataQueryOptions<T> queryOptions)
        {
            var dictFilters = new Dictionary<string, Tuple<object, ODataOperatorType>>();
    
            TryNodeValue(queryOptions.Filter?.FilterClause?.Expression, dictFilters);
    
            return dictFilters;
        }
    
        private static void TryNodeValue(SingleValueNode node,
            IDictionary<string, Tuple<object, ODataOperatorType>> dictFilters)
        {
            if (node is null)
                return;
    
            if (node is SingleValueFunctionCallNode valueFunction)
            {
                ParseSingleFunctionNode(valueFunction,
                    Enum.Parse<ODataOperatorType>(TextInfo.ToTitleCase(valueFunction.Name)), dictFilters);
            }
    
            if (node is BinaryOperatorNode binaryOperatorNode)
            {
                var left = binaryOperatorNode.Left;
                var right = binaryOperatorNode.Right;
    
                if (left is SingleValuePropertyAccessNode leftNodeRight && right is ConstantNode rightNodeRight)
                {
                    ParseSingleValueNode(
                        leftNodeRight,
                        rightNodeRight,
                        Enum.Parse<ODataOperatorType>(binaryOperatorNode.OperatorKind.ToString()),
                        dictFilters);
                }
    
                switch (left)
                {
                    case ConvertNode node1:
                    {
                        var convertLeft = node1.Source;
    
                        if (convertLeft is SingleValuePropertyAccessNode leftNodeLeft &&
                            right is ConstantNode rightNodeLeft)
                        {
                            ParseSingleValueNode(
                                leftNodeLeft,
                                rightNodeLeft,
                                Enum.Parse<ODataOperatorType>(
                                    binaryOperatorNode.OperatorKind.ToString()),
                                dictFilters);
                        }
                        else
                            TryNodeValue(node1.Source, dictFilters);
    
                        break;
                    }
                    case BinaryOperatorNode:
                        TryNodeValue(left, dictFilters);
                        break;
                    case SingleValueFunctionCallNode functionNode:
                        ParseSingleFunctionNode(functionNode,
                            Enum.Parse<ODataOperatorType>(TextInfo.ToTitleCase(functionNode.Name)),
                            dictFilters);
                        break;
                }
    
                switch (right)
                {
                    case BinaryOperatorNode:
                        TryNodeValue(right, dictFilters);
                        break;
                    case ConvertNode convertNode:
                        TryNodeValue(convertNode.Source, dictFilters);
                        break;
                    case SingleValueFunctionCallNode functionNode:
                        ParseSingleFunctionNode(functionNode,
                            Enum.Parse<ODataOperatorType>(TextInfo.ToTitleCase(functionNode.Name)),
                            dictFilters);
                        break;
                }
            }
        }
    
        private static void ParseSingleValueNode(
            SingleValuePropertyAccessNode left,
            SingleValueNode right,
            ODataOperatorType operatorKind,
            IDictionary<string, Tuple<object, ODataOperatorType>> dictFilters)
        {
            string key = left.Property.Name.Trim();
            object value = ((ConstantNode) right).Value;
    
            object specifiedValue = value is ODataEnumValue enumValue ? enumValue.Value : value;
    
            if (operatorKind is ODataOperatorType.LessThan or ODataOperatorType.LessThanOrEqual)
            {
                dictFilters.TryAdd($"{key}_To", new Tuple<object, ODataOperatorType>(value, operatorKind));
            }
            else if (dictFilters.TryGetValue(key, out Tuple<object, ODataOperatorType> currentValue))
            {
                dictFilters[key] = new Tuple<object, ODataOperatorType>(
                    $"{currentValue.Item1},{specifiedValue}",
                    operatorKind);
            }
            else
            {
                dictFilters.Add(key, new Tuple<object, ODataOperatorType>(specifiedValue, operatorKind));
            }
        }
    
        private static void ParseSingleFunctionNode(
            SingleValueFunctionCallNode node,
            ODataOperatorType operatorKind,
            IDictionary<string, Tuple<object, ODataOperatorType>> dictFilters)
        {
            string key = (node.Parameters.First() as SingleValuePropertyAccessNode)?.Property.Name.Trim();
            object value = (node.Parameters.Last() as ConstantNode)?.Value;
    
            if (string.IsNullOrEmpty(Convert.ToString(value)?.Trim()))
                return;
    
            dictFilters.TryAdd(key, new Tuple<object, ODataOperatorType>(value, operatorKind));
        }
    }
    
    public enum ODataOperatorType
    {
        Equal,
        NotEqual,
        GreaterThan,
        GreaterThanOrEqual,
        LessThan,
        LessThanOrEqual,
        Contains
    }
    

    对于调用 ODataUriParser 方法,您需要从输入操作中获取值。

    从请求 api 中获取 ODataQueryOptions>:

    输入端点动作 => ODataQueryOptions&lt;YourObjectModel&gt; options

     public Task<IQueryable<YourObject>> Get(ODataQueryOptions<YourObject> options)
            {
                // call your service class
            }
    

    然后在您的服务类中编写以下代码以调用 ODataUriParser 并使用结果运行:

       Dictionary<string, Tuple<object, ODataOperatorType>> dictFilters =
                ODataHelper<YourObject>.ODataUriParser(options);
    

    ODataUriParser方法使用示例结果:

    if (dictFilters.TryGetValue("Email", out Tuple<object, ODataOperatorType> emailValue))
                    {
                        bookRequestProfileDto.Email =
                            Convert.ToDateTime(emailValue.Item1.ToString());
                    }
    

    例如,我们想使用 Enum 将数字字符串列表转换为文本字符串列表: BookRequestType 是枚举类。

      if (dictFilters.TryGetValue("BookRequestType", out Tuple<object, ODataOperatorType> bookRequestTypeValue))
                    {
                        customerTransactionDto.BookRequestType =
                            Convert.ToString(bookRequestTypeValue.Item1)
                                .ConvertStringNamesEnumToStringNumbers<BookRequestType>();
                    }
    
    // Extesion Method
        public static string ConvertStringNamesEnumToStringNumbers<T>(this string stringTypes) where T : Enum
                {
                    var separateStringTypes = stringTypes.Split(',');
    
                StringBuilder stringBuilder = new StringBuilder();
    
                foreach (var item in separateStringTypes)
                {
                    stringBuilder.Append((int) Enum.Parse(typeof(T), item)).Append(',');
                }
    
                return stringBuilder.ToString().Remove(stringBuilder.Length - 1);
            }
    

    【讨论】:

    • 这似乎对我有用。我不得不为 C# 版本 8 编辑它,但更改非常微不足道。
    猜你喜欢
    • 2019-09-21
    • 1970-01-01
    • 1970-01-01
    • 2016-06-10
    • 2018-02-06
    • 1970-01-01
    • 1970-01-01
    • 2013-07-08
    相关资源
    最近更新 更多