Как анализировать фильтр 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
должен содержать фильтры в желаемом формате. Я уверен, что требуется больше работы, но если вы сможете получить эту скорость, я думаю, вы можете понять это.