skip to content

In OData, how does a function differ from an action, and what changes when either one is bound rather than unbound?

level: middleimportance: must knowfreq 28%

answer

  1. side effects decide it
  2. GET versus POST
  3. composable only when declared
  4. first parameter binds
  5. imports expose unbound operations

basics

~20 s

An OData function MUST NOT have observable side effects, must return data and is called with GET; an action may change state, may return nothing and is called with POST. Bound operations are called on a resource; unbound ones are static, reached mainly through container imports.

solid answer

~40 s

Both are service-defined operations declared in the model. A `Function` MUST NOT have observable side effects and MUST declare a return type; it is invoked with GET, parameters in the URL, and only if declared `IsComposable` may further path segments and query options follow it. An `Action` MAY have side effects and MAY return data; it is invoked with POST, non-binding parameters in the request body, and nothing can be composed after it. A bound operation (`IsBound="true"`) takes the resource it is called on as its first, binding parameter: `GET /Customers('C42')/Sales.RecentOrders(days=30)` or `POST /Orders(1001)/Sales.Cancel`. An unbound action is invoked through an `ActionImport`; an unbound function through a `FunctionImport` or as a static call inside `$filter` or `$orderby`. Bound overloads are told apart chiefly by binding parameter type.

code

http · 11 lines
http
GET /sales/Customers('C42')/Sales.RecentOrders(days=30) HTTP/1.1
Host: api.example.com
OData-Version: 4.01


POST /sales/Orders(1001)/Sales.Cancel HTTP/1.1
Host: api.example.com
OData-Version: 4.01
Content-Type: application/json

{"reason": "customer request"}

go deeper

for a junior

Recall the split: functions are side-effect-free and use GET, actions may change data and use POST. Know that both can be declared on an entity type.

for a middle

Explain binding: the first parameter is the resource the operation is called on, unbound operations go through ActionImport or FunctionImport, and only IsComposable functions can be followed by more segments.

for a senior

Show the production consequences: a state-changing function invoked with GET gets repeated by caches and retries, and per-instance advertisement lets clients hide unavailable actions.

for a principal

Weigh how far to push behaviour into operations: each bound action is a remote verb clients code against, so decide which business commands deserve a stable operation contract.

## Two kinds of operation OData's entity data model lets a service declare **operations** - custom logic beyond reading and writing entities. There are exactly two kinds, and the dividing line is **side effects**: | Aspect | Function | Action | |---|---|---| | Side effects | **MUST NOT** have observable side effects | **MAY** have side effects | | Return type | **MUST** return a value | **MAY** return a value | | Invoked with | GET | POST | | Parameters | In the URL | Non-binding ones in the request body | | Composition | Only if `IsComposable="true"` | Never | | Use inside `$filter` / `$orderby` | Yes | No | The protocol explains the composition rule: actions "cannot be further composed in order to avoid non-deterministic behavior" - appending a navigation segment or a filter to something that changes state would mix a write with a read whose result depends on it. A function marked **composable** can be followed by more path segments, key predicates and system query options appropriate to its return type. ## Bound operations An operation with `IsBound="true"` is **bound**. Its first parameter is the **binding parameter**, and a client invokes it by appending its namespace- or alias-qualified name to a URL that identifies a resource of the binding parameter's type (or a derived type): - `GET /Customers('C42')/Sales.RecentOrders(days=30)` - a function bound to `Customer`. - `POST /Orders(1001)/Sales.Cancel` - an action bound to `Order`, with any other parameters in the body. Rules worth knowing: - The binding parameter can be of any type - an entity, a collection of entities, a complex value - and **MAY** be nullable. - **Overloads.** Bound actions overload by binding parameter type: `Cancel` bound to `Order` and `Cancel` bound to `Shipment` may coexist. Bound functions overload by binding parameter type plus their non-binding parameter names and types, and every overload with the same name and binding type **MUST** return the same type. - **EntitySetPath** lets a bound operation that returns entities say which entity set they belong to, relative to the binding parameter - for example, the orders of the customer it was called on. - A service may advertise operations in an entity's payload and set one to null where it is unavailable for that instance, so a client can hide "Cancel" on an order that cannot be cancelled. ## Unbound operations and imports An operation without `IsBound` is **unbound** - a static operation of the service: 1. An **unbound action** is invoked through an `ActionImport` in the entity container: `POST /PlaceOrder`, with all parameter values in the body. Action imports are never listed in the service document. 2. An **unbound function** is invoked through a `FunctionImport` with GET - `GET /ExchangeRate(from='EUR',to='USD')` - or called directly by qualified name inside `$filter` or `$orderby`. A function import of a parameterless function may be listed in the service document if `IncludeInServiceDocument` says so (default `false`). Unbound actions do not overload at all; unbound functions overload by parameter names and types, again with one return type per name. ## Modelling an order-management service | Need | Operation | Why | |---|---|---| | Cancel an order and notify the warehouse | Action bound to `Order` | Changes state; belongs to one order | | A customer's orders from the last N days | Function bound to `Customer` | Read-only, scoped to one customer | | Place a new order from a basket | Unbound action via `ActionImport` | Changes state; no natural binding resource | | Today's exchange rate between two currencies | Unbound function via `FunctionImport` | Read-only, service-wide | Two mistakes recur. The first is declaring something a function because it "returns the order" when it also changes it: functions are invoked with GET, and anything that caches, prefetches or retries GET requests may repeat the change. The second is making every action unbound: a bound action carries its target in the URL, lets the service advertise per-instance availability and lets overloads share a name across types. ## Reading the declaration In the model, `Function` must contain exactly one `ReturnType`; `Action` may contain at most one. Both may contain `Parameter` elements, and a bound overload **MUST** declare at least one parameter, the binding one. Parameter order **MUST NOT** change unless the schema version changes; the protocol lists reordering action or function parameters among the breaking model changes.

  • Why can an action never be composed with further path segments?
    The protocol forbids it to avoid non-deterministic behaviour: an action may change state, so appending `/Lines` or a `$filter` would mix that change with a read whose result depends on it. A function has no side effects, so it may be declared `IsComposable` and a client can keep navigating or querying from its result.
  • How can a client learn that a bound action is unavailable for one particular entity?
    The service may advertise bound operations in the entity's representation and set one to null for an instance where it does not apply - for example `"#Sales.Cancel": null` on an order already shipped. The model still declares the action for the type; the per-instance advertisement tells a generic client whether to offer it.
  • Can two bound actions share a name in one schema?
    Yes. Bound actions overload by binding parameter type, so `Cancel` bound to `Order` and `Cancel` bound to `Shipment` may coexist as long as name plus binding parameter type is unique. Unbound actions cannot overload at all, and all overloads of one function name with the same binding type must declare the same return type.

saying these in an interview costs you the question

  • A function is just an action invoked with GET; either may update data.
  • Functions can never be used inside $filter or $orderby.
  • An action must always return the entity it modified.
  • An unbound action can be invoked by name without any action import.
  • Any function may have path segments and query options appended to it.