Microsoft.Azure.Cosmos.OData
Translates OData V4 queries into Azure Cosmos DB SQL queries.
v3 — complete rewrite. This version replaces the legacy Microsoft.Azure.Documents.OData.Sql package.
See MIGRATION.md for upgrade instructions from v1/v2.
Feature
v1/v2
v3
$select / $filter / $orderby / $top
✅
✅
$skip + $top → OFFSET … LIMIT …
❌
✅
$count=true → companion COUNT query
❌
✅
$apply (aggregate / groupby) → GROUP BY
❌
✅
Parameterized queries (@p0, @p1, …)
❌
✅ (default)
contains, startswith, endswith
✅
✅
toupper / tolower / length / indexof / substring / trim / concat
✅
✅
matchesPattern → RegexMatch
❌
✅
Math: round / floor / ceiling
❌
✅
Date parts: year(), month(), …
❌
✅
any() / all() → EXISTS sub-queries
❌
✅
in operator (x in ('a','b'))
❌
✅
Geospatial: geo.distance / geo.intersects → ST_DISTANCE / ST_INTERSECTS
❌
✅
Vector search: vectordistance(…) → VectorDistance(…)
❌
✅
Full-text search: fulltextcontains(…) → FullTextContains(…)
❌
✅
IS_DEFINED / ARRAY_CONTAINS (via OData function extensions)
❌
✅
Multi-target: netstandard2.0 / net8.0 / net9.0 / net10.0
❌
✅
SOLID architecture with pluggable interfaces
❌
✅
ASP.NET Core + ASP.NET Web API 2 adapters
Web API 2 only
Both (planned)
using Microsoft . Azure . Cosmos . OData ;
// 1. Create the translator (reuse it — it's thread-safe)
var translator = new ODataToCosmosSqlTranslator ( ) ;
// 2. Build clauses from an OData URI (or from ASP.NET's ODataQueryOptions via an adapter)
var clauses = new ODataQueryClauses
{
Filter = parser . ParseFilter ( ) ,
OrderBy = parser . ParseOrderBy ( ) ,
Select = parser . ParseSelectAndExpand ( ) ,
Top = parser . ParseTop ( ) ,
Skip = parser . ParseSkip ( ) ,
Count = parser . ParseCount ( ) ,
} ;
// 3. Translate
TranslatedQuery result = translator . Translate ( clauses ) ;
Console . WriteLine ( result . Sql ) ;
// SELECT * FROM c WHERE c.revenue > @p0 ORDER BY c.name ASC OFFSET 0 LIMIT 10
Console . WriteLine ( result . Parameters ) ;
// { "@p0": 1000 }
// 4. Use with Cosmos SDK
var queryDef = new QueryDefinition ( result . Sql ) ;
foreach ( var ( key , value ) in result . Parameters )
queryDef = queryDef . WithParameter ( key , value ) ;
var options = new TranslationOptions
{
Clauses = TranslationClauses . All , // which clauses to translate
Parameterization = ParameterizationMode . Parameters , // default: parameterized SQL
Pagination = PaginationMode . OffsetLimit , // default: OFFSET/LIMIT (not TOP)
DocumentAlias = "c" , // the FROM alias
AdditionalWhereClause = "c.isActive = true" , // extra raw WHERE fragment
} ;
var result = translator . Translate ( clauses , options ) ;
Inline mode (for debugging)
var options = new TranslationOptions { Parameterization = ParameterizationMode . Inline } ;
// Produces: SELECT * FROM c WHERE c.name = 'Microsoft'
var options = new TranslationOptions { Pagination = PaginationMode . Top } ;
// Produces: SELECT TOP 10 * FROM c ...
The engine is designed around SOLID principles :
IFieldNameResolver — maps OData property names to Cosmos document paths (englishName → c.englishName). Default: DefaultFieldNameResolver.
ISqlFunctionMapper — maps OData functions to Cosmos SQL functions (contains → CONTAINS, trim → LTRIM(RTRIM(…))). Built-in mappers:
DefaultFunctionMapper — string, math, date, type-check functions.
GeospatialFunctionMapper — geo.distance → ST_DISTANCE, etc.
VectorSearchFunctionMapper — vectordistance → VectorDistance.
FullTextSearchFunctionMapper — fulltextcontains → FullTextContains, etc.
CompositeFunctionMapper — composes multiple mappers in priority order.
ISqlExpressionRenderer — renders the SQL expression tree to a string with parameter substitution. Default: CosmosSqlRenderer.
ODataToCosmosSqlTranslator — the orchestrator. Accepts ODataQueryClauses (a framework-agnostic DTO) and TranslationOptions, delegates to the above.
Instead of concatenating strings, the visitor builds an immutable SqlExpression tree:
SqlExpression
├── SqlLiteral(value)
├── SqlMember(path)
├── SqlBinary(op, left, right)
├── SqlUnary(op, operand)
├── SqlFunctionCall(name, args)
├── SqlExists(rangeVar, source, predicate)
├── SqlRaw(text)
└── SqlNull
This makes the engine testable, extensible, and free of leaky abstractions.
Supported OData → Cosmos SQL mappings
OData
Cosmos SQL
$select=a,b
SELECT c.a, c.b FROM c
$filter=x eq 5
WHERE c.x = 5
$orderby=x desc
ORDER BY c.x DESC
$top=10&$skip=20
OFFSET 20 LIMIT 10
$top=10 (legacy mode)
SELECT TOP 10 …
$count=true
companion SELECT VALUE COUNT(1) FROM c …
$apply=aggregate(price with sum as total)
SELECT SUM(c.price) AS total FROM c
$apply=groupby((cat),aggregate(…))
SELECT c.cat, SUM(…) FROM c GROUP BY c.cat
OData
Cosmos SQL
contains(field,'value')
CONTAINS(c.field,'value')
startswith(field,'value')
STARTSWITH(c.field,'value')
endswith(field,'value')
ENDSWITH(c.field,'value')
toupper(field)
UPPER(c.field)
tolower(field)
LOWER(c.field)
length(field)
LENGTH(c.field)
indexof(field,'value')
INDEX_OF(c.field,'value')
substring(field,idx1,idx2)
SUBSTRING(c.field,idx1,idx2)
trim(field)
LTRIM(RTRIM(c.field))
concat(field,'value')
CONCAT(c.field,'value')
matchesPattern(field,'^A')
RegexMatch(c.field,'^A')
left(field,n)
LEFT(c.field,n)
right(field,n)
RIGHT(c.field,n)
replace(field,'old','new')
REPLACE(c.field,'old','new')
reverse(field)
REVERSE(c.field)
stringequals(field,'value')
StringEquals(c.field,'value')
tostring(field)
ToString(c.field)
OData
Cosmos SQL
round(field) / floor / ceiling
ROUND / FLOOR / CEILING
abs(field)
ABS(c.field)
power(field,n)
POWER(c.field,n)
sqrt(field)
SQRT(c.field)
log(field) / log10(field)
LOG / LOG10
exp(field)
EXP(c.field)
sin / cos / tan / atn / atn2
SIN / COS / TAN / ATN / ATN2
degrees(field) / radians(field)
DEGREES / RADIANS
rand()
RAND()
numberbin(field,size)
NumberBin(c.field,size)
OData
Cosmos SQL
year(field) / month / day / hour / minute / second
DateTimePart('yyyy',c.field) etc.
datetimeadd(part,n,field)
DateTimeAdd(part,n,c.field)
datetimediff(part,start,end)
DateTimeDiff(part,start,end)
getcurrentdatetime()
GetCurrentDateTime()
getcurrentticks()
GetCurrentTicks()
datetimebin(field,part,size)
DateTimeBin(c.field,part,size)
OData
Cosmos SQL
isdefined(field)
IS_DEFINED(c.field)
isnull(field)
IS_NULL(c.field)
isnumber(field) / isstring / isbool / isarray / isobject
IS_NUMBER / IS_STRING / IS_BOOL / IS_ARRAY / IS_OBJECT
isinteger(field) / isprimitive / isfinitenumber
IS_INTEGER / IS_PRIMITIVE / IS_FINITE_NUMBER
OData
Cosmos SQL
arraycontains(arr,val)
ARRAY_CONTAINS(c.arr,val)
arraylength(arr)
ARRAY_LENGTH(c.arr)
arrayslice(arr,start,len)
ARRAY_SLICE(c.arr,start,len)
arrayconcat(arr1,arr2)
ARRAY_CONCAT(c.arr1,c.arr2)
OData
Cosmos SQL
geo.distance(loc,point)
ST_DISTANCE(c.loc,point)
geo.intersects(loc,polygon)
ST_INTERSECTS(c.loc,polygon)
geo.within(loc,polygon)
ST_WITHIN(c.loc,polygon)
geo.isvalid(loc)
ST_ISVALID(c.loc)
geo.area(polygon)
ST_AREA(c.polygon)
Vector & Full-text search
OData
Cosmos SQL
vectordistance(embedding,query)
VectorDistance(c.embedding,query)
fulltextcontains(field,'term')
FullTextContains(c.field,'term')
fulltextcontainsall(field,'a','b')
FullTextContainsAll(c.field,'a','b')
fulltextcontainsany(field,'a','b')
FullTextContainsAny(c.field,'a','b')
fulltextscore(field,'term')
FullTextScore(c.field,'term')
rrf(score1,score2)
RRF(score1,score2) (hybrid ranking)
src/
Microsoft.Azure.Cosmos.OData/ ← engine (netstandard2.0 + net8/9/10)
Abstractions/ ← IFieldNameResolver, ISqlFunctionMapper, ISqlExpressionRenderer
Ast/ ← SqlExpression record hierarchy
Errors/ ← ODataTranslationException hierarchy
Functions/ ← Default, Geospatial, Vector, FullText function mappers
Naming/ ← DefaultFieldNameResolver
Options/ ← TranslationOptions, TranslationClauses, ParameterizationMode, PaginationMode
Rendering/ ← CosmosSqlRenderer
Translation/ ← ODataExpressionVisitor, ODataToCosmosSqlTranslator
ODataQueryClauses.cs ← Framework-agnostic input DTO
TranslatedQuery.cs ← Result (SQL + parameters)
tests/
Microsoft.Azure.Cosmos.OData.Tests/ ← xUnit tests
See CONTRIBUTING.md (coming soon).
MIT
Ziyou Zheng — original author
Contributors welcome!