Extract 55 knowledge articles from BC review-agent prompt

Adds 55 articles (plus 76 code samples) spanning four new domains and
two existing domains, extracted from the internal Business Central
review-agent prompt. Content was filtered against BCQuality's
remedial-knowledge premise: each article encodes BC-specific behaviour,
a CodeCop rule, a platform API semantic, or an anti-false-positive
guideline that a capable LLM would otherwise get wrong.

New domains:
- privacy (11 articles): DataClassification inheritance semantics, the
  StrSubstNo-defeats-Error-telemetry-classification pitfall, Privacy
  Notice consent for outgoing requests, anti-false-positives for pages
  and in-memory data.
- upgrade (11 articles): upgrade-codeunit structure, upgrade-tag
  lifecycle and registration, protected DB reads, DataTransfer for
  large datasets, InitValue semantics, enum-ordinal preservation,
  obsolete-workflow, first-install detection.
- ui (9 articles): caption capitalization by phrase type, tooltip voice,
  teaching-tip vs tooltip, tour-tip conventions, character limits,
  banned terms, ampersand handling, title punctuation.
- style (11 articles): label-suffix convention, API page naming,
  temporary-variable prefix, label properties (Comment/Locked), named
  invocations, FieldCaption in user messages, OptionCaption pairing,
  Error-parameter passing, `this` keyword, required parentheses, file
  naming.

Gaps in existing domains:
- performance (11 articles): production-scale table catalog (no row
  counts, per internal-data concern), anti-false-positive for bounded
  tables, guard-before-Get ordering, redundant-Get-in-OnAfterGetRecord,
  LockTable in read-only helpers, combined ModifyAll passes, writes in
  OnAfterGetRecord, SetLoadFields heuristics, temporary-table
  regressions, FlowField source-table widening, MaintainSQLIndex
  disabling SIFT.
- security (2 articles): environment-specific hardcoded GUIDs,
  ValidateTableRelation=false on user input.

Intentionally excluded: specific production P95 row-count numbers
(aggregated internal telemetry); rewritten as categorical guidance on
which tables to treat as production-scale without publishing sizes.

All articles use `bc-version: [all]` (applies to every BC version, per
the new schema sentinel). Validator passes with 0 errors / 0 warnings.
This commit is contained in:
Jesper Schulz-Wedde 2026-04-23 16:43:42 +02:00
parent 9a4198eb28
commit e570d6113f
131 changed files with 2799 additions and 0 deletions

View file

@ -0,0 +1,17 @@
codeunit 51101 "Style Sample LabelSuffix Bad"
{
procedure Example()
var
CannotDeleteLine: Label 'Cannot delete this line.';
Text000: Label 'Update complete';
UpdateLocation: Label 'Update location?';
WrongSuffixTok: Label 'Customer %1 not found.', Comment = '%1 = Customer No.';
CustomerNo: Code[20];
begin
Error(CannotDeleteLine);
Message(Text000);
if Confirm(UpdateLocation) then
;
Error(WrongSuffixTok, CustomerNo);
end;
}

View file

@ -0,0 +1,20 @@
codeunit 51100 "Style Sample LabelSuffix Good"
{
procedure Example()
var
UpdateCompleteMsg: Label 'Update complete.';
CannotDeleteLineErr: Label 'Cannot delete this line.';
UpdateLocationQst: Label 'Update location?';
CustomerNameLbl: Label 'Customer Name';
HttpsMethodTok: Label 'GET', Locked = true;
TelemetryCustomerUpdatedTxt: Label 'Customer updated.';
begin
Message(UpdateCompleteMsg);
if Confirm(UpdateLocationQst) then
;
Session.LogMessage('0001', TelemetryCustomerUpdatedTxt,
Verbosity::Normal, DataClassification::SystemMetadata,
TelemetryScope::ExtensionPublisher);
Error(CannotDeleteLineErr);
end;
}

View file

