skip to content

You are exposing an OData 4.01 entity set to many client teams; how do you keep arbitrary $filter, $expand and $orderby queries from becoming too expensive?

level: seniorimportance: must knowfreq 19%

answer

  1. support some options, not all
  2. unsupported means fail, not ignore
  3. Capabilities annotations advertise limits
  4. page every collection yourself

basics

~20 s

Support only what you can serve cheaply and fail the rest (unsupported options MUST be rejected, SHOULD with 501), advertise limits with Capabilities annotations such as FilterRestrictions and ExpandRestrictions, and cap every collection with server-driven paging.

solid answer

~40 s

OData lets a service support some or all system query options, and one it does not support MUST fail the request, preferably with `501 Not Implemented` — never silently ignore it. Then narrow what is supported and say so in `$metadata` with the Capabilities vocabulary: `FilterRestrictions` (`NonFilterableProperties`, `RequiresFilter`, `RequiredProperties`, `FilterExpressionRestrictions`, `MaxLevels`), `SortRestrictions` (`NonSortableProperties`), `ExpandRestrictions` (`MaxLevels`, `NonExpandableProperties`), `SearchRestrictions`, `CountRestrictions`, `TopSupported` and `FilterFunctions`. Those annotations only describe; the service must still enforce them on every request. Finally, page every collection — expanded ones included — at a size the service chooses, whatever `maxpagesize` asks for. Query timeouts, a ceiling on `$top` and per-client budgets are implementation choices the specification leaves to you.

go deeper

for a junior

Recall that an OData service does not have to support every query option and must reject one it does not support.

for a middle

Explain how Capabilities annotations such as FilterRestrictions and ExpandRestrictions advertise limits, and why 501 is the expected failure.

for a senior

Show that you would align restrictions with indexes and joins, enforce them on every request, and page expanded collections as well as the top level.

for a principal

Argue how much query power a shared service should expose at all, trading client flexibility against predictable cost and the round trips each restriction creates.

## Why an open query language is a cost problem OData gives the client a query language in the URL. That is its appeal — one endpoint serves many screens — and its risk: the client chooses the work. A few characters can ask for a lot: - `$filter=contains(Notes,'late')` on an unindexed text column scans the whole table. - `$expand=Orders($expand=Items($expand=Product))` turns a list into a tree three hops deep. - `$orderby` on an unindexed property forces a sort of the full filtered set before paging. - `$count=true` over a large set computes an exact total on every request. - `$filter=Orders/any(o:o/Items/any(i:i/Quantity gt 100))` is a nested semi-join per customer. ## What the specification lets a service refuse The OData 4.01 Protocol gives a service explicit room to support less: 1. An OData service **MAY support some or all** of the system query options. If it does not support one, it **MUST fail** any request containing it and **SHOULD return `501 Not Implemented`**. 2. At the Intermediate conformance level a service MUST support `$filter` with `eq` and `ne` on the entity set's properties, SHOULD support the other operators and canonical functions, and **MUST return 501** for any operator or function it does not support. 3. `$levels=max` is optional: services MAY support it. 4. Server-driven paging is always the service's decision; the `maxpagesize` preference is only a request. Failing is the point. A service that silently ignored an unsupported `$filter` would return unfiltered data that the client believes is filtered. ## Advertising the limits The Capabilities vocabulary lets a service tell clients in its metadata what it permits, so generated clients and UIs can avoid sending what will fail: | Term | What it can say | |---|---| | `FilterRestrictions` | `Filterable`; `RequiresFilter`; `RequiredProperties`; `NonFilterableProperties`; `FilterExpressionRestrictions` with `AllowedExpressions` such as `SingleValue`, `SingleRange` or `SearchExpression`; `MaxLevels` for traversal depth | | `SortRestrictions` | `Sortable`; `NonSortableProperties`; `AscendingOnlyProperties`; `DescendingOnlyProperties` | | `ExpandRestrictions` | `Expandable`; `MaxLevels`; `NonExpandableProperties` | | `SearchRestrictions` | `Searchable`; `UnsupportedExpressions` | | `CountRestrictions` | `Countable`; `NonCountableProperties` | | `TopSupported`, `SkipSupported`, `ComputeSupported` | tags saying whether those options work on a collection | | `FilterFunctions` | the functions and operators allowed in filter expressions | A `MaxLevels` of `-1`, the default, means no restriction — so leaving it unset advertises unlimited depth. `RequiresFilter` with `RequiredProperties` is a strong tool for very large sets: a client must always narrow by, say, `CustomerID` or a date range. `FilterExpressionRestrictions` names a property and the subset of expressions allowed on it, through `AllowedExpressions`: - `SingleValue` — the property can be used in a single `eq` clause. - `MultiValue` — several `eq` and `in` clauses combined by `or`. - `SingleRange` — one closed, half-open or open interval, such as `OrderDate ge 2026-01-01 and OrderDate lt 2026-02-01`. - `MultiRange` — a union of such intervals. - `SearchExpression` — a string property used with `startswith`, `endswith` or `contains`, combined by `or`. A date restricted to `SingleRange` maps naturally onto an index range scan, which is exactly the kind of query a service can promise to serve cheaply. ## Enforcing at run time The annotations are documentation that machines can read; they do not stop a request. The service must: - parse every query and check it against the same rules it advertises, failing violations rather than ignoring them; - page every collection, including each expanded collection, at a size it chooses, returning next links; - refuse `$expand` beyond its depth limit, and decline `$levels=max` if it does not support it; - apply its own guards the specification does not define — a statement timeout, a ceiling on `$top`, rate limits or per-client query budgets. Those are implementation choices, and the specification names no status code for a well-formed query that breaks such a self-imposed limit, so that choice is the service's too. ## The trade-off behind the limits Every restriction moves work back to the client: a client that cannot expand makes more round trips, and one that cannot filter on a property downloads more rows. The useful design move is to restrict along the lines of the storage — filterable and sortable where indexed, expandable where the join is cheap, required filters where the set is huge — and to publish those lines in metadata, so client teams learn the boundary before production rather than from a 501.

  • Your OData metadata lists Notes in FilterRestrictions/NonFilterableProperties, yet a client filters on Notes. What should the service do?
    Fail the request. Capabilities annotations describe the service; they do not enforce anything, and the protocol says a service MUST fail a request that uses functionality it does not support, returning 501 for unsupported operations. Ignoring the filter would be worse than failing: the client would receive unfiltered rows it believes are filtered.
  • Why is capping $top on an OData service not enough to bound response size?
    `$top` limits only the collection it is applied to. Each customer's expanded orders, each order's expanded items and every nested collection grow independently, so fifty customers can bring thousands of rows. The service needs server-driven paging on every collection, a limit on expand depth and on which properties are expandable, plus restrictions on expensive filters and sorts.
  • What does an ExpandRestrictions MaxLevels value of -1 tell an OData client?
    That there is no restriction on how many levels may be expanded — `-1` is the vocabulary's default. A service that wants to bound `$expand` depth must set a positive number explicitly, and still enforce it when a request exceeds it.

saying these in an interview costs you the question

  • If metadata marks a property non-filterable, the server no longer needs to check
  • An OData service must implement every system query option to conform
  • Capping $top bounds the response, since expanded collections follow it
  • An unsupported query option should be silently ignored for compatibility
  • The OData specification defines a standard maximum page size and expand depth