mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-10-05 22:56:55 +01:00
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:
parent
07e324ddbc
commit
057e17c202
52 changed files with 1109 additions and 0 deletions
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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`.
|
||||
|
|
@ -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) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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`.
|
||||
|
|
@ -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") { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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`.
|
||||
Loading…
Add table
Add a link
Reference in a new issue