@ -0,0 +1,26 @@
---
bc-version: [all]
domain: style
keywords: [label, textconst, suffix, msg, err, qst, tok, lbl, txt, aa0074]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Suffix every Label and TextConst with its approved usage tag
## Description
CodeCop rule AA0074 requires every Label and TextConst to carry a suffix indicating how the value is consumed: `Msg` for Message calls, `Err` for Error calls, `Qst` for Confirm or StrMenu prompts, `Tok` for locked tokens (URLs, JSON keys, short literals with `Locked = true`), `Lbl` for captions and tooltips, and `Txt` for telemetry strings. The suffix is not decoration — it is how the compiler, linter, and reviewer detect misuse (a `Tok` value passed to `Error`, a `Msg` used as an error label). The cost of adopting the convention is one short suffix per declaration; the cost of ignoring it is that every reviewer has to inspect every call site to judge appropriateness.
## Best Practice
Name every Label and TextConst with one of `Msg`, `Err`, `Qst`, `Tok`, `Lbl`, or `Txt` at the end. Pick the suffix that matches the consuming call, not the look of the string. When multiple suffixes are grammatically valid (`Tok` vs `Lbl` for a short caption on a locked token) the choice is a judgment call; the violation is missing a suffix or using one inconsistent with the call site.
See sample: `apply-approved-label-suffixes.good.al`.
## Anti Pattern
`CannotDeleteLine: Label 'Cannot delete this line.';` — no suffix, used with Error. `Text000: Label 'Update complete';` — generic name with no suffix at all. `WrongSuffixTok: Label 'Customer %1 not found.'` used with Error — a Tok suffix on an error label.
See sample: `apply-approved-label-suffixes.bad.al`.

View file

@ -0,0 +1,22 @@
page 51103 "Style Sample ApiPage Bad"
{
PageType = API;
APIPublisher = 'Contoso-App'; // hyphen not allowed
APIGroup = 'app_1'; // underscore not allowed
APIVersion = 'v2'; // missing minor version
EntityName = 'customers'; // should be singular
EntitySetName = 'customer'; // should be plural
SourceTable = Customer;
// DelayedInsert omitted; composite-key inserts misbehave
layout
{
area(Content)
{
repeater(Group)
{
field(number; Rec."No.") { }
}
}
}
}

View file

@ -0,0 +1,25 @@
page 51102 "Style Sample ApiPage Good"
{
PageType = API;
APIPublisher = 'contoso';
APIGroup = 'app1';
APIVersion = 'v2.0';
EntityName = 'customer';
EntitySetName = 'customers';
SourceTable = Customer;
DelayedInsert = true;
ODataKeyFields = SystemId;
layout
{
area(Content)
{
repeater(Group)
{
field(systemId; Rec.SystemId) { }
field(number; Rec."No.") { }
field(displayName; Rec.Name) { }
}
}
}
}

View file

@ -0,0 +1,26 @@
---
bc-version: [all]
domain: style
keywords: [api-page, apiversion, entityname, entitysetname, apipublisher, apigroup, delayedinsert]
technologies: [al]
countries: [w1]
application-area: [all]
---
# API pages follow strict naming and property rules that differ from regular pages
## Description
Pages declared `PageType = API` are exposed through the OData API surface. The platform enforces a set of conventions that regular pages do not share: `APIPublisher`, `APIGroup`, `EntityName`, and `EntitySetName` must be camelCase alphanumeric only — no spaces, hyphens, or underscores. `APIVersion` must match the pattern `vX.Y` (for example `v2.0`) or the literal `beta`. `EntityName` is the singular form (`customer`); `EntitySetName` is the plural (`customers`). `DelayedInsert = true` is effectively required for the OData insert workflow to behave correctly on composite keys. These rules are platform-enforced and tooling-enforced; violations produce runtime errors or consumer-visible inconsistencies rather than soft warnings.
## Best Practice
For every API page: camelCase alphanumeric API properties; `APIVersion` as `vX.Y` or `beta`; singular `EntityName` and plural `EntitySetName`; `DelayedInsert = true`. Keep these properties together near the top of the page definition so reviewers can check the set at a glance.
See sample: `follow-api-page-naming-rules.good.al`.
## Anti Pattern
`APIPublisher = 'Contoso-App'` (hyphen rejected), `EntityName = 'customers'` and `EntitySetName = 'customer'` (swapped), `APIVersion = 'v2'` (missing minor version), `DelayedInsert` omitted. Each violation surfaces only when a consumer exercises the endpoint.
See sample: `follow-api-page-naming-rules.bad.al`.

View file

