From 94c371dd90be3b73af38e254e4e30ba2ba0224a9 Mon Sep 17 00:00:00 2001 From: Jesper Schulz-Wedde Date: Wed, 24 Jun 2026 12:47:28 +0200 Subject: [PATCH] Trim web-services domain to 6 non-duplicative articles Drop API entity-naming/camelCase and DelayedInsert articles (owned by the style domain). Reframe the committed-data and API-versioning articles to stay strictly within the endpoint design/behavior lane, and update the leaf skill's worklist tokens and Output example accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...delayedinsert-on-writable-api-pages.bad.al | 37 ------------------- ...elayedinsert-on-writable-api-pages.good.al | 36 ------------------ ...ble-delayedinsert-on-writable-api-pages.md | 26 ------------- ...pose-only-committed-data-from-api-reads.md | 4 +- ...-with-singular-and-plural-camelcase.bad.al | 34 ----------------- ...with-singular-and-plural-camelcase.good.al | 32 ---------------- ...ties-with-singular-and-plural-camelcase.md | 26 ------------- ...-adding-not-mutating-published-versions.md | 2 +- .../skills/review/al-web-services-review.md | 10 ++--- 9 files changed, 8 insertions(+), 199 deletions(-) delete mode 100644 microsoft/knowledge/web-services/enable-delayedinsert-on-writable-api-pages.bad.al delete mode 100644 microsoft/knowledge/web-services/enable-delayedinsert-on-writable-api-pages.good.al delete mode 100644 microsoft/knowledge/web-services/enable-delayedinsert-on-writable-api-pages.md delete mode 100644 microsoft/knowledge/web-services/name-api-entities-with-singular-and-plural-camelcase.bad.al delete mode 100644 microsoft/knowledge/web-services/name-api-entities-with-singular-and-plural-camelcase.good.al delete mode 100644 microsoft/knowledge/web-services/name-api-entities-with-singular-and-plural-camelcase.md diff --git a/microsoft/knowledge/web-services/enable-delayedinsert-on-writable-api-pages.bad.al b/microsoft/knowledge/web-services/enable-delayedinsert-on-writable-api-pages.bad.al deleted file mode 100644 index bb21587..0000000 --- a/microsoft/knowledge/web-services/enable-delayedinsert-on-writable-api-pages.bad.al +++ /dev/null @@ -1,37 +0,0 @@ -// Writable API page with no DelayedInsert: on a POST the platform may insert the -// row on the first assigned field, before the rest of the payload is applied, -// failing validation that needs later fields or persisting a partial record. -page 50347 "WS DelayedInsert Bad" -{ - PageType = API; - APIPublisher = 'contoso'; - APIGroup = 'sales'; - APIVersion = 'v1.0'; - EntityName = 'customer'; - EntitySetName = 'customers'; - ODataKeyFields = SystemId; - SourceTable = Customer; - - layout - { - area(content) - { - repeater(records) - { - field(id; Rec.SystemId) - { - Caption = 'id'; - Editable = false; - } - field(number; Rec."No.") - { - Caption = 'number'; - } - field(displayName; Rec.Name) - { - Caption = 'displayName'; - } - } - } - } -} diff --git a/microsoft/knowledge/web-services/enable-delayedinsert-on-writable-api-pages.good.al b/microsoft/knowledge/web-services/enable-delayedinsert-on-writable-api-pages.good.al deleted file mode 100644 index ceac51e..0000000 --- a/microsoft/knowledge/web-services/enable-delayedinsert-on-writable-api-pages.good.al +++ /dev/null @@ -1,36 +0,0 @@ -page 50346 "WS DelayedInsert Good" -{ - PageType = API; - Caption = 'customer'; - APIPublisher = 'contoso'; - APIGroup = 'sales'; - APIVersion = 'v1.0'; - EntityName = 'customer'; - EntitySetName = 'customers'; - ODataKeyFields = SystemId; - SourceTable = Customer; - DelayedInsert = true; - - layout - { - area(content) - { - repeater(records) - { - field(id; Rec.SystemId) - { - Caption = 'id'; - Editable = false; - } - field(number; Rec."No.") - { - Caption = 'number'; - } - field(displayName; Rec.Name) - { - Caption = 'displayName'; - } - } - } - } -} diff --git a/microsoft/knowledge/web-services/enable-delayedinsert-on-writable-api-pages.md b/microsoft/knowledge/web-services/enable-delayedinsert-on-writable-api-pages.md deleted file mode 100644 index 89a2985..0000000 --- a/microsoft/knowledge/web-services/enable-delayedinsert-on-writable-api-pages.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -bc-version: [all] -domain: web-services -keywords: [api-page, delayedinsert, writable-api, insert-trigger, mandatory-fields, partial-record] -technologies: [al] -countries: [w1] -application-area: [all] ---- - -# Set DelayedInsert = true on writable API pages - -## Description - -When a client POSTs a new record to an API page, the platform receives the posted fields one at a time. Without `DelayedInsert = true`, the page can insert the row as soon as the first field is assigned — before the remaining fields in the payload have been applied. That premature insert fails mandatory-field and table validation that depends on fields arriving later, or it persists a partial record that violates the table's invariants. Setting `DelayedInsert = true` defers the actual `Insert` until every posted field has been assigned, so validation sees the complete record. The official API page template ships with this property set. LLMs that generate an API page by analogy with an ordinary editable page routinely omit it, because interactive pages do not need it; this file is remedial because the default is a footgun specific to the API/POST flow. - -## Best Practice - -On any API page that accepts inserts (a writable entity), set `DelayedInsert = true`. The platform then collects all posted fields and inserts the row once, after assignment is complete, so mandatory-field checks and `OnInsert` validation run against the full record. Treat `DelayedInsert = true` as a default for every writable API page, exactly as the standard API page template does. - -See sample: `enable-delayedinsert-on-writable-api-pages.good.al`. - -## Anti Pattern - -A writable API page that omits `DelayedInsert`. On a POST the platform may insert the row on the first assigned field, before the rest of the payload lands — failing validation that needs the later fields, or leaving a half-populated record behind. The detection signal: a writable `PageType = API` page (one that allows inserts) with no `DelayedInsert = true`. - -See sample: `enable-delayedinsert-on-writable-api-pages.bad.al`. diff --git a/microsoft/knowledge/web-services/expose-only-committed-data-from-api-reads.md b/microsoft/knowledge/web-services/expose-only-committed-data-from-api-reads.md index 842c127..739b5aa 100644 --- a/microsoft/knowledge/web-services/expose-only-committed-data-from-api-reads.md +++ b/microsoft/knowledge/web-services/expose-only-committed-data-from-api-reads.md @@ -11,11 +11,11 @@ application-area: [all] ## Description -By default a read can observe uncommitted changes made by other, still-open transactions running concurrently. For most interactive pages that is harmless, but an API whose contract is "return only data that is durably committed" must not leak those dirty reads — a consumer could fetch a row that a concurrent transaction later rolls back, and act on data that never really existed. From runtime 22.0 (BC 2023 release wave 1) AL exposes `Rec.ReadIsolation`, letting a page pin its isolation level. Setting `Rec.ReadIsolation := IsolationLevel::ReadCommitted;` in `OnOpenPage` guarantees the endpoint only returns committed rows. LLMs rarely set isolation explicitly because the platform's default "just works" for ordinary UI; this file is remedial because committed-only API semantics require an explicit opt-in that the model would not add on its own. +This is about the data-consistency contract of an API endpoint: what a consumer receives when it reads. By default an API read can return in-flight rows that a concurrent, still-open transaction has written but not yet committed. For an endpoint whose contract is "return only data that is durably committed," that is wrong — a consumer could fetch a row that the writing transaction later rolls back, then act on data that never really existed. From runtime 22.0 (BC 2023 release wave 1) an API page can pin the isolation level its reads use: setting `Rec.ReadIsolation := IsolationLevel::ReadCommitted;` in the page's `OnOpenPage` trigger makes the endpoint expose only committed rows. LLMs rarely set this on an API page because the platform default "just works" for ordinary UI; this file is remedial because the committed-only endpoint contract requires an explicit opt-in the model would not add on its own. ## Best Practice -For an API page that must expose only committed data, set the isolation level once as the page opens: in the `OnOpenPage` trigger write `Rec.ReadIsolation := IsolationLevel::ReadCommitted;`. Every subsequent read on that record variable then ignores uncommitted writes from concurrent transactions, so the endpoint never returns a row that another transaction might still roll back. +For an API page that must expose only committed data, set the endpoint's read isolation once as the page opens: in the `OnOpenPage` trigger write `Rec.ReadIsolation := IsolationLevel::ReadCommitted;`. Every read the endpoint then serves ignores uncommitted writes from concurrent transactions, so a consumer never receives a row that another transaction might still roll back. See sample: `expose-only-committed-data-from-api-reads.good.al`. diff --git a/microsoft/knowledge/web-services/name-api-entities-with-singular-and-plural-camelcase.bad.al b/microsoft/knowledge/web-services/name-api-entities-with-singular-and-plural-camelcase.bad.al deleted file mode 100644 index d164847..0000000 --- a/microsoft/knowledge/web-services/name-api-entities-with-singular-and-plural-camelcase.bad.al +++ /dev/null @@ -1,34 +0,0 @@ -// Swapped and miscased entity names: EntityName is plural and PascalCase while -// EntitySetName is singular. The single-record name reads as a collection and -// vice versa, and the leading capitals violate camelCase. -page 50343 "WS Entity Naming Bad" -{ - PageType = API; - APIPublisher = 'contoso'; - APIGroup = 'sales'; - APIVersion = 'v1.0'; - EntityName = 'Customers'; - EntitySetName = 'Customer'; - ODataKeyFields = SystemId; - SourceTable = Customer; - DelayedInsert = true; - - layout - { - area(content) - { - repeater(records) - { - field(id; Rec.SystemId) - { - Caption = 'id'; - Editable = false; - } - field(displayName; Rec.Name) - { - Caption = 'displayName'; - } - } - } - } -} diff --git a/microsoft/knowledge/web-services/name-api-entities-with-singular-and-plural-camelcase.good.al b/microsoft/knowledge/web-services/name-api-entities-with-singular-and-plural-camelcase.good.al deleted file mode 100644 index 5d52359..0000000 --- a/microsoft/knowledge/web-services/name-api-entities-with-singular-and-plural-camelcase.good.al +++ /dev/null @@ -1,32 +0,0 @@ -page 50342 "WS Entity Naming Good" -{ - PageType = API; - Caption = 'customer'; - APIPublisher = 'contoso'; - APIGroup = 'sales'; - APIVersion = 'v1.0'; - EntityName = 'customer'; - EntitySetName = 'customers'; - ODataKeyFields = SystemId; - SourceTable = Customer; - DelayedInsert = true; - - layout - { - area(content) - { - repeater(records) - { - field(id; Rec.SystemId) - { - Caption = 'id'; - Editable = false; - } - field(displayName; Rec.Name) - { - Caption = 'displayName'; - } - } - } - } -} diff --git a/microsoft/knowledge/web-services/name-api-entities-with-singular-and-plural-camelcase.md b/microsoft/knowledge/web-services/name-api-entities-with-singular-and-plural-camelcase.md deleted file mode 100644 index c4e425e..0000000 --- a/microsoft/knowledge/web-services/name-api-entities-with-singular-and-plural-camelcase.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -bc-version: [all] -domain: web-services -keywords: [api-page, entityname, entitysetname, camelcase, singular-plural, apiversion, naming-convention] -technologies: [al] -countries: [w1] -application-area: [all] ---- - -# Name API entities with a singular EntityName and a plural EntitySetName, both camelCase - -## Description - -API v2 entity names follow a strict convention: `EntityName` is the singular noun for one record (`customer`), `EntitySetName` is its plural for the collection (`customers`), and both are camelCase — a lowercase first letter, no spaces or underscores. `APIVersion` follows the `vX.Y` shape (`v1.0`). The platform treats these as contract-shaping rules, not cosmetics: a casing violation raises a compiler warning and a naming violation (using a plural where the singular is expected, or PascalCase where camelCase is expected) is flagged as an error. LLMs frequently default to BC's PascalCase object-naming habit (`Customer`, `SalesOrders`) and carry it into these properties, producing client-facing entity names that read wrong and trip the analyzer. This file is remedial because the singular/plural split is the opposite of how a developer names AL objects. - -## Best Practice - -Pick the singular camelCase noun for `EntityName` and its plural for `EntitySetName`: `EntityName = 'customer'` with `EntitySetName = 'customers'`; `EntityName = 'salesOrder'` with `EntitySetName = 'salesOrders'`. Keep the first letter lowercase and use camelCase for compound names. Set `APIVersion` to a `vX.Y` literal such as `'v1.0'`. The pair should read naturally in a URL: one `customer`, a set of `customers`. - -See sample: `name-api-entities-with-singular-and-plural-camelcase.good.al`. - -## Anti Pattern - -Swapping and miscasing the pair — `EntityName = 'Customers'` (plural and PascalCase) with `EntitySetName = 'Customer'` (singular). The single-record name now reads as a collection, the collection name reads as one record, and the leading capitals violate camelCase. The detection signal: an `EntityName` that is plural or starts with an uppercase letter, or an `EntitySetName` that is singular. - -See sample: `name-api-entities-with-singular-and-plural-camelcase.bad.al`. diff --git a/microsoft/knowledge/web-services/version-apis-by-adding-not-mutating-published-versions.md b/microsoft/knowledge/web-services/version-apis-by-adding-not-mutating-published-versions.md index 7910f31..af998ed 100644 --- a/microsoft/knowledge/web-services/version-apis-by-adding-not-mutating-published-versions.md +++ b/microsoft/knowledge/web-services/version-apis-by-adding-not-mutating-published-versions.md @@ -11,7 +11,7 @@ application-area: [all] ## Description -Once an API version is published, external clients depend on its exact shape — the entity name, the set of exposed fields, the key. Changing any of that on the published version is a breaking change delivered silently: existing integrations that worked yesterday fail today with no warning. AL supports versioning directly because `APIVersion` accepts a *list* of versions on the same page. The correct way to evolve a published API is to add a new version (`'v2.0'`) alongside the existing one (`'v1.0'`) so both contracts are served, letting clients migrate on their own schedule. LLMs tend to "fix" an API by editing the live version in place, because in ordinary code you just change what's wrong; this file is remedial because a published API contract is immutable in a way ordinary internal code is not. This is API-specific versioning and complements, without duplicating, the general breaking-changes domain. +Once an API version is published, external clients depend on its exact shape — the entity name, the set of exposed fields, the key — as a frozen contract. Changing any of that on the already-published version is a breaking change delivered silently: integrations that worked yesterday fail today with no warning. The platform gives you a clean way to evolve without breaking anyone, because `APIVersion` accepts a *list* of versions on one page. The correct way to change a published API is to add the new version (`'v2.0'`) alongside the existing one (`'v1.0'`) — or publish a new API page for it — so both contracts are served side by side and clients migrate on their own schedule. LLMs tend to "fix" an API by editing the live version in place, because in ordinary code you just change what's wrong; this file is remedial because a published API version is an immutable contract in a way ordinary internal code is not. ## Best Practice diff --git a/microsoft/skills/review/al-web-services-review.md b/microsoft/skills/review/al-web-services-review.md index 5d7c8f2..4109722 100644 --- a/microsoft/skills/review/al-web-services-review.md +++ b/microsoft/skills/review/al-web-services-review.md @@ -39,7 +39,7 @@ Narrow the relevant files to the subset that applies to the changes under review - The changed AL object names and types — especially page objects declared with `PageType = API`, and any procedure on such a page that exposes a bound action. - The changed properties and triggers, weighted toward API page metadata (`APIPublisher`, `APIGroup`, `APIVersion`, `EntityName`, `EntitySetName`, `ODataKeyFields`, `SourceTable`), CRUD guards (`InsertAllowed`, `ModifyAllowed`, `DeleteAllowed`, `Editable`), the `OnOpenPage` trigger, and `OnValidate` triggers on exposed fields. -- Tokens extracted from the diff that relate to API surface and behaviour (`PageType`, `API`, `APIPublisher`, `APIGroup`, `APIVersion`, `EntityName`, `EntitySetName`, `ODataKeyFields`, `SystemId`, `DelayedInsert`, `ServiceEnabled`, `WebServiceActionContext`, `SetActionResponse`, `ReadIsolation`, `IsolationLevel`, `ReadCommitted`, `InsertAllowed`, `ModifyAllowed`, `DeleteAllowed`, `Editable`, `SourceTable`). +- Tokens extracted from the diff that relate to API surface and behaviour (`PageType`, `API`, `APIPublisher`, `APIGroup`, `APIVersion`, `EntityName`, `EntitySetName`, `ODataKeyFields`, `SystemId`, `ServiceEnabled`, `WebServiceActionContext`, `SetActionResponse`, `ReadIsolation`, `IsolationLevel`, `ReadCommitted`, `InsertAllowed`, `ModifyAllowed`, `DeleteAllowed`, `Editable`, `SourceTable`). A file enters the candidate worklist when its `keywords` intersect the extracted tokens or its topic (derived from the index entry's `path`, `title`, and `description`) matches a changed object type. Read an article's full file — its `## Best Practice` / `## Anti Pattern` bodies — only after it makes the worklist; candidate selection uses the index alone. @@ -63,7 +63,7 @@ Set `confidence` to: After evaluating each worklist entry, also consider whether the diff exhibits a web-services defect the agent recognises from its general AL knowledge that no knowledge file in the worklist covers. Such candidates are agent findings within this skill's domain — emit them with `references: []`, an `id` slug prefixed with `agent:`, `confidence` capped at `medium`, `severity` capped at `minor` (agent findings are advisory and non-gating), and a `message` that is self-contained (describing both the issue and a concrete recommendation, since there is no knowledge-file footer for the consumer to fall back on). Hold every candidate to the precision bar in `skills/do.md` (*Agent findings*): emit only a concrete, material web-services defect a knowledgeable BC reviewer would agree is wrong — steelman it first and drop anything stylistic, speculative, dependent on code outside the diff, or merely a valid alternative; when in doubt, omit. The scope is strictly API pages and web-service surfaces; defects outside this domain belong to other leaves and MUST NOT be emitted here. Before emitting, check the worklist for a knowledge file that matches the candidate — if one exists, upgrade the candidate to a knowledge-backed finding instead. See `skills/do.md` for the full contract. -For every emitted finding, decide whether the fix is mechanical. A fix is mechanical when it is small, local, and unambiguous from the diff context (for example: add a missing `DelayedInsert = true`; set `ODataKeyFields = SystemId`; add the three `*Allowed = false` guards to a read-only page). For mechanical findings, emit `findings[].suggested-code` with the literal replacement for the source lines indicated by `location`. The payload must be a verbatim replacement — no diff markers, no fences, no commentary — that the consumer can render as a one-click suggestion. When a `.good.al` companion exists and the diff context matches the `.bad.al` shape, adapt the `.good.al` replacement into `suggested-code`. +For every emitted finding, decide whether the fix is mechanical. A fix is mechanical when it is small, local, and unambiguous from the diff context (for example: set `ODataKeyFields = SystemId`; add the three `*Allowed = false` guards to a read-only page; add the missing `OnOpenPage` isolation assignment). For mechanical findings, emit `findings[].suggested-code` with the literal replacement for the source lines indicated by `location`. The payload must be a verbatim replacement — no diff markers, no fences, no commentary — that the consumer can render as a one-click suggestion. When a `.good.al` companion exists and the diff context matches the `.bad.al` shape, adapt the `.good.al` replacement into `suggested-code`. Omit `suggested-code` only when the appropriate fix depends on context the skill cannot determine, when multiple defensible replacements exist, or when the fix spans non-contiguous code. If a finding is mechanical-looking but you omit `suggested-code`, set `findings[].suggested-code-omission-reason` to a short explanation. See `skills/do.md` for the full contract. @@ -103,15 +103,15 @@ Output conforms to the DO output contract. A populated example: "confidence": "high" }, { - "id": "microsoft/knowledge/web-services/enable-delayedinsert-on-writable-api-pages.md", + "id": "microsoft/knowledge/web-services/expose-systemid-as-the-api-key.md", "severity": "minor", - "message": "This writable API page omits DelayedInsert = true, so a POST can insert the row before all posted fields are assigned. Add DelayedInsert = true.", + "message": "This API page sets ODataKeyFields to a renamable business field instead of SystemId, so stored references break when the business key changes. Set ODataKeyFields = SystemId and expose field(id; Rec.SystemId).", "location": { "file": "src/Api/CustomerApi.Page.al", "line": 9 }, "references": [ - { "path": "microsoft/knowledge/web-services/enable-delayedinsert-on-writable-api-pages.md" } + { "path": "microsoft/knowledge/web-services/expose-systemid-as-the-api-key.md" } ], "confidence": "high" }