Add 18 community AL/BC patterns across style, data-modeling, web-services, appsource, breaking-changes, performance, and testing

Contributed by CURABIS ApS, generalized from patterns observed across real AppSource/PTE development. Each article follows the knowledge file format (frontmatter, Description/Best Practice/Anti Pattern, sibling .good.al/.bad.al samples).
This commit is contained in:
Michael Dieringer 2026-09-04 20:43:00 +02:00
parent 07e324ddbc
commit 057e17c202
52 changed files with 1109 additions and 0 deletions

View file

@ -0,0 +1,24 @@
page 50100 "Item Availability API"
{
PageType = API;
APIPublisher = 'contoso';
APIGroup = 'inventory';
APIVersion = 'v1.0';
EntityName = 'itemAvailability';
EntitySetName = 'itemAvailabilities';
SourceTable = Item;
layout
{
area(content)
{
repeater(General)
{
field(itemNo; Rec."No.") { }
field(quantityOnHand; Rec.Inventory) { }
// No OnAfterGetRecord CalcFields — Inventory is a FlowField
// and returns 0 to every consumer.
}
}
}
}

View file

@ -0,0 +1,27 @@
page 50100 "Item Availability API"
{
PageType = API;
APIPublisher = 'contoso';
APIGroup = 'inventory';
APIVersion = 'v1.0';
EntityName = 'itemAvailability';
EntitySetName = 'itemAvailabilities';
SourceTable = Item;
layout
{
area(content)
{
repeater(General)
{
field(itemNo; Rec."No.") { }
field(quantityOnHand; Rec.Inventory) { }
}
}
}
trigger OnAfterGetRecord()
begin
Rec.CalcFields(Inventory);
end;
}

View file

@ -0,0 +1,28 @@
---
bc-version: [all]
domain: web-services
keywords: [api-page, flowfield, calcfields, odata]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Explicitly Calculate FlowFields on API Pages
> Contributions welcome — open a PR to refine or extend this article.
## Description
FlowFields are not stored in the database — Business Central computes them on demand. Regular pages trigger that calculation automatically while rendering, but API pages do not. A FlowField referenced in an API page's layout returns an empty value to external consumers unless it is calculated explicitly, producing a silent data gap in OData responses that is easy to miss in review.
## Best Practice
Call `CalcFields` for every FlowField referenced in the page layout from the `OnAfterGetRecord` trigger, combining multiple fields into a single call.
See sample: `api-page-flowfields-must-be-calcfields.good.al`.
## Anti Pattern
Relying on the implicit calculation that regular pages perform. Any FlowField left out of the `CalcFields` call returns a blank value to every API consumer with no visible error.
See sample: `api-page-flowfields-must-be-calcfields.bad.al`.

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,24 @@
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.") { }
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`.
## 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`.

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,31 @@
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)
{
field(remainingHours; RemainingHoursCalc) { }
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 both the recalculated value and the source field so the consumer can verify it.
See sample: `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`.