@ -0,0 +1,15 @@
codeunit 51107 "Style Sample LabelProps Bad"
{
procedure Example()
var
// Two placeholders, no Comment. The translator has to guess which
// identifier maps to %1 and which to %2.
CustomerLocationErr: Label 'Customer %1 not found in %2.';
// URL without Locked: enters the localization pipeline, may be translated.
HttpsUrlLbl: Label 'https://example.com';
CustomerNo: Code[20];
LocationCode: Code[10];
begin
Error(CustomerLocationErr, CustomerNo, LocationCode);
end;
}

View file

@ -0,0 +1,14 @@
codeunit 51106 "Style Sample LabelProps Good"
{
procedure Example()
var
CustomerNotFoundErr: Label 'Customer %1 does not exist for document %2.',
Comment = '%1 = Customer No., %2 = Document No.';
HttpsProtocolTok: Label 'HTTPS', Locked = true;
ShortDescLbl: Label 'Description text', MaxLength = 50;
CustomerNo: Code[20];
DocumentNo: Code[20];
begin
Error(CustomerNotFoundErr, CustomerNo, DocumentNo);
end;
}

View file

@ -0,0 +1,26 @@
---
bc-version: [all]
domain: style
keywords: [label, placeholder, comment, locked, maxlength, localization]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Label placeholders need a Comment; locked strings need Locked = true
## Description
AL Labels accept optional properties — `Comment`, `Locked`, `MaxLength` — that travel with the string to localization. The Comment is the translator's only signal for what `%1` and `%2` mean; without it, `'Document %1 has errors in %2.'` translates unpredictably because the translator has to guess whether %1 is a document number, document type, or document name. `Locked = true` marks a string as non-translatable — URLs, JSON keys, short command tokens — and keeps the localization pipeline from translating literals that must stay verbatim. `MaxLength` limits how much of the label survives truncation. The Comment is required whenever placeholders are not self-evident; Locked is required on any non-text value.
## Best Practice
For placeholders, write `Comment = '%1 = Customer No., %2 = Document Type'` alongside the Label. For URLs, HTTP methods, JSON keys, and similar literals, set `Locked = true` and use the `Tok` suffix (see `apply-approved-label-suffixes`). For captions with a tight visual budget, set `MaxLength` to the enforceable length. When the placeholder meaning is obvious (`'Customer %1 not found.'`) the Comment is optional.
See sample: `include-comment-on-labels-with-placeholders.good.al`.
## Anti Pattern
`CustomerLocationErr: Label 'Customer %1 not found in %2.';` with no Comment — translators will not know which identifier maps to which placeholder. `HttpsUrl: Label 'https://example.com';` with no Locked — the URL enters the localization pipeline and may be translated into a broken address.
See sample: `include-comment-on-labels-with-placeholders.bad.al`.

View file

@ -0,0 +1,22 @@
table 51113 "Style Sample Option Bad"
{
fields
{
field(1; "Entry No."; Integer) { }
field(10; Priority; Option)
{
// Four members, three captions. Critical renders with no caption.
OptionMembers = Low,Medium,High,Critical;
OptionCaption = 'Low,Medium,High';
}
field(20; Status; Option)
{
// Missing OptionCaption entirely.
OptionMembers = Open,Released,Pending;
}
}
keys
{
key(PK; "Entry No.") { Clustered = true; }
}
}

View file

@ -0,0 +1,21 @@
table 51112 "Style Sample Option Good"
{
fields
{
field(1; "Entry No."; Integer) { }
field(10; Priority; Option)
{
OptionMembers = Low,Medium,High,Critical;
OptionCaption = 'Low,Medium,High,Critical';
}
field(20; Status; Option)
{
OptionMembers = Open,Released,Pending;
OptionCaption = 'Open,Released,Pending';
}
}
keys
{
key(PK; "Entry No.") { Clustered = true; }
}
}

View file

