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 \'
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.
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. ;)
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");
)