Add 18 more community AL/BC patterns across appsource, data-modeling, error-handling, security, style, testing, ui, upgrade, and web-services

Second contribution from CURABIS ApS, generalized from patterns observed across real AppSource/PTE development. Cross-checked against the current microsoft/knowledge corpus before opening; several originally-drafted candidates were dropped as duplicates of existing files.
This commit is contained in:
Michael Dieringer 2026-09-04 20:59:54 +02:00
parent 07e324ddbc
commit a4d85c3e9e
50 changed files with 1262 additions and 0 deletions

View file

@ -0,0 +1,18 @@
codeunit 50101 "Credit Memo Routing"
{
procedure PostSalesLine(var SalesHeader: Record "Sales Header"; var SalesLine: Record "Sales Line"; CustomerNo: Code[20]; Amount: Decimal)
begin
// Set the customer number
SalesHeader.Validate("Sell-to Customer No.", CustomerNo);
// Insert the line
SalesLine.Insert(true);
// Check if the amount is positive
if Amount > 0 then
// Post the entry
PostEntry(Amount);
end;
local procedure PostEntry(Amount: Decimal)
begin
end;
}

View file

@ -0,0 +1,20 @@
codeunit 50101 "Credit Memo Routing"
{
procedure PostSalesLine(var SalesHeader: Record "Sales Header"; var SalesLine: Record "Sales Line"; CustomerNo: Code[20]; Amount: Decimal)
begin
SalesHeader.Validate("Sell-to Customer No.", CustomerNo);
SalesLine.Insert(true);
// Negative amounts arrive from credit memos routed through this
// codeunit; PostEntry() rejects them, so they're filtered here.
if Amount < 0 then
exit;
if Amount > 0 then
PostEntry(Amount);
end;
local procedure PostEntry(Amount: Decimal)
begin
end;
}

View file

@ -0,0 +1,30 @@
---
bc-version: [all]
domain: style
keywords: [comments, verbosity, self-documenting, restate, tutorial-style]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Comments must not restate what the code already shows
## Description
A comment above nearly every statement that just narrates what the statement already says (`// Validate the customer number` above `SalesHeader.Validate("Sell-to Customer No.", CustomerNo)`) adds noise without adding information. Production AL — the Base Application, mature partner codebases — is comment-sparse by comparison: identifiers do the explaining, and a comment appears only when the code alone can't carry the reason.
A comment earns its place only when it captures something the code cannot: a non-obvious business rule, a workaround for a specific platform limitation, or a constraint that would surprise the next reader. If removing the comment would leave the reader no worse off, the comment should not have been written.
This does not override required structural documentation — feature/scenario test tags and XML-doc summaries on public library procedures remain required where they apply; those are structural markers, not narrative comments.
## Best Practice
Let the code speak for itself; reserve comments for the reason a reader could not otherwise infer.
See sample: `al-comments-must-not-restate-what-code-already-shows.good.al`.
## Anti Pattern
A comment line before every statement, repeating in English what the statement's own identifiers already say.
See sample: `al-comments-must-not-restate-what-code-already-shows.bad.al`.

View file

@ -0,0 +1,20 @@
page 50100 "Sales Line Card"
{
PageType = Card;
SourceTable = "Sales Line";
actions
{
area(Processing)
{
action(Recalculate)
{
trigger OnAction()
begin
Rec."Total Amount" := Rec.Quantity * Rec."Unit Price";
Rec.Modify();
end;
}
}
}
}

View file

@ -0,0 +1,31 @@
codeunit 50100 "Sales Line Management"
{
procedure RecalculateLine(var SalesLine: Record "Sales Line")
begin
SalesLine."Total Amount" := SalesLine.Quantity * SalesLine."Unit Price";
SalesLine.Modify();
end;
}
page 50100 "Sales Line Card"
{
PageType = Card;
SourceTable = "Sales Line";
actions
{
area(Processing)
{
action(Recalculate)
{
trigger OnAction()
begin
SalesLineMgt.RecalculateLine(Rec);
end;
}
}
}
var
SalesLineMgt: Codeunit "Sales Line Management";
}

View file

@ -0,0 +1,31 @@
---
bc-version: [all]
domain: style
keywords: [pages, business-logic, codeunit, separation-of-concerns, presentation-layer]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Keep business logic out of page objects
## Description
A page procedure that calculates a value and assigns it to a field, calls `Rec.Modify()` directly, or implements a business rule is an architecture violation even when it compiles. Pages are a presentation layer: they bind data to the UI and invoke actions. Calculations, validations, and record mutations belong in codeunits, where they can be tested, reused, and called consistently regardless of which page (or API, or batch job) triggers them. When logic lives on a page, it only applies when a user opens that specific page — the same business rule silently doesn't run through any other entry point.
A narrow set of patterns are conventional rather than violations:
- A setup page reading and writing its own singleton setup record.
- A dedicated "Run Conversion" page invoking a conversion codeunit directly.
- The standard singleton-initialization idiom on `OnOpenPage` (`if not Rec.Get() then begin Rec.Init(); Rec.Insert(); end`) used by cue/activities pages to bootstrap their own presentation-state record — this is not business logic, it is the same pattern used throughout base-app cue pages.
## Best Practice
Delegate all business operations to a codeunit: the page owns presentation, the codeunit owns logic. A calculation or validation triggered from a page action should call a codeunit procedure rather than compute the result inline.
See sample: `pages-must-not-contain-business-logic.good.al`.
## Anti Pattern
Direct calculations in a page trigger (e.g. `Rec."Total Amount" := Rec.Quantity * Rec."Unit Price"`), calls to `Rec.Modify()` from a page trigger, or business-rule validation embedded in `OnValidate`/`OnAction` instead of routed through a codeunit.
See sample: `pages-must-not-contain-business-logic.bad.al`.

View file

@ -0,0 +1,36 @@
---
bc-version: [all]
domain: style
keywords: [folder-structure, feature-organization, source-layout, maintainability]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Organize AL source by business feature, not object type
## Description
Source folders inside an AL app should group files by the business feature or module they belong to (`src/Sales/Invoice/`, `src/NoSeries/`), not by which kind of AL object they are (`src/Tables/`, `src/Pages/`, `src/Codeunits/`). Object-type folders scatter everything belonging to one feature across half a dozen directories, so a developer picking up a feature has to jump between folders that share nothing but object type to see the whole picture. Feature folders keep a table, its pages, its codeunits, and its test setup physically together.
Code genuinely shared across multiple features (utility codeunits, common interfaces, shared enums) belongs in a `Common` or `Shared` folder, not duplicated per feature and not left in a catch-all root.
## Best Practice
src/
├── NoSeries/
├── Sales/
│ ├── Invoice/
│ └── Order/
└── Common/
Each feature folder holds every object type it needs; shared code has one dedicated home.
## Anti Pattern
src/
├── Tables/
├── Pages/
└── Codeunits/
Finding everything related to one feature now requires searching multiple folders and mentally reassembling it from scattered pieces.