@ -0,0 +1,26 @@
---
bc-version: [all]
domain: style
keywords: [option, optionmembers, optioncaption, aa0221, aa0223, aa0224]
technologies: [al]
countries: [w1]
application-area: [all]
---
# OptionCaption must list exactly as many captions as OptionMembers
## Description
Option fields declare their values in `OptionMembers` and their localized display text in `OptionCaption`. The two lists are positionally paired — the Nth caption maps to the Nth member — and a mismatch either in count or in intent produces a field that renders blank for some values or shows the wrong caption for others. CodeCop rules AA0221, AA0223, and AA0224 flag the variants of this mistake: missing OptionCaption entirely on non-table-sourced option fields, OptionCaption with a different element count than OptionMembers, and OptionCaption content that does not correspond to the member names.
## Best Practice
Whenever OptionMembers is declared, declare OptionCaption with the same number of entries in the same order. For table-sourced option fields, the base table's caption applies and a per-page override is usually unnecessary — the rule applies to option fields defined in pages, reports, and non-table sources.
See sample: `match-optioncaption-count-to-optionmembers.good.al`.
## Anti Pattern
`OptionMembers = Low,Medium,High,Critical;` paired with `OptionCaption = 'Low,Medium,High';` — three captions for four members. `Critical` rows render with the empty caption, or fall back to the member name, depending on where the option is displayed.
See sample: `match-optioncaption-count-to-optionmembers.bad.al`.

View file

@ -0,0 +1,22 @@
---
bc-version: [all]
domain: style
keywords: [file-name, convention, object-type, al-project]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Name AL files as `<ObjectName>.<ObjectType>.al`
## Description
Business Central AL projects follow a consistent file-naming convention: the file name is the object's name, followed by a dot, followed by the object type (`Page`, `Codeunit`, `Table`, `Report`, `Enum`, etc.), followed by `.al`. `CustomerCard.Page.al`, `PostSalesInvoice.Codeunit.al`, `SalesLine.Table.al`. The convention produces an alphabetically-ordered folder that groups all of an entity's objects (`SalesLine.Table.al`, `SalesLine.TableExt.al`, `SalesLineCard.Page.al`) next to each other, and makes navigation by file name in large repos predictable.
## Best Practice
Match the file name to the object declaration: PascalCase name, type segment, `.al`. Use `TableExt`, `PageExt`, `EnumExt` for the corresponding extension types. When multiple objects share a file (generally discouraged), name the file after the primary object.
## Anti Pattern
`customer_page.al`, `PostSalesInvoiceLogic.al`, `tests_noSeries.al` — all three violate the convention. The first uses snake_case, the second adds a descriptive suffix after the object name, the third prefixes the type instead of suffixing it. Tooling that expects the convention (AL-Go scaffolding, navigation helpers, diff conventions) then misbehaves on these files.

View file

@ -0,0 +1,13 @@
codeunit 51115 "Style Sample ErrorParams Bad"
{
procedure Fail(CustomerNo: Code[20])
var
CustomerNotFoundErr: Label 'Customer %1 does not exist.', Comment = '%1 = Customer No.';
begin
// Pre-built Text to Error: translation skipped, telemetry opaque.
Error(StrSubstNo(CustomerNotFoundErr, CustomerNo));
// Concatenation: translation skipped, hard-coded delimiters baked in.
Error('Customer ' + CustomerNo + ' not found');
end;
}

View file

@ -0,0 +1,11 @@
codeunit 51114 "Style Sample ErrorParams Good"
{
procedure Fail(CustomerNo: Code[20]; DocumentNo: Code[20])
var
CustomerNotFoundErr: Label 'Customer %1 does not exist for document %2.',
Comment = '%1 = Customer No., %2 = Document No.';
begin
// Label + arguments passed directly. Translations apply; telemetry classifies per field.
Error(CustomerNotFoundErr, CustomerNo, DocumentNo);
end;
}

View file

@ -0,0 +1,26 @@
---
bc-version: [all]
domain: style
keywords: [error, label, strsubstno, concatenation, telemetry, aa0216, aa0217]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Pass Error parameters directly to the Label; do not pre-build with StrSubstNo or concatenation
## Description
`Error` accepts a Label and its substitution parameters directly (`Error(CustomerNotFoundErr, CustomerNo, DocumentNo)`). Pre-building the message via `StrSubstNo` and passing the resulting Text, or concatenating parts with `+` and passing the result, compiles but produces two distinct regressions. The localization pipeline can only translate the Label; a pre-built Text is passed through untouched, so non-English users see the English template. Platform telemetry inspects the Label's placeholder arguments for DataClassification; a pre-built Text is opaque, so PII in the arguments is logged verbatim (see `strsubstno-prebuild-breaks-error-telemetry-classification` in the privacy domain).
## Best Practice
Declare the Label with placeholders and pass arguments directly to Error: `Error(CustomerNotFoundErr, CustomerNo, DocumentNo)`. Use `Comment` on the Label to document each placeholder (see `include-comment-on-labels-with-placeholders`). `Error('')` is acceptable when the caller is responsible for the surfaced error.
See sample: `pass-parameters-directly-to-error-no-strsubstno.good.al`.
## Anti Pattern
`Error(StrSubstNo(CustomerNotFoundErr, CustomerNo))` — loses translation. `Error(CustomerNotFoundErr + ': ' + CustomerNo)` — loses translation, concatenates hard-coded delimiters. `Error('Customer ' + CustomerNo + ' not found')` — uses no Label at all.
See sample: `pass-parameters-directly-to-error-no-strsubstno.bad.al`.

View file

@ -0,0 +1,17 @@
codeunit 51105 "Style Sample TempPrefix Bad"
{
procedure BuildWorkingSet()
var
WIPBuffer: Record "Job WIP Buffer" temporary;
Customer: Record Customer;
begin
// Call sites read as persistent. A reviewer cannot tell at a glance
// whether DeleteAll hits the database or the in-memory buffer.
WIPBuffer.DeleteAll();
if Customer.FindSet() then
repeat
WIPBuffer.Init();
WIPBuffer.Insert();
until Customer.Next() = 0;
end;
}

View file

@ -0,0 +1,16 @@
codeunit 51104 "Style Sample TempPrefix Good"
{
procedure BuildWorkingSet()
var
TempJobWIPBuffer: Record "Job WIP Buffer" temporary;
Customer: Record Customer;
begin
// Every read site shows whether the variable is temporary.
TempJobWIPBuffer.DeleteAll();
if Customer.FindSet() then
repeat
TempJobWIPBuffer.Init();
TempJobWIPBuffer.Insert();
until Customer.Next() = 0;
end;
}

View file

@ -0,0 +1,26 @@
---
bc-version: [all]
domain: style
keywords: [temporary, record, variable, prefix, naming, temp]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Prefix temporary record variables with "Temp"
## Description
A `Record X temporary` variable behaves differently from a persistent Record variable of the same type: Insert/Modify/Delete mutate an in-memory buffer, not the underlying table. Code that mixes persistent and temporary variables of the same type is a recurring source of data-loss bugs — a helper that does `DeleteAll` on what the caller believed was a temporary buffer wipes the real table. The convention across Business Central is to prefix every temporary record variable with `Temp` (`TempJobWIPBuffer`, `TempSalesLine`, `TempCustomer`) so the distinction is visible at every read site, not only at the declaration.
## Best Practice
Prefix every temporary-record variable with `Temp`. The prefix goes on the variable name, not the type; the `temporary` keyword remains on the declaration. Matching the prefix against the declaration makes it a one-line check in code review: if the name starts with `Temp`, the declaration ends in `temporary`, and vice versa.
See sample: `prefix-temporary-record-variables-with-temp.good.al`.
## Anti Pattern
`WIPBuffer: Record "Job WIP Buffer" temporary` — the variable reads like a persistent record in every call site below the declaration. A reviewer scanning a mutation call (`WIPBuffer.DeleteAll()`) cannot tell from the call site whether the effect is in-memory or production.
See sample: `prefix-temporary-record-variables-with-temp.bad.al`.

View file

@ -0,0 +1,13 @@
codeunit 51119 "Style Sample Parentheses Bad"
{
procedure Example(var Customer: Record Customer)
var
TempBuffer: Record "Integer" temporary;
begin
// Parentheses omitted. The call site reads like a field access.
Customer.Init;
TempBuffer.DeleteAll;
if Customer.FindFirst then
;
end;
}

View file

@ -0,0 +1,12 @@
codeunit 51118 "Style Sample Parentheses Good"
{
procedure Example(var Customer: Record Customer)
var
TempBuffer: Record "Integer" temporary;
begin
Customer.Init();
TempBuffer.DeleteAll();
if Customer.FindFirst() then
;
end;
}

View file

@ -0,0 +1,26 @@
---
bc-version: [all]
domain: style
keywords: [parentheses, function-call, aa0008, invocation]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Every function call carries parentheses, even with no arguments
## Description
AL allows `Customer.Init`, `TempBuffer.DeleteAll`, and `Customer.FindFirst` without trailing parentheses when the method takes no parameters. CodeCop rule AA0008 requires the parentheses anyway. The reason is readability: without `()`, the reader has to know the member is a method and not a property — an ambiguity that resolves differently for the platform's own APIs (FindFirst is a method; `Name` is a field). With `()`, the call site is visibly a method invocation and a simple grep for `Init(` or `DeleteAll(` finds every usage.
## Best Practice
Always write parentheses on method calls, even when empty: `Customer.Init()`, `TempBuffer.DeleteAll()`, `if Customer.FindFirst() then`. Apply the rule to platform methods and to user-defined procedures alike.
See sample: `require-parentheses-on-function-calls.good.al`.
## Anti Pattern
`Customer.Init;`, `TempBuffer.DeleteAll;`, `if Customer.FindFirst then` — all three compile but obscure what is a call and what is a field access. The inconsistency compounds when the same codebase has both conventions.
See sample: `require-parentheses-on-function-calls.bad.al`.

View file

@ -0,0 +1,12 @@
codeunit 51111 "Style Sample FieldCaption Bad"
{
procedure Example(var SalesLine: Record "Sales Line")
var
UpdateLocationQst: Label 'Update the %1?', Comment = '%1 = field';
begin
// FieldName/TableName return English identifiers. User with a non-English
// locale sees the English "Location Code" inside an otherwise translated dialog.
if not Confirm(UpdateLocationQst, true, SalesLine.FieldName("Location Code")) then
exit;
end;
}

View file

@ -0,0 +1,13 @@
codeunit 51110 "Style Sample FieldCaption Good"
{
procedure Example(var SalesLine: Record "Sales Line")
var
UpdateLocationQst: Label 'Update the %1?', Comment = '%1 = field caption';
TableUpdatedMsg: Label 'Updated %1.', Comment = '%1 = table caption';
begin
// Captions are localized for the current user's language.
if not Confirm(UpdateLocationQst, true, SalesLine.FieldCaption("Location Code")) then
exit;
Message(TableUpdatedMsg, SalesLine.TableCaption());
end;
}

View file

@ -0,0 +1,26 @@
---
bc-version: [all]
domain: style
keywords: [fieldcaption, tablecaption, fieldname, tablename, localization, user-message]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Use FieldCaption and TableCaption in user messages, not FieldName and TableName
## Description
`FieldName` and `TableName` return the object's internal identifier in English — the name the developer typed into the declaration. `FieldCaption` and `TableCaption` return the translated caption for the current user's language. In user-facing messages, errors, confirmations, and notifications, the two pairs diverge the moment the user is running a non-English locale: `FieldName("Location Code")` reads `Location Code` in every language, while `FieldCaption("Location Code")` reads the translated equivalent. Using the wrong one leaks the English identifier into a localized UI and defeats the product's translation work.
## Best Practice
In any string the user will read, use `FieldCaption(<field>)` and `TableCaption`. Reserve `FieldName` and `TableName` for diagnostic and telemetry contexts where the stable English identifier is preferable. The same rule applies to `XmlPort`, `Query`, and other objects with a caption/name pair.
See sample: `use-fieldcaption-and-tablecaption-in-user-messages.good.al`.
## Anti Pattern
`Confirm(UpdateLocationQst, true, FieldName("Location Code"))`, `Message('Updated %1', TableName())` — both surface English identifiers to a user whose entire UI is in a different language.
See sample: `use-fieldcaption-and-tablecaption-in-user-messages.bad.al`.

View file

@ -0,0 +1,10 @@
codeunit 51109 "Style Sample NamedInvoke Bad"
{
procedure Example(var SalesShptLine: Record "Sales Shipment Line")
begin
// Numeric ID. The reader has to look up 525 and 206 to know what is called.
// If either object is renumbered in a future release, this call silently retargets.
Page.RunModal(525, SalesShptLine);
Report.Run(206, true);
end;
}

View file

