Merge main into integration

Resolve testing and web-services review routing conflicts by preserving the approved reliability guidance and incorporating current main coverage.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Jesper Schulz-Wedde 2026-09-29 12:59:41 +02:00
commit 008d458e75
70 changed files with 1406 additions and 9 deletions

View file

@ -0,0 +1,27 @@
page 50101 "Customer Info API"
{
PageType = API;
APIPublisher = 'contoso';
APIGroup = 'sales';
APIVersion = 'v1.0';
EntityName = 'customerInfo';
EntitySetName = 'customerInfos';
SourceTable = Customer;
ODataKeyFields = "No.";
InsertAllowed = true;
layout
{
area(content)
{
repeater(General)
{
field(customerNo; Rec."No.")
{
Editable = false; // consumer must supply "No." on POST — this rejects it
}
field(name; Rec.Name) { }
}
}
}
}

View file

@ -0,0 +1,25 @@
page 50101 "Customer Info API"
{
PageType = API;
APIPublisher = 'contoso';
APIGroup = 'sales';
APIVersion = 'v1.0';
EntityName = 'customerInfo';
EntitySetName = 'customerInfos';
SourceTable = Customer;
ODataKeyFields = "No.";
InsertAllowed = true;
DelayedInsert = true;
layout
{
area(content)
{
repeater(General)
{
field(customerNo; Rec."No.") { }
field(name; Rec.Name) { }
}
}
}
}

View file

@ -0,0 +1,28 @@
---
bc-version: [all]
domain: web-services
keywords: [api-page, key-fields, editable, insert, odata]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Keep Consumer-Provided Key Fields Editable on API Pages
> Contributions welcome — open a PR to refine or extend this article.
## Description
A field listed in `ODataKeyFields` cannot have `Editable = false` when the API page allows inserts and the field's value must be supplied by the caller. Marking it read-only removes the field from the OData write schema, so a POST that includes it is rejected as an unknown property. This only applies to keys the consumer must supply — a system-generated key such as `SystemId` is a valid exception, since Business Central assigns its value automatically on insert.
## Best Practice
Leave every consumer-supplied key field referenced in `ODataKeyFields` without `Editable = false` on pages where `InsertAllowed = true`, so the OData layer accepts it as a writable property on POST.
See sample: [`api-page-key-fields-must-be-editable-on-insert.good.al`](api-page-key-fields-must-be-editable-on-insert.good.al).
## Anti Pattern
Marking a consumer-provided key field `Editable = false`, out of habit or for perceived safety. This silently breaks create operations with a generic `BadRequest` instead of a clear validation error.
See sample: [`api-page-key-fields-must-be-editable-on-insert.bad.al`](api-page-key-fields-must-be-editable-on-insert.bad.al).

View file

@ -0,0 +1,24 @@
page 50102 "Project Task API"
{
PageType = API;
APIPublisher = 'contoso';
APIGroup = 'jobs';
APIVersion = 'v1.0';
EntityName = 'projectTask';
EntitySetName = 'projectTasks';
SourceTable = "Project Task";
layout
{
area(content)
{
repeater(General)
{
// "Remaining Hours" is a stored field, set inside the
// OnValidate of "Budgeted Hours" — it goes stale whenever
// "Hours Used" changes through any other path.
field(remainingHours; Rec."Remaining Hours") { }
}
}
}
}

View file

@ -0,0 +1,33 @@
page 50102 "Project Task API"
{
PageType = API;
APIPublisher = 'contoso';
APIGroup = 'jobs';
APIVersion = 'v1.0';
EntityName = 'projectTask';
EntitySetName = 'projectTasks';
SourceTable = "Project Task";
DelayedInsert = true;
layout
{
area(content)
{
repeater(General)
{
field(remainingHours; RemainingHoursCalc) { }
field(budgetedHours; Rec."Budgeted Hours") { }
field(hoursUsed; Rec."Hours Used") { }
}
}
}
trigger OnAfterGetRecord()
begin
Rec.CalcFields("Hours Used");
RemainingHoursCalc := Rec."Budgeted Hours" - Rec."Hours Used";
end;
var
RemainingHoursCalc: Decimal;
}

View file

@ -0,0 +1,28 @@
---
bc-version: [all]
domain: web-services
keywords: [api-page, derived-fields, exposure, odata]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Recalculate Stored Derived Fields Before Exposing Them on API Pages
> Contributions welcome — open a PR to refine or extend this article.
## Description
A stored field whose value is derived from other fields inside an `OnValidate` trigger only updates when that specific trigger fires. If the underlying source data changes through some other path, the stored value goes stale without raising any error. Exposing such a field directly on an API page hands external consumers a snapshot that may be significantly out of date.
## Best Practice
Recalculate the derived value in `OnAfterGetRecord` from its authoritative source — typically a FlowField — using a page-level variable, and expose that recalculated value instead of the stale stored field. Whether to also expose the source fields is a separate design decision, not a requirement of this pattern; keep the API contract scoped to what consumers actually need. If letting the consumer verify the recalculation is itself a requirement, expose every field the calculation reads, not just one of them — a derived value with two inputs needs both exposed, or the "verification" is incomplete.
See sample: [`stored-derived-fields-must-not-be-exposed-directly.good.al`](stored-derived-fields-must-not-be-exposed-directly.good.al).
## Anti Pattern
Exposing the stored field directly via `Rec`, trusting that it was kept in sync by whichever trigger last touched it.
See sample: [`stored-derived-fields-must-not-be-exposed-directly.bad.al`](stored-derived-fields-must-not-be-exposed-directly.bad.al).