--- name: odata-data-sharing description: "Share data between Mendix apps over OData — published services, view entities as an abstraction layer, consumed clients and external entities. Use when exposing data from one app to another, consuming another app's OData service, or refreshing a cached contract." --- # OData Data Sharing Between Mendix Apps This skill covers how to use OData services to share data between Mendix applications, with emphasis on using view entities as an abstraction layer to decouple the API contract from the internal domain model. ## Reference files `SKILL.md` covers the architecture, the contract rules, and the conventions. The long build-outs are next door: - [`reference/walkthroughs.md`](reference/walkthroughs.md) — four complete builds, start to finish: a read-only API behind view entities, a non-persistable published entity, a read-write API with microflow handlers, publishing a microflow as an OData action, and the GraphQL variant. - [`reference/errors-and-auth.md`](reference/errors-and-auth.md) — which HTTP status each capability can actually return, the authentication methods and what Basic auth costs, and the configuration microflow for custom headers. ## When to Use This Skill - User asks to expose data from one Mendix app to another - User wants to set up inter-app communication via OData - User needs to create an API layer that abstracts internal entities - User asks about external entities, consumed/published OData services - User wants to decouple modules or apps for independent deployment - User asks about the view entity pattern for OData services - User asks about local metadata files or offline OData development ## MetadataUrl Formats `CREATE CONSUMED ODATA SERVICE` supports three formats for the `MetadataUrl` parameter: | Format | Example | Stored In Model | |--------|---------|-----------------| | **HTTP(S) URL** | `https://api.example.com/odata/v4/$metadata` | Unchanged | | **Absolute file:// URI** | `file:///Users/team/contracts/service.xml` | Unchanged | | **Relative path** | `./metadata/service.xml` or `metadata/service.xml` | **Normalized to absolute `file://`** | **Path Normalization:** - Relative paths (with or without `./`) are **automatically converted** to absolute `file://` URLs in the Mendix model - This ensures Studio Pro can properly detect local file vs HTTP metadata sources (radio button in UI) - Example: `./metadata/service.xml` → `file:///absolute/path/to/project/metadata/service.xml` **Path Resolution (before normalization):** - With project loaded (`-p` flag or REPL): relative paths are resolved against the `.mpr` file's directory - Without project: relative paths are resolved against the current working directory ## Refreshing the Cached Contract mxcli caches the `$metadata` document in the client so the model rebuilds without the service running. That cache is a snapshot, and a consumed service that gains entity sets makes it stale — the file on disk has five, the client still answers three. `CREATE OR MODIFY CONSUMED ODATA SERVICE` re-reads the contract every time it runs, so refreshing is a re-run of the statement you already have: ```mdl mdl 1; -- after refreshing ./contracts/live-now-metadata.xml from the running backend CREATE OR MODIFY CONSUMED ODATA SERVICE F1Now.NowApi ( ODataVersion: OData4, MetadataUrl: './contracts/live-now-metadata.xml', Timeout: 300, ServiceUrl: F1Now.ApiLocation ); ``` Read the verb it prints — it tells you which happened: | Output | Meaning | |--------|---------| | `Modified OData client: …` + `Refreshed $metadata: 5 entity types, 0 actions` | The contract changed and the client now carries the new one | | `Unchanged OData client: …` | The contract is identical, so nothing was written | | `Warning: could not refresh $metadata: …` | The contract could not be read; the **previously cached one is kept**, so re-run once it is reachable | Then re-import: `CREATE OR MODIFY EXTERNAL ENTITIES FROM F1Now.NowApi` maps the new entity sets. Do **not** `DROP CONSUMED ODATA SERVICE` and recreate it to force a refresh — that invalidates the client ID the existing external entities point at. Note that `ALTER CONSUMED ODATA SERVICE … SET ( MetadataUrl: … )` does *not* re-fetch. Use `CREATE OR MODIFY` when the contract is what changed. `describe` prints a client and an external entity as `create or modify …`, and re-running that output changes nothing: the rewrite keeps what it cannot print (the client's icon, UseQuerySegment, the catalog and proxy settings; each attribute's OData mapping). An external entity's attribute **types** are the service's, not the script's: declaring another (`OrderId: String(20)` for an `Edm.Int64` property) is CE6616 in mx check. Under `mdl 1` that statement is refused with nothing written; without the header it still writes, with warning `MDL-V1-REMOTETYPE`. **Use Cases for Local Metadata:** - **Offline development** — no network access required - **Testing and CI/CD** — reproducible builds with metadata snapshots - **Version control** — commit metadata files alongside code - **Pre-production** — test against upcoming API changes before deployment - **Firewall-friendly** — works in locked-down corporate environments ## ServiceUrl Must Be a Constant **IMPORTANT:** The `ServiceUrl` parameter **must always be a constant reference** (prefixed with `@`). Direct URLs are not allowed. **Correct:** ```sql mdl 1; CREATE CONSTANT ProductClient.ProductDataApiLocation ( Type: String, DefaultValue: 'http://localhost:8080/odata/productdataapi/v1/' ); CREATE CONSUMED ODATA SERVICE ProductClient.ProductDataApiClient ( ODataVersion: OData4, MetadataUrl: 'https://api.example.com/$metadata', ServiceUrl: ProductClient.ProductDataApiLocation -- ✅ Constant reference ); ``` **Incorrect:** ```sql mdl 1; CREATE CONSUMED ODATA SERVICE ProductClient.ProductDataApiClient ( ODataVersion: OData4, MetadataUrl: 'https://api.example.com/$metadata', ServiceUrl: 'https://api.example.com/odata' -- ❌ Direct URL not allowed ); ``` This enforces Mendix best practice of externalizing configuration values for different environments. ## Architecture Overview OData data sharing follows a **producer/consumer** pattern with three layers: ``` ┌─────────────────────────────────────────────┐ │ PRODUCER APP │ │ │ │ persistent entities ──▶ view entities │ │ (Shop.Customer, (Api.CustomerVE) │ │ Shop.Address) │ │ ▼ │ │ odata service │ │ (Api.CustomerApi) │ └──────────────────────┬──────────────────────┘ │ HTTP/OData4 ┌──────────────────────▼──────────────────────┐ │ CONSUMER APP │ │ │ │ odata client │ │ (Client.CustomerApiClient) │ │ ▼ │ │ external entities │ │ (Client.CustomersEE) │ │ ▼ │ │ pages & microflows │ └─────────────────────────────────────────────┘ ``` ### Why View Entities? Publishing persistent entities directly exposes your internal schema. When you change a column name, add a table, or restructure associations, every consumer app breaks. **View entities** solve this: 1. **Stable API contract** -- the view's shape stays the same even when the underlying tables change 2. **Flattened data** -- joins across multiple tables into a single flat resource (e.g., Customer + BillingAddress + DeliveryAddress into one `CustomerAddressVE`) 3. **Computed fields** -- add calculated columns like `FullAddress` or `ActivePrice` using OQL expressions 4. **Filtered datasets** -- restrict what's visible (e.g., only active products, cheap products) 5. **Aggregations** -- expose pre-aggregated metrics (e.g., orders per day, sum of line items) ## API Versioning When your API contract changes, create a new version rather than breaking existing consumers: ```sql -- v1: Original API (keep running for existing consumers) create published odata service ProductApi.ProductDataApi ( path: 'odata/productdataapi/v1/', version: '1.0.0', ... ); -- v2: New version with additional fields create published odata service ProductApi.ProductDataApi_v2 ( path: 'odata/productdataapi/v2/', version: '2.0.0', ODataVersion: OData4, ServiceName: 'ProductDataApi', Summary: 'Product API v2 - includes weight and tags', ... ) authentication basic { publish entity ProductApi.ProductWithPriceAndTagsVE as 'Product' ( ReadMode: ReadFromDatabase, InsertMode: microflow ProductApi.InsertProductV2, UpdateMode: microflow ProductApi.UpdateProductV2, DeleteMode: microflow ProductApi.DeleteProductV2 ) expose (...); }; ``` ## Folder Organization Use the `Folder` property to organize OData documents within modules. **MetadataUrl accepts three formats:** 1. **HTTP(S) URL** — fetches from remote service (production) 2. **file:///absolute/path** — reads from local absolute path 3. **./path or path/file.xml** — reads from local relative path (resolved against .mpr directory) ```sql -- Format 1: HTTP(S) URL create consumed odata service ProductClient.ProductDataApiClient folder 'Integration/ProductAPI' ( ODataVersion: OData4, MetadataUrl: 'https://api.example.com/odata/v4/$metadata' ); -- Format 2: Absolute file:// URI create consumed odata service ProductClient.ProductDataApiClient folder 'Integration/ProductAPI' ( ODataVersion: OData4, MetadataUrl: 'file:///Users/team/contracts/productdataapi.xml' ); -- Format 3a: Relative path with ./ create consumed odata service ProductClient.ProductDataApiClient folder 'Integration/ProductAPI' ( ODataVersion: OData4, MetadataUrl: './metadata/productdataapi.xml' ); -- Format 3b: Relative path without ./ create consumed odata service ProductClient.ProductDataApiClient folder 'Integration/ProductAPI' ( ODataVersion: OData4, MetadataUrl: 'metadata/productdataapi.xml' ); create published odata service ProductApi.ProductDataApi ( path: 'odata/productdataapi/v1/', version: '1.0.0', ODataVersion: OData4, folder: 'Integration/APIs' ) authentication basic { ... }; ``` Folders are created automatically if they don't exist. Use `/` for nested folders. ## Module Organization Conventions Follow this naming convention for clean separation: | Module | Purpose | Contains | |--------|---------|----------| | `Shop` | Core domain | Persistent entities, business logic | | `ShopApi` or `ShopViews` | API layer (producer) | View entities, OData service, CUD microflows | | `ShopClient` or `ShopViewsClient` | API consumer | OData client, external entities, client constants | This keeps the API contract separate from the domain logic, and the consumer separate from the producer. ## Checklist Before publishing: - [ ] View entities expose only the fields consumers need (no internal IDs unless needed for writes) - [ ] View entity has at least one `key` field for OData identity - [ ] Module role created and granted on view entities (READ, optionally WRITE) - [ ] OData service has AUTHENTICATION set (Basic, Session, or Microflow) - [ ] GRANT ACCESS ON PUBLISHED ODATA SERVICE to the API module role - [ ] CUD microflows (if writable) accept `($ViewEntity, $HttpRequest)` parameters - [ ] CUD microflows granted EXECUTE to the API module role Before consuming: - [ ] Location constant created for environment-specific URLs - [ ] OData client `MetadataUrl` points to either: - HTTP(S) URL: `https://api.example.com/$metadata` - Local file (absolute): `file:///path/to/metadata.xml` - Local file (relative): `./metadata/service.xml` (resolved against `.mpr` directory) - [ ] OData client uses `ServiceUrl: Module.Constant` for runtime endpoint - [ ] External entities match the published exposed names and types - [ ] Module role created and granted on external entities (READ, optionally CREATE/WRITE/DELETE) ## Exploration Commands Use these commands to inspect existing OData setup in a project: ```sql -- List all published and consumed services list published odata services; list consumed odata services; -- Inspect a specific service describe published odata service ShopViews.ShopViewsApi; describe consumed odata service ShopViewsClient.ShopViewsApiClient; -- See external entities and view entities list entities in ShopViewsClient; list external entities; list external actions; -- Browse available assets from cached $metadata contract list contract entities from ShopViewsClient.ShopViewsApiClient; list contract actions from ShopViewsClient.ShopViewsApiClient; describe contract entity ShopViewsClient.ShopViewsApiClient.Product; describe contract entity ShopViewsClient.ShopViewsApiClient.Product format mdl; -- Check security setup list access on odata service ShopViews.ShopViewsApi; list module roles in ShopViews; ```