skip to content

In OData v4, how does `Customers?$filter=Orders/any(o:o/Amount gt 500)` differ from `Customers?$expand=Orders($filter=Amount gt 500)`?

level: middleimportance: should knowfreq 17%

answer

  1. which collection gets filtered
  2. a lambda variable over orders
  3. parents versus inline children
  4. empty collections and all

basics

~20 s

The any filter returns only customers who have at least one order over 500, with no orders inlined. The expand filter returns every customer, each carrying only its orders over 500 — possibly an empty list.

solid answer

~40 s

`any` and `all` are OData's **lambda operators**: prefixed with a path to a collection, they evaluate a Boolean expression over its members using a variable (`o:`), so `$filter=Orders/any(o:o/Amount gt 500)` filters the **customers**. A `$filter` inside `$expand=Orders(...)` filters only the **inline orders**; every customer still comes back. `any` is true if at least one member matches, so it is false for an empty collection; `all` is true if every member matches, so it is true for an empty one. `any()` with no argument tests for a non-empty collection, while `all` always needs one. To get customers with a large order and show those orders, combine both: `$filter=Orders/any(o:o/Amount gt 500)&$expand=Orders($filter=Amount gt 500)`.

go deeper

for a junior

Recall that any means at least one member matches and that a filter inside $expand trims only the inline related items.

for a middle

Explain the lambda variable, what any and all return for an empty collection, and how unprefixed paths resolve to the parent.

for a senior

Show how to combine an outer lambda with an inner expand filter for a report, and recognise the vacuous-truth bug with all.

for a principal

Reason about whether a public service should allow lambdas over large related collections, given that each one is a per-parent semi-join.

## Two different questions The two URLs look alike but answer different questions, because a `$filter` always filters **the collection it is attached to**: | Request | Collection filtered | Customers returned | Orders in the response | |---|---|---|---| | `Customers?$filter=Orders/any(o:o/Amount gt 500)` | `Customers` | only those with at least one order over 500 | none — nothing is expanded | | `Customers?$expand=Orders($filter=Amount gt 500)` | each customer's `Orders` | all customers | per customer, only orders over 500 | The first is "which customers have a big order?"; the second is "show me every customer, with their big orders". A worked example makes the difference concrete. Suppose three customers: **Arnaud** has orders of 800 and 120, **Bertin** has one order of 90, and **Cordier** has none. - The `any` request returns only Arnaud, with no orders in the body. - The expand request returns Arnaud with the 800 order inline, Bertin with an empty `Orders` array, and Cordier with an empty `Orders` array. - An `all(o:o/Amount gt 500)` filter would return only Cordier — Arnaud fails on the 120 order, Bertin on the 90 one, and Cordier passes because he has no orders at all. ## How lambda operators work OData v4 (URL Conventions, section 5.1.1.13) defines two **lambda operators** that evaluate a Boolean expression on a collection. Both must be prepended with a navigation path that identifies a collection. - The argument is a **lambda variable** name, a colon and a Boolean expression: `o:o/Amount gt 500`. The variable stands for each member of the collection in turn, so `o/Amount` is an order's amount. - **`any`** returns true if and only if the expression is true for at least one member. That implies it is **false for an empty collection**. - `any` also has a **short form with no argument**: `Orders/any()` is false only when the collection is empty, so it means "has at least one order". - **`all`** returns true if the expression is true for every member. That implies it is **true for an empty collection**, and it cannot be used without an argument. Scope inside the expression follows explicit rules. A path prefixed with the lambda variable refers to the member. A path with neither the variable nor `$it` is evaluated in the scope of the instances **at the origin of the navigation path** — the customer: ```http GET /odata/Customers?$filter=Orders/any(o:o/ShippingAddress ne Address) ``` Here `Address` is the customer's address, so this finds customers with an order shipped somewhere else. If the variable's name collides with a property of the current resource, the variable wins; `$it` is the way to reach the current resource's property explicitly. ## Filtering the children instead A `$filter` inside the parentheses of an expand item is an **expand option**. It is evaluated for each customer's related orders and keeps only matching ones in the inline `Orders` array. It has no effect on which customers are selected: the outer collection is filtered only by an outer `$filter`, so customers with no qualifying orders still appear, with an empty array. ## Combining the two Most reporting screens want the intersection — customers with a large order, showing those orders: ```http GET /odata/Customers?$filter=Orders/any(o:o/Amount gt 500) &$expand=Orders($filter=Amount gt 500;$orderby=Amount desc) ``` The condition is deliberately repeated: the outer lambda selects parents, the inner filter trims children. Writing only one of them is the classic source of either too many customers or too many orders. ## Edge cases that are asked about 1. **`all` on an empty collection.** `Customers?$filter=Orders/all(o:o/Amount gt 500)` includes customers who have **no orders**, because `all` is vacuously true. Add `Orders/any()` with `and` to exclude them. 2. **Nesting.** Lambdas nest: `Orders/any(o:o/Items/any(i:i/Quantity gt 100))` finds customers with an order holding a large line. 3. **Counting instead.** `$filter=Orders/$count gt 5` uses the exact count of related entities, which is a different test from any lambda. 4. **Nulls.** Items for which the `$filter` expression evaluates to null are omitted, just like false. ## Why interviewers like this pair It checks whether a candidate reasons about **which set** an expression ranges over, and it has a cost angle: a lambda over a large related collection is a semi-join the service must evaluate for every parent, which is one reason a service may restrict how deep filter expressions may traverse.

  • Which customers does the OData filter `Customers?$filter=Orders/all(o:o/Amount gt 500)` return?
    Customers whose every order is over 500 — and also customers with no orders at all, because `all` is defined to be true for an empty collection. If those should be excluded, combine it with the short form of `any`: `$filter=Orders/any() and Orders/all(o:o/Amount gt 500)`, which first requires at least one order.
  • Inside an OData lambda such as `Orders/any(o:o/ShippingAddress ne Address)`, what does the unprefixed `Address` refer to?
    The customer's `Address`. A path prefixed with neither the lambda variable nor `$it` is evaluated in the scope of the instances at the origin of the navigation path, which are the customers here. So the filter finds customers with at least one order shipped to an address other than their own. If the variable name clashed with a customer property, the variable would take precedence, and `$it` would reach the customer's property.

saying these in an interview costs you the question

  • A $filter inside $expand also removes customers whose orders do not match
  • all returns false for a customer who has no orders
  • any always needs an argument expression
  • The any filter inlines the matching orders in the response
  • Unprefixed names inside a lambda always refer to the collection member