How to parse OData $filter with regular expression in C#?

前端 未结 3 1749
误落风尘
误落风尘 2020-12-05 05:47

Hi I\'m wondering what the best approach would be to parse an OData $filter string in C#, for example

/API/organisations?$filter=\"name eq \'Facebook\' or name eq \'

相关标签:
3条回答
  • 2020-12-05 06:03

    I think you are supposed to travserse the AST with the interface provided using the visitor pattern.

    Consider you have this class that represents a filter

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

    So, how do we "extract" the filters that comes with the OData parameters to your class?

    Well the FilterClause object have an Expression property which is a SingleValueNode wich inherits from a QueryNode. The QueryNode have the Accept method who takes a QueryNodeVisitor.

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

    Right, so you must implement your own QueryNodeVisitor and do your stuff. Below is a non finished example (I dont override all possible visitors).

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

    Then, fire away :)

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

    When it has traversed the tree your

    visitor.filterValueList
    

    should contain the filters in your desired format. Im sure more work is needed but if you can get this rolling I think you can figure it out.

    0 讨论(0)
  • Building on what Jen S says, you can traverse the AST tree that is returned by the FilterClause.

    For instance, you can retrieve the FilterClause from the controller's query options:

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

    You can then traverse the resultant AST tree with code like the following (borrowed from this article):

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

    The function called is like so:

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

    You then can go through the list dictionary and retrieve your values:

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

    This example is fairly simplistic in that it only accounts for "eq" evaluations, but for my purposes it worked well. YMMV. ;)

    0 讨论(0)
  • 2020-12-05 06:25

    In .NET, there's a library available that will do this for you. Writing your own regex runs the risk of missing some edge case.

    Using NuGet, bring in Microsoft.Data.OData. Then, you can do:

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

    result here will be in the form of an AST representing the filter clause.

    (To get the model and type inputs, you could parse your $metadata file using something like this:

    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");
    

    )

    0 讨论(0)
提交回复
热议问题