Skip to content

Instantly share code, notes, and snippets.

@jimmywim
Created September 5, 2025 21:54
Show Gist options
  • Select an option

  • Save jimmywim/d9c6a8d1c76fdc143166cb2f2473b6bf to your computer and use it in GitHub Desktop.

Select an option

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
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;
}
}
@jimmywim
Copy link
Author

jimmywim commented Sep 5, 2025

This doesn't actually support $skip, I've just noticed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment