Instantly share code, notes, and snippets.
Created
September 5, 2025 21:54
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
-
Save jimmywim/d9c6a8d1c76fdc143166cb2f2473b6bf to your computer and use it in GitHub Desktop.
Takes an incoming ODataOptions from an inbound ASP.NET Core REST API call and converts it to a SharePoint CAML query
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| public static class ODataExtensions | |
| { | |
| public static string CreateSPCamlQuery<T>(this ODataQueryOptions<T> options) | |
| { | |
| var camlViewElement = new XElement("View"); | |
| var camlQueryElement = new XElement("Query"); | |
| camlViewElement.Add(camlQueryElement); | |
| if (options.Filter != null) | |
| { | |
| var whereElement = new XElement("Where"); | |
| camlQueryElement.Add(whereElement); | |
| // options.Filter.FilterClause.Expression | |
| var camlWhere = BuildWhere(options.Filter.FilterClause.Expression); | |
| whereElement.Add(camlWhere); | |
| } | |
| if (options.SelectExpand != null) | |
| { | |
| var viewFieldsElement = new XElement("ViewFields"); | |
| var selectedItems = options.SelectExpand.SelectExpandClause.SelectedItems; | |
| foreach (var selectedItem in selectedItems) | |
| { | |
| var pathItem = selectedItem as PathSelectItem; | |
| if (pathItem != null) | |
| { | |
| foreach (var pathSegment in pathItem.SelectedPath) | |
| { | |
| if (pathSegment is PropertySegment) | |
| { | |
| var propertySegment = pathSegment as PropertySegment; | |
| var viewField = new XElement("FieldRef", new XAttribute("Name", propertySegment.Property.Name)); | |
| viewFieldsElement.Add(viewField); | |
| } | |
| } | |
| } | |
| } | |
| camlQueryElement.Add(viewFieldsElement); | |
| } | |
| if (options.Top != null) | |
| { | |
| var rowLimit = new XElement("RowLimit", new XAttribute("Paged", "TRUE"), options.Top.Value); | |
| camlQueryElement.Add(rowLimit); | |
| } | |
| if (options.OrderBy != null) | |
| { | |
| var orderBy = new XElement("OrderBy"); | |
| foreach (var node in options.OrderBy.OrderByNodes) | |
| { | |
| var typedNode = node as OrderByPropertyNode; | |
| orderBy.Add( | |
| new XElement("FieldRef", | |
| new XAttribute("Name", typedNode.Property.Name), | |
| new XAttribute("Ascending", typedNode.OrderByClause.Direction == Microsoft.OData.UriParser.OrderByDirection.Ascending ? "TRUE" : "FALSE") | |
| ) | |
| ); | |
| } | |
| camlQueryElement.Add(orderBy); | |
| } | |
| return camlViewElement.ToString(); | |
| } | |
| private static XElement BuildWhere(SingleValueNode node) | |
| { | |
| if (node.Kind == QueryNodeKind.BinaryOperator) | |
| { | |
| var binOp = node as BinaryOperatorNode; | |
| var op = ConvertOperatorToCaml(binOp.OperatorKind); | |
| var elem = new XElement(op); | |
| if (binOp.Left.Kind == QueryNodeKind.Convert && | |
| binOp.Right.Kind == QueryNodeKind.Constant) | |
| { | |
| var convertNode = binOp.Left as ConvertNode; | |
| if (convertNode.Source.Kind == QueryNodeKind.SingleValuePropertyAccess) | |
| { | |
| var fieldName = convertNode.Source as SingleValuePropertyAccessNode; | |
| var fieldValue = binOp.Right as ConstantNode; | |
| var valueType = GetCamlValueType(fieldValue.TypeReference); | |
| var value = fieldValue.Value; | |
| if (valueType == "Boolean") | |
| { | |
| value = fieldValue.Value.ToString() == "True" ? 1 : 0; | |
| } | |
| elem.Add(new XElement("FieldRef", new XAttribute("Name", fieldName.Property.Name))); | |
| elem.Add(new XElement("Value", value, new XAttribute("Type", valueType))); | |
| } | |
| } | |
| if (binOp.Left.Kind == QueryNodeKind.SingleValuePropertyAccess && | |
| binOp.Right.Kind == QueryNodeKind.Constant) | |
| { | |
| var fieldName = binOp.Left as SingleValuePropertyAccessNode; | |
| var fieldValue = binOp.Right as ConstantNode; | |
| var valueType = GetCamlValueType(fieldValue.TypeReference); | |
| var value = fieldValue.Value; | |
| if (valueType == "Boolean") | |
| { | |
| value = fieldValue.Value.ToString() == "True" ? 1 : 0; | |
| } | |
| elem.Add(new XElement("FieldRef", new XAttribute("Name", fieldName.Property.Name))); | |
| elem.Add(new XElement("Value", value, new XAttribute("Type", valueType))); | |
| } | |
| if (binOp.Left.Kind == QueryNodeKind.SingleValueFunctionCall) | |
| { | |
| var childElem = BuildWhere(binOp.Left); | |
| elem.Add(childElem); | |
| } | |
| if (binOp.Right.Kind == QueryNodeKind.SingleValueFunctionCall) | |
| { | |
| var childElem = BuildWhere(binOp.Right); | |
| elem.Add(childElem); | |
| } | |
| if (binOp.Left.Kind == QueryNodeKind.BinaryOperator || | |
| binOp.Left.Kind == QueryNodeKind.Convert) | |
| { | |
| var childElem = BuildWhere(binOp.Left); | |
| elem.Add(childElem); | |
| } | |
| if (binOp.Right.Kind == QueryNodeKind.BinaryOperator || | |
| binOp.Right.Kind == QueryNodeKind.Convert) | |
| { | |
| var childElem = BuildWhere(binOp.Right); | |
| elem.Add(childElem); | |
| } | |
| return elem; | |
| } | |
| if (node.Kind == QueryNodeKind.Convert) | |
| { | |
| var convertNode = node as ConvertNode; | |
| return BuildWhere(convertNode.Source); | |
| } | |
| if (node.Kind == QueryNodeKind.SingleValueFunctionCall) | |
| { | |
| var callNode = node as SingleValueFunctionCallNode; | |
| var functionNode = new XElement(GetCamlFunctionName(callNode.Name)); | |
| foreach (var funcParam in callNode.Parameters) | |
| { | |
| if (funcParam is SingleValuePropertyAccessNode) | |
| { | |
| var prop = funcParam as SingleValuePropertyAccessNode; | |
| functionNode.Add(new XElement("FieldRef", new XAttribute("Name", prop.Property.Name))); | |
| } | |
| if (funcParam is ConstantNode) | |
| { | |
| var value = funcParam as ConstantNode; | |
| var valueType = GetCamlValueType(value.TypeReference); | |
| functionNode.Add(new XElement("Value", new XAttribute("Type", valueType), value.Value)); | |
| } | |
| if (funcParam is ConvertNode) | |
| { | |
| var convertNode = funcParam as ConvertNode; | |
| if (convertNode.Source is SingleValuePropertyAccessNode) | |
| { | |
| var prop = convertNode.Source as SingleValuePropertyAccessNode; | |
| functionNode.Add(new XElement("FieldRef", new XAttribute("Name", prop.Property.Name))); | |
| } | |
| } | |
| } | |
| return functionNode; | |
| } | |
| return null; | |
| } | |
| private static string GetCamlFunctionName(string functionName) | |
| { | |
| switch (functionName) | |
| { | |
| case "startswith": | |
| return "BeginsWith"; | |
| case "contains": | |
| return "Contains"; | |
| default: | |
| throw new System.Exception($"Invalid function: {functionName}"); | |
| } | |
| } | |
| private static string GetCamlValueType(IEdmTypeReference typeRef) | |
| { | |
| var typeString = typeRef.Definition.ToString(); | |
| switch (typeString) | |
| { | |
| case "Edm.String": | |
| return "Text"; | |
| case "Edm.Boolean": | |
| return "Boolean"; | |
| case "Edm.Decimal": | |
| case "Edm.Double": | |
| case "Edm.Float": | |
| case "Edm.Int16": | |
| case "Edm.Int32": | |
| case "Edm.Int64": | |
| return "Number"; | |
| case "Edm.DateTime": | |
| return "DateTime"; | |
| case "Edm.Guid": | |
| return "Guid"; | |
| default: | |
| return "Text"; | |
| } | |
| } | |
| private static string ConvertOperatorToCaml(BinaryOperatorKind binaryOperator) | |
| { | |
| string camlOp = ""; | |
| switch (binaryOperator) | |
| { | |
| case BinaryOperatorKind.Equal: | |
| camlOp = "Eq"; break; | |
| case BinaryOperatorKind.NotEqual: | |
| camlOp = "Neq"; break; | |
| case BinaryOperatorKind.GreaterThan: | |
| camlOp = "Gt"; break; | |
| case BinaryOperatorKind.GreaterThanOrEqual: | |
| camlOp = "Geq"; break; | |
| case BinaryOperatorKind.LessThan: | |
| camlOp = "Lt"; break; | |
| case BinaryOperatorKind.LessThanOrEqual: | |
| camlOp = "Leq"; break; | |
| case BinaryOperatorKind.And: | |
| camlOp = "And"; break; | |
| case BinaryOperatorKind.Or: | |
| camlOp = "Or"; break; | |
| case BinaryOperatorKind.Has: | |
| camlOp = "Contains"; break; | |
| } | |
| return camlOp; | |
| } | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This doesn't actually support $skip, I've just noticed.