@ -0,0 +1,9 @@
codeunit 51108 "Style Sample NamedInvoke Good"
{
procedure Example(var SalesShptLine: Record "Sales Shipment Line")
begin
// Named invocation: reviewer sees the object, rename of 525 cannot retarget.
Page.RunModal(Page::"Posted Sales Shipment Lines", SalesShptLine);
Report.Run(Report::"Sales - Invoice", true);
end;
}

View file

@ -0,0 +1,26 @@
---
bc-version: [all]
domain: style
keywords: [object-id, page-run, report-run, codeunit-run, named-invocation]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Invoke objects by name, not by numeric ID
## Description
AL supports calling `Page.RunModal(525, ...)` or `Report.Run(206, ...)` with a bare numeric ID. The platform accepts the number, but the call site loses every signal that makes the code reviewable and refactor-safe: the reader cannot tell which object is being invoked without looking up 525 in the object catalog, and the renumbering of an object in a future release (legal in AL — IDs are not a stable contract) silently retargets the call to a different object. The `Page::"..."` / `Report::"..."` syntax compiles to the same runtime call but makes the target explicit and binds by name, which is the stable identity.
## Best Practice
Write `Page.RunModal(Page::"Posted Sales Shipment Lines", SalesShptLine)` and `Report.Run(Report::"Sales - Invoice", true)`. Apply the same rule to `Codeunit.Run`, `XmlPort.Run`, and similar runtime invocations. Reserve numeric IDs for diagnostic tooling that genuinely needs them.
See sample: `use-named-invocations-instead-of-object-ids.good.al`.
## Anti Pattern
`Page.RunModal(525, SalesShptLine);` — the reader has no idea what page 525 is without a lookup, and a future rename of page 525 or renumber of "Posted Sales Shipment Lines" produces a silent mismatch.
See sample: `use-named-invocations-instead-of-object-ids.bad.al`.

View file

@ -0,0 +1,15 @@
codeunit 51117 "Style Sample ThisKeyword Bad"
{
procedure ProcessRecord(var Customer: Record Customer)
begin
// Ambiguous: is ValidateCustomer a local, a global, or a method on
// another codeunit in scope?
ValidateCustomer(Customer);
// No way to pass the current codeunit without `this`.
end;
local procedure ValidateCustomer(var Customer: Record Customer)
begin
end;
}

View file

@ -0,0 +1,21 @@
codeunit 51116 "Style Sample ThisKeyword Good"
{
procedure ProcessRecord(var Customer: Record Customer)
var
Other: Codeunit "Style Sample ThisKeyword Good";
begin
// Clearly this codeunit's method.
this.ValidateCustomer(Customer);
// Only way to pass the current codeunit as an argument.
Other.DoWith(this);
end;
local procedure ValidateCustomer(var Customer: Record Customer)
begin
end;
procedure DoWith(var Helper: Codeunit "Style Sample ThisKeyword Good")
begin
end;
}

View file

@ -0,0 +1,26 @@
---
bc-version: [all]
domain: style
keywords: [this, codeunit, self-reference, aa0248, readability]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Use the `this` keyword for codeunit self-reference
## Description
CodeCop rule AA0248 recommends the `this` keyword inside codeunit procedures when referring to the codeunit's own members or passing the codeunit itself to another procedure. AL's scope resolution otherwise blurs global-variable access, local-variable access, and same-codeunit method calls into the same unqualified syntax — a reader of `ValidateCustomer(Customer)` cannot tell at the call site whether `ValidateCustomer` is a local, a global, or a method on a different codeunit in scope. `this.ValidateCustomer(Customer)` removes the ambiguity, and `OtherCodeunit.DoWork(this)` is the only way to pass the current codeunit as a parameter.
## Best Practice
In codeunits, prefix same-codeunit method calls with `this.` when the call is ambiguous or when the scope spans more than a few lines. When the current codeunit needs to be passed as an argument, write `this` — there is no alternative syntax. The rule applies to codeunits; pages, reports, and tables have their own scoping.
See sample: `use-this-keyword-in-codeunits.good.al`.
## Anti Pattern
`ValidateCustomer(Customer); SomeOtherCodeunit.DoWork(/* this codeunit? */);` — the first call has ambiguous origin, and the second cannot pass the current codeunit without `this`. The style becomes load-bearing as the codeunit grows past a few small procedures.
See sample: `use-this-keyword-in-codeunits.bad.al`.