Table of Contents

Querying

Part of the OhData.Client guide. See also Terminal operations for the methods that execute a query.

For<T>() returns an EntitySetClient<T>. All builder methods are immutable - each call returns a new instance, making it safe to compose partial queries:

var base = client.For<Product>().Filter(x => x.IsActive);

var cheap  = await base.Filter(x => x.Price < 10).ToListAsync();
var pricey = await base.Filter(x => x.Price > 100).OrderBy(x => x.Name).ToListAsync();

Property-name casing. Every typed (expression-based) builder — Filter, Select, OrderBy/OrderByDescending/ThenBy/ThenByDescending, and Expand — runs each property name through OhDataClientOptions.JsonOptions.PropertyNamingPolicy before emitting it. The default policy is null (PascalCase — the CLR names), matching the OhData server's PascalCase-default $metadata and responses, so x => x.Price > 10 emits $filter=Price gt 10. Set PropertyNamingPolicy = JsonNamingPolicy.CamelCase to emit camelCase for a server configured for camelCase. The raw-string overloads (Filter(string), Select(params string[]), Expand(params string[])) are never rewritten — those names are sent exactly as you typed them. The examples below show the CLR property names, which are what the default options emit.

$filter

Filter with a LINQ predicate - translated to an OData $filter string at call time:

// Comparison and logical operators
.Filter(x => x.Price > 10 && x.Name.StartsWith("W"))
// → $filter=Price gt 10 and startswith(Name,'W')

// Navigation path
.Filter(x => x.Category.Name == "Electronics")
// → $filter=Category/Name eq 'Electronics'

// String methods
.Filter(x => x.Name.Contains("cog") || x.Description.EndsWith("v2"))
// → $filter=contains(Name,'cog') or endswith(Description,'v2')

// Captured variables (evaluated immediately at translation time)
decimal min = 5m;
.Filter(x => x.Price >= min)
// → $filter=Price ge 5

Supported operators and functions:

LINQ OData
==, !=, >, >=, <, <= eq, ne, gt, ge, lt, le
&&, \|\|, ! and, or, not
+, -, *, /, % add, sub, mul, div, mod
.Contains(s) contains(prop,'s')
.StartsWith(s) startswith(prop,'s')
.EndsWith(s) endswith(prop,'s')
.ToLower(), .ToUpper() tolower(prop), toupper(prop)
.Trim() trim(prop)
string.IsNullOrEmpty(x.P) (x.P eq null or x.P eq '')
.Length (string) length(prop)
.Year / .Month / .Day (DateTime, DateTimeOffset, DateOnly) year(prop) / month(prop) / day(prop)
.Hour / .Minute / .Second (DateTime, DateTimeOffset, TimeOnly) hour(prop) / minute(prop) / second(prop)
.Any(t => ...) / .All(t => ...) (collection property) prop/any(t: ...) / prop/all(t: ...)

Inside an Any/All lambda you can reference the outer entity — the translator emits the OData implicit iteration variable $it for it:

.Filter(x => x.Tags.Any(t => t.Name == x.Name))
// → $filter=Tags/any(t: t/Name eq $it/Name)

Expressions that reference a lambda range variable in a way that has no OData path equivalent (e.g. a member access on a ternary) throw NotSupportedException at translation time rather than silently producing a wrong query.

For unsupported patterns, pass a raw OData string:

.Filter("round(Price) eq 5")

$select

// Anonymous projection (most common)
.Select(x => new { x.Id, x.Name, x.Price })
// → $select=Id,Name,Price

// Multiple members
.Select(x => x.Id, x => x.Name)
// → $select=Id,Name

// Navigation path
.Select(x => new { x.Category.Name })
// → $select=Category/Name

// String overload
.Select("Id", "Name", "Category/Name")

$expand

// Single navigation
.Expand(x => x.Category)
// → $expand=Category

// Multiple
.Expand(x => x.Category, x => x.Tags)
// → $expand=Category,Tags

// Nested options (string overload)
.Expand("Category($select=Name;$expand=Parent($select=Id))")

$orderby

.OrderBy(x => x.Name)
// → $orderby=Name

.OrderByDescending(x => x.Price)
// → $orderby=Price desc

Chain secondary sorts with ThenBy / ThenByDescending:

.OrderBy(x => x.Category).ThenByDescending(x => x.Price)
// → $orderby=Category,Price desc

.OrderByDescending(x => x.UpdatedAt).ThenBy(x => x.Name)
// → $orderby=UpdatedAt desc,Name

$top and $skip

.Top(20).Skip(40)
// → $top=20&$skip=40

Both validate >= 0 and throw ArgumentOutOfRangeException otherwise.

IncludeCount

Appends $count=true to the request so the server includes the total matching count in the response envelope. The count is available on ODataPage<T>.TotalCount when you call ToPageAsync:

ODataPage<Product> page = await client.For<Product>()
    .Filter(x => x.IsActive)
    .IncludeCount()
    .Top(20)
    .ToPageAsync();

Console.WriteLine($"{page.TotalCount} active products");

Note: ToPageAsync always forces $count=true regardless of whether IncludeCount was called. IncludeCount is useful when composing query state before calling ToPageAsync from a helper.


Next: Terminal operations →