mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-10-05 14:46:55 +01:00
Improve partner onboarding and documentation navigation (#174)
Lead with a complete plugin quick start and add task-oriented usage, troubleshooting, customization, and contribution guides. Preserve the broader plugin framing, correct conflicting contract guidance, support Agents folder reviews, and align repository validation. Convert existing sample references to clickable links without changing knowledge rules. Co-authored-by: Jesper Schulz-Wedde <jesper.schulzwedde@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
a21edfec46
commit
2b5550c346
276 changed files with 1287 additions and 756 deletions
|
|
@ -21,7 +21,7 @@ LLMs treat one carrier as universal. Some assume the caption is serialised and r
|
|||
|
||||
Establish which schema versions the field is served under before changing anything about its enum. Under schema 2.0 (Microsoft's API v2.0, an explicit `$schemaversion=2.0` in the consumer contract, or another reliable context signal) the member name is the contract: keep names stable, put wording changes in `Caption`, add a value by appending a new name with an ordinal above every existing one, and retire a value through `ObsoleteState` rather than by deleting it. For a custom API that clients may still call as schema 1.0, any install of BC 17 to 23 or a caller that pins 1.0, the caption is a contract as well: change neither name nor caption in place, or publish the change as a new `APIVersion` on a new page object. A rename is out in every case: AppSourceCop AS0082 rejects it against a baseline, and dependent extensions bind to the name.
|
||||
|
||||
See sample: `api-enum-values-are-a-contract-by-name-not-ordinal.good.al`.
|
||||
See sample: [`api-enum-values-are-a-contract-by-name-not-ordinal.good.al`](api-enum-values-are-a-contract-by-name-not-ordinal.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
|
|
@ -31,7 +31,7 @@ Detection signal: a diff hunk that changes the name in a `value(...)` line while
|
|||
|
||||
The mirror image is a review defect: suppressing a caption-change finding because "the API serialises names". That holds only under schema 2.0. Do not flag a `Caption` change when the reviewer can establish schema 2.0 for every consumer; on a custom API where clients may select schema 1.0, report a caption change on an exposed value as a consumer-visible change and ask for versioning. A value appended at the end changes no contract under either schema and is never a finding.
|
||||
|
||||
See sample: `api-enum-values-are-a-contract-by-name-not-ordinal.bad.al`.
|
||||
See sample: [`api-enum-values-are-a-contract-by-name-not-ordinal.bad.al`](api-enum-values-are-a-contract-by-name-not-ordinal.bad.al).
|
||||
|
||||
## See also
|
||||
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ An API meant purely for reading — a reporting or lookup endpoint — is not re
|
|||
|
||||
For a read-only / reporting API page set all three CRUD guards off — `InsertAllowed = false`, `ModifyAllowed = false`, `DeleteAllowed = false` — and mark the page `Editable = false`. The endpoint then serves GET requests and rejects any insert, modify, or delete, matching the read-only contract regardless of the caller. Make the read-only stance explicit rather than depending on the writable default.
|
||||
|
||||
See sample: `disable-write-operations-on-read-only-api-pages.good.al`.
|
||||
See sample: [`disable-write-operations-on-read-only-api-pages.good.al`](disable-write-operations-on-read-only-api-pages.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
An API intended for read-only consumption that omits the CRUD guards, leaving `InsertAllowed`, `ModifyAllowed`, and `DeleteAllowed` at their writable defaults. The endpoint silently accepts POST, PATCH, and DELETE, so a client can mutate or remove data the API was never meant to expose for writing. The detection signal: a read-only/reporting `PageType = API` page that does not set the three `*Allowed = false` properties.
|
||||
|
||||
See sample: `disable-write-operations-on-read-only-api-pages.bad.al`.
|
||||
See sample: [`disable-write-operations-on-read-only-api-pages.bad.al`](disable-write-operations-on-read-only-api-pages.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ This is about the data-consistency contract of an API endpoint: what a consumer
|
|||
|
||||
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`.
|
||||
See sample: [`expose-only-committed-data-from-api-reads.good.al`](expose-only-committed-data-from-api-reads.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
An API intended to return committed-only data that sets no isolation level, leaving reads at the default that can observe in-flight, uncommitted writes. A consumer can fetch a row created by a concurrent transaction that is later rolled back — a dirty read that surfaces data which never durably existed. The detection signal: a committed-only read API with no `Rec.ReadIsolation := IsolationLevel::ReadCommitted` in `OnOpenPage`.
|
||||
|
||||
See sample: `expose-only-committed-data-from-api-reads.bad.al`.
|
||||
See sample: [`expose-only-committed-data-from-api-reads.bad.al`](expose-only-committed-data-from-api-reads.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ An API consumer that needs to *do* something to a record — post it, ship it, r
|
|||
|
||||
Declare the operation as `[ServiceEnabled] procedure Post(var ActionContext: WebServiceActionContext)` on the API page. Inside, perform the operation against `Rec`, then call a `SetActionResponse` helper that writes the result — the bound record and its id — back into the `WebServiceActionContext` so the caller receives a well-formed response. The operation is now an explicit, named endpoint action separate from ordinary field writes.
|
||||
|
||||
See sample: `expose-operations-as-bound-actions.good.al`.
|
||||
See sample: [`expose-operations-as-bound-actions.good.al`](expose-operations-as-bound-actions.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Exposing a writable Boolean (for example `posted`) whose `OnValidate` performs the posting. A client that PATCHes the field to `true` — an action indistinguishable from any other data edit — silently triggers a side-effecting business operation. The detection signal: an API page field whose `OnValidate` posts, ships, or releases, instead of a `[ServiceEnabled]` bound action.
|
||||
|
||||
See sample: `expose-operations-as-bound-actions.bad.al`.
|
||||
See sample: [`expose-operations-as-bound-actions.bad.al`](expose-operations-as-bound-actions.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ Every BC table carries a `SystemId` — an immutable GUID assigned at insert and
|
|||
|
||||
Set `ODataKeyFields = SystemId` so OData routes records by the stable GUID, and expose it as `field(id; Rec.SystemId)` marked `Editable = false`. Clients then address a record at `.../customers(<guid>)`, an identity that survives any rename of the business key. Keep the business key (for example `No.`) as an ordinary exposed field, not as the OData key.
|
||||
|
||||
See sample: `expose-systemid-as-the-api-key.good.al`.
|
||||
See sample: [`expose-systemid-as-the-api-key.good.al`](expose-systemid-as-the-api-key.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Setting `ODataKeyFields = "No."` so the endpoint addresses records by a renamable business field. As soon as a user changes that `No.`, every external reference built on the old value points at nothing, silently breaking integrations. The detection signal: `ODataKeyFields` set to a business field rather than `SystemId`, or an API page that exposes no `id` field bound to `Rec.SystemId`.
|
||||
|
||||
See sample: `expose-systemid-as-the-api-key.bad.al`.
|
||||
See sample: [`expose-systemid-as-the-api-key.bad.al`](expose-systemid-as-the-api-key.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,13 +17,13 @@ application-area: [all]
|
|||
|
||||
Define the child foreign key as `Guid` with a `TableRelation` to the parent table's `SystemId`, then use `SubPageLink = "<Parent Id>" = Field(SystemId)` on the parent API page. A child collection may omit `Multiplicity` and rely on the default 1:N relationship, or declare `Multiplicity = Many` explicitly. Set `Multiplicity = ZeroOrOne` when the intended navigation metadata is a singleton.
|
||||
|
||||
See sample: `link-api-parts-on-systemid-and-set-multiplicity.good.al`.
|
||||
See sample: [`link-api-parts-on-systemid-and-set-multiplicity.good.al`](link-api-parts-on-systemid-and-set-multiplicity.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
On a parent API with `ODataKeyFields = SystemId`, linking a child business field such as `"Order No."` to the parent's `"No."` creates a second identity scheme for navigation instead of using the contract's stable GUID. A separate defect is an explicit `Multiplicity` that conflicts with the intended shape, such as `ZeroOrOne` on an order-lines collection or `Many` on a singleton. Do not treat omission alone as a defect: it is valid for a collection because the default is 1:N, while an intended singleton must explicitly use `Multiplicity = ZeroOrOne`.
|
||||
|
||||
See sample: `link-api-parts-on-systemid-and-set-multiplicity.bad.al`.
|
||||
See sample: [`link-api-parts-on-systemid-and-set-multiplicity.bad.al`](link-api-parts-on-systemid-and-set-multiplicity.bad.al).
|
||||
|
||||
## Source
|
||||
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ An API page needs `APIPublisher`, `APIGroup`, `EntityName`, `EntitySetName`, and
|
|||
|
||||
Declare the five routing/entity properties required by the API page and set `APIVersion` explicitly for a stable published contract, for example `'v1.0'`. Expose the record's fields inside a repeater under `area(content)`. Review missing routing metadata as a malformed API definition, but review a missing `APIVersion` as unintended publication under `beta`, not as an unpublished endpoint.
|
||||
|
||||
See sample: `set-required-api-page-properties.good.al`.
|
||||
See sample: [`set-required-api-page-properties.good.al`](set-required-api-page-properties.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Leaving out `APIPublisher`, `APIGroup`, `EntityName`, `EntitySetName`, or `SourceTable` leaves the API definition incomplete. A subtler contract defect is declaring all of those but omitting `APIVersion`: the page is exposed as `beta`, which is valid runtime behavior but not the explicit stable route a production client expects.
|
||||
|
||||
See sample: `set-required-api-page-properties.bad.al`.
|
||||
See sample: [`set-required-api-page-properties.bad.al`](set-required-api-page-properties.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ Once an API version is published, external clients depend on its exact shape —
|
|||
|
||||
Keep the existing page object and its `APIVersion = 'v1.0'` contract unchanged. Copy the page to a new object ID, set that object's `APIVersion = 'v2.0'`, and make the v2-only shape changes there. A multi-value `APIVersion` list is appropriate only when the exact same page shape is supported under each listed version.
|
||||
|
||||
See sample: `version-apis-by-adding-not-mutating-published-versions.good.al`.
|
||||
See sample: [`version-apis-by-adding-not-mutating-published-versions.good.al`](version-apis-by-adding-not-mutating-published-versions.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Editing the published `v1.0` page in place breaks its clients. So does adding `v2.0` to that same page and assuming subsequent field changes apply only to v2: both routes use one object shape. The detection signal is a breaking shape change without a separate API page object retaining the old version.
|
||||
|
||||
See sample: `version-apis-by-adding-not-mutating-published-versions.bad.al`.
|
||||
See sample: [`version-apis-by-adding-not-mutating-published-versions.bad.al`](version-apis-by-adding-not-mutating-published-versions.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,13 +17,13 @@ Business Central can subscribe only to eligible API pages, not every endpoint th
|
|||
|
||||
Before creating a subscription, confirm the resource appears in `webhookSupportedResources` and that a custom endpoint is an API page with a single stable key over an eligible persistent table. Use one validation path that echoes `validationToken` for both create (`POST`) and renew (`PATCH`) handshakes. Track `expirationDateTime` and renew before expiry: online subscriptions expire after three days, while on-premises lifetime defaults to three days and can be changed with `ApiSubscriptionExpiration`.
|
||||
|
||||
See samples: `webhook-eligibility-and-validationtoken-renewal.good.al` and `webhook-eligibility-and-validationtoken-renewal.good.js`.
|
||||
See samples: [`webhook-eligibility-and-validationtoken-renewal.good.al`](webhook-eligibility-and-validationtoken-renewal.good.al) and [`webhook-eligibility-and-validationtoken-renewal.good.js`](webhook-eligibility-and-validationtoken-renewal.good.js).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Attempting to subscribe to an API query, temporary/composite/system-table/Job Queue Entry API page, or assuming a successful create handshake makes renewal automatic. Composite includes an explicit multi-field `ODataKeyFields` and a missing `ODataKeyFields` when the source table's primary key has multiple fields. A renewal issues the same validation challenge; a notification handler that ignores the query-string token cannot create or renew the subscription.
|
||||
|
||||
See samples: `webhook-eligibility-and-validationtoken-renewal.bad.al` and `webhook-eligibility-and-validationtoken-renewal.bad.js`.
|
||||
See samples: [`webhook-eligibility-and-validationtoken-renewal.bad.al`](webhook-eligibility-and-validationtoken-renewal.bad.al) and [`webhook-eligibility-and-validationtoken-renewal.bad.js`](webhook-eligibility-and-validationtoken-renewal.bad.js).
|
||||
|
||||
## Source
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue