Как анализировать фильтр OData $с регулярным выражением в С#?

Привет, мне интересно, как лучше всего будет разбирать строку фильтра OData $в С#, например

/API/организации? $filter = "name eq 'Facebook' или имя eq 'Twitter' и подписчики gt '30'"

Должны возвращать все организации с именем Facebook или Twitter и у которых более 30 подписчиков. Я исследовал довольно много, но не могу найти решения, которые не вращаются вокруг WCF. Я думал об использовании Regex и группировке их, поэтому у меня есть список классов фильтра, которые:

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

но я в тупике, как обрабатывать ANDs/ORs.

Ответ 1

Проверьте это регулярное выражение с флагами i и x.

(?<Filter>
 (?<Resource>.+?)\s+
 (?<Operator>eq|ne|gt|ge|lt|le|add|sub|mul|div|mod)\s+
 '?(?<Value>.+?)'?
)
(?:
    \s*$
   |\s+(?:or|and|not)\s+
)

Demo

http://regexhero.net/tester/?id=0a26931f-aaa3-4fa0-9fc9-1a67d34c16b3

Пример кода

string strRegex = @"(?<Filter>" + 
"\n" + @"     (?<Resource>.+?)\s+" + 
"\n" + @"     (?<Operator>eq|ne|gt|ge|lt|le|add|sub|mul|div|mod)\s+" + 
"\n" + @"     '?(?<Value>.+?)'?" + 
"\n" + @")" + 
"\n" + @"(?:" + 
"\n" + @"    \s*$" + 
"\n" + @"   |\s+(?:or|and|not)\s+" + 
"\n" + @")" + 
"\n";
Regex myRegex = new Regex(strRegex, RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
string strTargetString = @"name eq 'Facebook' or name eq 'Twitter' and subscribers gt '30'";
string strReplace = @"Filter >> ${Filter}" + "\n" + @"   Resource : ${Resource}" + "\n" + @"   Operator : ${Operator}" + "\n" + @"   Value    : ${Value}" + "\n\n";

return myRegex.Replace(strTargetString, strReplace);

Выход

Filter >> name eq 'Facebook'
   Resource : name
   Operator : eq
   Value    : Facebook

Filter >> name eq 'Twitter'
   Resource : name
   Operator : eq
   Value    : Twitter

Filter >> subscribers gt '30'
   Resource : subscribers
   Operator : gt
   Value    : 30

Обсуждение

Чтобы иметь верхний регистр для ресурса и оператора, используйте MatchEvaluator. Однако группировка с ( и ) не поддерживается. Оставьте комментарий, если вы хотите, чтобы регулярное выражение поддерживало его.

Ответ 2

В .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, представляющего предложение фильтра.

(Чтобы получить входы model и type, вы можете проанализировать свой файл метаданных $, используя что-то вроде этого:

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

)

Ответ 3

Основываясь на том, что говорит Jen S, вы можете пересечь дерево AST, которое возвращается FilterClause.

Например, вы можете получить FilterClause из параметров запроса контроллера:

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

Затем вы можете пересечь результирующее дерево AST с кодом, подобным следующему (заимствованный из в этой статье):

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

Я думаю, вы должны трассировать 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, которое является SingleValueNode, которое наследуется от QueryNode. У QueryNode есть метод Accept, который принимает QueryNodeVisitor.

    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

должен содержать фильтры в желаемом формате. Я уверен, что требуется больше работы, но если вы сможете получить эту скорость, я думаю, вы можете понять это.