mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-08-06 17:36:53 +01:00
Add CMFRT best practices and anti-patterns documentation for various coding standards
This commit is contained in:
parent
cfcf16a00e
commit
906ddcfd72
21 changed files with 427 additions and 5 deletions
|
|
@ -0,0 +1,15 @@
|
|||
page 55035 "CMFRT AQ FS JJL API"
|
||||
{
|
||||
PageType = API;
|
||||
SourceTable = "Job Journal Line"; // real table exposed directly to the API
|
||||
|
||||
trigger OnInsertRecord(BelowxRec: Boolean): Boolean
|
||||
begin
|
||||
// Manual insert/exit boilerplate duplicates the framework insert
|
||||
// and couples the HTTP request to the real-table insert: no retry,
|
||||
// no error queue, one validation error fails the whole request.
|
||||
Rec.Insert(true);
|
||||
Rec.CMFRTAQFSLogIncomingRequest();
|
||||
exit(false);
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
page 55035 "CMFRT AQ FS JJL API"
|
||||
{
|
||||
PageType = API;
|
||||
SourceTable = "CMFRT AQ FS JJL Buffer"; // buffer table, not Job Journal Line
|
||||
|
||||
trigger OnInsertRecord(BelowxRec: Boolean): Boolean
|
||||
begin
|
||||
// Side effects only; framework performs the default insert.
|
||||
Rec.CMFRTAQFSLogIncomingRequest();
|
||||
end;
|
||||
}
|
||||
|
||||
codeunit 55038 "CMFRT AQ FS JJL Proc"
|
||||
{
|
||||
procedure CMFRTAQProcessPendingEntries()
|
||||
var
|
||||
JJLBuffer: Record "CMFRT AQ FS JJL Buffer";
|
||||
JJLCreator: Codeunit "CMFRT AQ FS JJL Creator";
|
||||
begin
|
||||
JJLBuffer.SetRange("CMFRT AQ Status", "CMFRT AQ Buffer Status"::"CMFRT AQ Pending");
|
||||
if JJLBuffer.FindSet(true) then
|
||||
repeat
|
||||
// Proc owns the error boundary: one bad row does not abort the batch.
|
||||
if Codeunit.Run(Codeunit::"CMFRT AQ FS JJL Creator", JJLBuffer) then
|
||||
JJLBuffer."CMFRT AQ Status" := "CMFRT AQ Buffer Status"::"CMFRT AQ Processed"
|
||||
else begin
|
||||
JJLBuffer."CMFRT AQ Status" := "CMFRT AQ Buffer Status"::"CMFRT AQ Error";
|
||||
JJLBuffer."CMFRT AQ Error Message" := CopyStr(GetLastErrorText(), 1, MaxStrLen(JJLBuffer."CMFRT AQ Error Message"));
|
||||
end;
|
||||
JJLBuffer.Modify(true);
|
||||
until JJLBuffer.Next() = 0;
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: architecture
|
||||
keywords: [api, buffer, staging, oninsertrecord, api-page, inbound, creator, proc, codeunit-run]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# CMFRT inbound API pages write to buffer tables
|
||||
|
||||
## Description
|
||||
|
||||
Inbound CMFRT API pages never source from real application tables. Each API page sources from a dedicated buffer (staging) table owned by the extension. The buffer row records the raw inbound payload plus a status field (Pending/Processing/Processed/Error) and an operation enum where applicable. A Proc codeunit picks up pending buffer rows and owns the `Codeunit.Run` error boundary; a Creator codeunit transfers buffer values into the real table via `Validate` calls. The API page's `OnInsertRecord` trigger contains no manual insert plumbing — the framework performs the default insert; the trigger only performs side effects such as request logging.
|
||||
|
||||
## Best Practice
|
||||
|
||||
For each inbound API: one buffer table (with status and error-message fields), one API page sourced from the buffer, one Proc codeunit that iterates pending rows and calls the Creator inside a `Codeunit.Run` boundary so one failing row does not abort the batch, and one Creator codeunit that fills and inserts the real record. `OnInsertRecord` bodies contain only logging or metadata capture and no `exit` statement, so the framework insert proceeds.
|
||||
|
||||
See sample: `cmfrt-buffer-table-api-pattern.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
An API page sourced directly from a real table (Sales Header, Job, Ship-to Address), or an `OnInsertRecord` trigger that calls `Rec.Insert(true)` followed by `exit(false)` to suppress the framework insert. Direct-to-real-table APIs make inbound failures atomic with the HTTP request (no retry, no error queue), and the manual insert/exit boilerplate duplicates framework behaviour while hiding the insert from other trigger logic.
|
||||
|
||||
See sample: `cmfrt-buffer-table-api-pattern.bad.al`.
|
||||
34
custom/knowledge/events/cmfrt-onbefore-do-onafter.bad.al
Normal file
34
custom/knowledge/events/cmfrt-onbefore-do-onafter.bad.al
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
codeunit 55043 "CMFRT AQ FS ProForma Meth"
|
||||
{
|
||||
procedure CMFRTAQFSResolveItemToGLAcc(var SalesLine: Record "Sales Line")
|
||||
var
|
||||
Item: Record Item;
|
||||
GeneralPostingSetup: Record "General Posting Setup";
|
||||
IsHandled: Boolean;
|
||||
begin
|
||||
OnBeforeCMFRTAQFSResolveItemToGLAcc(SalesLine, IsHandled);
|
||||
if IsHandled then
|
||||
exit;
|
||||
|
||||
// Business logic inline in the shell: this early exit
|
||||
// silently skips the OnAfter event below.
|
||||
if SalesLine."CMFRT AQ FS Item No." = '' then
|
||||
exit;
|
||||
|
||||
if not Item.Get(SalesLine."CMFRT AQ FS Item No.") then
|
||||
exit;
|
||||
// ... more inline logic ...
|
||||
|
||||
OnAfterCMFRTAQFSResolveItemToGLAcc(SalesLine);
|
||||
end;
|
||||
|
||||
[IntegrationEvent(false, false)]
|
||||
local procedure OnBeforeCMFRTAQFSResolveItemToGLAcc(var SalesLine: Record "Sales Line"; var IsHandled: Boolean)
|
||||
begin
|
||||
end;
|
||||
|
||||
[IntegrationEvent(false, false)]
|
||||
local procedure OnAfterCMFRTAQFSResolveItemToGLAcc(var SalesLine: Record "Sales Line")
|
||||
begin
|
||||
end;
|
||||
}
|
||||
38
custom/knowledge/events/cmfrt-onbefore-do-onafter.good.al
Normal file
38
custom/knowledge/events/cmfrt-onbefore-do-onafter.good.al
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
codeunit 55043 "CMFRT AQ FS ProForma Meth"
|
||||
{
|
||||
procedure CMFRTAQFSResolveItemToGLAcc(var SalesLine: Record "Sales Line")
|
||||
var
|
||||
IsHandled: Boolean;
|
||||
begin
|
||||
IsHandled := false;
|
||||
OnBeforeCMFRTAQFSResolveItemToGLAcc(SalesLine, IsHandled);
|
||||
if IsHandled then
|
||||
exit;
|
||||
|
||||
DoCMFRTAQFSResolveItemToGLAcc(SalesLine);
|
||||
|
||||
OnAfterCMFRTAQFSResolveItemToGLAcc(SalesLine);
|
||||
end;
|
||||
|
||||
local procedure DoCMFRTAQFSResolveItemToGLAcc(var SalesLine: Record "Sales Line")
|
||||
var
|
||||
Item: Record Item;
|
||||
GeneralPostingSetup: Record "General Posting Setup";
|
||||
ItemNotFoundErr: Label 'Item %1 was not found.', Comment = '%1 = Item No.';
|
||||
begin
|
||||
Item.SetLoadFields("No.", "Gen. Prod. Posting Group", Description);
|
||||
if not Item.Get(SalesLine."CMFRT AQ FS Item No.") then
|
||||
Error(ItemNotFoundErr, SalesLine."CMFRT AQ FS Item No.");
|
||||
// ... business logic only; early exits here cannot skip OnAfter ...
|
||||
end;
|
||||
|
||||
[IntegrationEvent(false, false)]
|
||||
local procedure OnBeforeCMFRTAQFSResolveItemToGLAcc(var SalesLine: Record "Sales Line"; var IsHandled: Boolean)
|
||||
begin
|
||||
end;
|
||||
|
||||
[IntegrationEvent(false, false)]
|
||||
local procedure OnAfterCMFRTAQFSResolveItemToGLAcc(var SalesLine: Record "Sales Line")
|
||||
begin
|
||||
end;
|
||||
}
|
||||
26
custom/knowledge/events/cmfrt-onbefore-do-onafter.md
Normal file
26
custom/knowledge/events/cmfrt-onbefore-do-onafter.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: events
|
||||
keywords: [on-before, on-after, do-procedure, thin-shell, meth, is-handled, extraction, entry-point]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# CMFRT OnBefore → Do → OnAfter procedure shape
|
||||
|
||||
## Description
|
||||
|
||||
Global procedures in CMFRT Meth codeunits are thin shells: the body consists of firing `OnBefore<Name>` with `var IsHandled: Boolean`, exiting if handled, calling a local `Do<Name>` procedure that holds all business logic, and firing `OnAfter<Name>`. Local variables that only serve the business logic (record buffers, labels, working values) live in the `Do` procedure, not in the shell. This keeps the event bracket structurally impossible to bypass — the `OnAfter` event cannot be skipped by an early `exit` inside business logic, because business logic lives one level down.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Write every global Meth procedure as: reset `IsHandled`, fire `OnBefore`, `if IsHandled then exit;`, call `Do<Name>(...)`, fire `OnAfter`. When review finds a global procedure whose body mixes event calls with business logic, extract the logic into `Do<Name>` and move its private variables and labels along with it.
|
||||
|
||||
See sample: `cmfrt-onbefore-do-onafter.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
A global procedure whose business logic sits inline between the `OnBefore` and `OnAfter` calls. Inline bodies grow early `exit` paths that silently skip the `OnAfter` event, and their local variables and labels accumulate at the shell level where every branch can touch them. Equally wrong: declaring the event pair but never calling the events from the procedure (dead events), which advertises an extension point that never fires.
|
||||
|
||||
See sample: `cmfrt-onbefore-do-onafter.bad.al`.
|
||||
|
|
@ -23,4 +23,6 @@ See sample: `cmfrt-onbefore-onafter-all-globals.good.al`.
|
|||
|
||||
Publishing a global procedure without both `OnBefore` and `OnAfter` integration events, or adding events only after a downstream extension explicitly requests an extension point. A global procedure with no event bracket is a black box: dependent extensions cannot inject logic around it without an AL override, which is a breaking pattern. Adding events later is itself a non-breaking change but causes unnecessary churn and review cycles.
|
||||
|
||||
The inverse is equally a violation: event declarations that nothing raises. An `[IntegrationEvent]` declared but never called from any procedure, or a wrapper procedure (for example a `[TryFunction]` insert wrapper) that no caller invokes, is dead code that advertises an extension point which never fires. Wire the event into the owning procedure or delete the declaration and its plumbing.
|
||||
|
||||
See sample: `cmfrt-onbefore-onafter-all-globals.bad.al`.
|
||||
|
|
|
|||
33
custom/knowledge/naming/cmfrt-caption-prefix.bad.al
Normal file
33
custom/knowledge/naming/cmfrt-caption-prefix.bad.al
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
table 55008 "CMFRT AQ Setup"
|
||||
{
|
||||
fields
|
||||
{
|
||||
field(55011; "CMFRT AQ FS Jnl. Template Name"; Code[10])
|
||||
{
|
||||
Caption = 'FS Journal Template Name'; // missing CMFRT AQ prefix
|
||||
DataClassification = CustomerContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
page 55020 "CMFRT AQ Setup"
|
||||
{
|
||||
layout
|
||||
{
|
||||
area(Content)
|
||||
{
|
||||
field("CMFRT AQ FS Jnl. Template Name"; Rec."CMFRT AQ FS Jnl. Template Name")
|
||||
{
|
||||
ApplicationArea = All;
|
||||
// Page-level overrides duplicate the table definition and drift apart.
|
||||
Caption = 'FS Journal Template Name';
|
||||
ToolTip = 'Specifies the Job Journal Template used for material lines created by the Field Service API.';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum 55040 "CMFRT AQ Buffer Status"
|
||||
{
|
||||
value(55000; "CMFRT AQ Pending") { Caption = 'Pending'; } // missing prefix
|
||||
}
|
||||
32
custom/knowledge/naming/cmfrt-caption-prefix.good.al
Normal file
32
custom/knowledge/naming/cmfrt-caption-prefix.good.al
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
table 55008 "CMFRT AQ Setup"
|
||||
{
|
||||
fields
|
||||
{
|
||||
field(55011; "CMFRT AQ FS Jnl. Template Name"; Code[10])
|
||||
{
|
||||
Caption = 'CMFRT AQ FS Journal Template Name';
|
||||
ToolTip = 'Specifies the Job Journal Template used for material lines created by the Field Service API.';
|
||||
DataClassification = CustomerContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
page 55020 "CMFRT AQ Setup"
|
||||
{
|
||||
layout
|
||||
{
|
||||
area(Content)
|
||||
{
|
||||
field("CMFRT AQ FS Jnl. Template Name"; Rec."CMFRT AQ FS Jnl. Template Name")
|
||||
{
|
||||
ApplicationArea = All; // Caption and ToolTip inherited from the table field
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum 55040 "CMFRT AQ Buffer Status"
|
||||
{
|
||||
value(55000; "CMFRT AQ Pending") { Caption = 'CMFRT AQ Pending'; }
|
||||
value(55001; "CMFRT AQ Processed") { Caption = 'CMFRT AQ Processed'; }
|
||||
}
|
||||
26
custom/knowledge/naming/cmfrt-caption-prefix.md
Normal file
26
custom/knowledge/naming/cmfrt-caption-prefix.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: naming
|
||||
keywords: [caption, prefix, enum-value, field-caption, tooltip, table-level, page-override, translation]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# CMFRT caption prefix and table-level captions
|
||||
|
||||
## Description
|
||||
|
||||
Captions in a CMFRT extension follow the same prefix rule as names: every field caption and every enum value caption carries the `CMFRT <ABBR>` prefix — for example `Caption = 'CMFRT AQ FS Journal Template Name'` on a field and `Caption = 'CMFRT AQ Pending'` on an enum value. Captions and tooltips are defined once, on the table field (or enum value), never overridden on pages. Pages inherit the table-level `Caption` and `ToolTip`, so the text is maintained in one place and every page showing the field stays consistent.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Set `Caption` with the full `CMFRT <ABBR>` prefix and `ToolTip` on the table field definition. On pages, declare only `ApplicationArea` for the field — no `Caption`, no `ToolTip`. Give every enum value a prefixed caption matching its prefixed value name.
|
||||
|
||||
See sample: `cmfrt-caption-prefix.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
An unprefixed caption (`Caption = 'Pending'`, `Caption = 'FS Journal Template Name'`), or a page field that repeats or overrides the table-level `Caption`/`ToolTip`. Unprefixed captions are indistinguishable from base-application text for users and translators, and duplicated page-level text drifts from the table definition the first time either copy is edited.
|
||||
|
||||
See sample: `cmfrt-caption-prefix.bad.al`.
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: naming
|
||||
keywords: [object-id, id-range, numbering, product-extension, customer-extension, range]
|
||||
keywords: [object-id, id-range, numbering, product-extension, customer-extension, range, field-id, enum-value-id]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
|
|
@ -11,7 +11,7 @@ application-area: [all]
|
|||
|
||||
## Description
|
||||
|
||||
CMFRT object IDs are partitioned into two non-overlapping numeric ranges by deployment scope. Product extensions — features delivered to all customers under the CMFRT product — use IDs from `2045081` to `2046580`. Customer-specific extensions — functionality tailored to a single customer deployment — use IDs from `55000` to `55999`. Always pick the next free ID in the correct range. A removed object's ID must never be recycled; the platform retains historical references to deleted object IDs and recycling causes silent conflicts with upgrade and telemetry systems.
|
||||
CMFRT object IDs are partitioned into two non-overlapping numeric ranges by deployment scope. Product extensions — features delivered to all customers under the CMFRT product — use IDs from `2045081` to `2046580`. Customer-specific extensions — functionality tailored to a single customer deployment — use IDs from `55000` to `55999`. The range applies to member IDs as well as object IDs: table fields and enum values declared by the extension take IDs from the same licensed range (for example `field(55011; ...)`, `value(55000; ...)`), even on tables the extension owns. Always pick the next free ID in the correct range. A removed object's ID must never be recycled; the platform retains historical references to deleted object IDs and recycling causes silent conflicts with upgrade and telemetry systems.
|
||||
|
||||
## Best Practice
|
||||
|
||||
|
|
@ -21,6 +21,6 @@ See sample: `cmfrt-object-id-ranges.good.al`.
|
|||
|
||||
## Anti Pattern
|
||||
|
||||
Assigning an ID outside both ranges, choosing a round-number ID that has no relation to the next free slot, or reusing an ID from a previously removed object. ID conflicts between extensions produce runtime application errors that are difficult to reproduce and trace, because the conflict may only manifest when both extensions are installed in the same environment.
|
||||
Assigning an ID outside both ranges, choosing a round-number ID that has no relation to the next free slot, reusing an ID from a previously removed object, or numbering table fields or enum values outside the licensed range (for example `field(10; ...)` on a customer-range table). ID conflicts between extensions produce runtime application errors that are difficult to reproduce and trace, because the conflict may only manifest when both extensions are installed in the same environment.
|
||||
|
||||
See sample: `cmfrt-object-id-ranges.bad.al`.
|
||||
|
|
|
|||
18
custom/knowledge/patterns/cmfrt-labels-local-scope.bad.al
Normal file
18
custom/knowledge/patterns/cmfrt-labels-local-scope.bad.al
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
codeunit 55026 "CMFRT AQ ServiceWarehouseMeth"
|
||||
{
|
||||
var
|
||||
// Global labels: shared between unrelated procedures, outlive their
|
||||
// callers as dead text, and %1/%2 are undocumented for translators.
|
||||
CreateTransferOrdersQst: Label 'Create 2 transfer orders for job %1?';
|
||||
TransferOrdersCreatedMsg: Label 'Transfer orders %1 and %2 created.';
|
||||
|
||||
local procedure DoCMFRTAQMakeTransferOrders(var Job: Record Job)
|
||||
var
|
||||
ConfirmMgt: Codeunit "Confirm Management";
|
||||
begin
|
||||
if not ConfirmMgt.GetResponseOrDefault(StrSubstNo(CreateTransferOrdersQst, Job."No."), true) then
|
||||
exit;
|
||||
// ... create orders ...
|
||||
Message(TransferOrdersCreatedMsg, 'T-001', 'T-002');
|
||||
end;
|
||||
}
|
||||
14
custom/knowledge/patterns/cmfrt-labels-local-scope.good.al
Normal file
14
custom/knowledge/patterns/cmfrt-labels-local-scope.good.al
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
codeunit 55026 "CMFRT AQ ServiceWarehouseMeth"
|
||||
{
|
||||
local procedure DoCMFRTAQMakeTransferOrders(var Job: Record Job)
|
||||
var
|
||||
ConfirmMgt: Codeunit "Confirm Management";
|
||||
CreateTransferOrdersQst: Label 'Create 2 transfer orders for job %1?', Comment = '%1 = Job No.';
|
||||
TransferOrdersCreatedMsg: Label 'Transfer orders %1 and %2 created.', Comment = '%1 = Transfer Order No. 1, %2 = Transfer Order No. 2';
|
||||
begin
|
||||
if not ConfirmMgt.GetResponseOrDefault(StrSubstNo(CreateTransferOrdersQst, Job."No."), true) then
|
||||
exit;
|
||||
// ... create orders ...
|
||||
Message(TransferOrdersCreatedMsg, 'T-001', 'T-002');
|
||||
end;
|
||||
}
|
||||
26
custom/knowledge/patterns/cmfrt-labels-local-scope.md
Normal file
26
custom/knowledge/patterns/cmfrt-labels-local-scope.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: patterns
|
||||
keywords: [label, local-scope, global-var, comment-attribute, placeholder, translation, strsubstno]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# CMFRT Labels are local with documented placeholders
|
||||
|
||||
## Description
|
||||
|
||||
`Label` variables in CMFRT code are declared in the `var` section of the one procedure that uses them, not in the codeunit's global `var` section. Every label whose text contains `%1`-style placeholders carries a `Comment` attribute documenting each placeholder (`Comment = '%1 = Job No.'`). Global label sections accumulate text that no procedure references anymore, and undocumented placeholders leave translators guessing what will be substituted.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Declare each label local to the procedure (usually the `Do<Name>` procedure) that passes it to `Error`, `Message`, `Confirm Management`, or `StrSubstNo`. Add `Comment` naming every placeholder. When review moves logic into a `Do` procedure, move its labels with it.
|
||||
|
||||
See sample: `cmfrt-labels-local-scope.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Labels declared in the codeunit-level `var` section, or placeholder labels without a `Comment` attribute. Global labels outlive their callers as dead text, are shared between unrelated procedures, and their missing placeholder documentation produces mistranslations that only surface in localized builds.
|
||||
|
||||
See sample: `cmfrt-labels-local-scope.bad.al`.
|
||||
9
custom/knowledge/patterns/cmfrt-maxstrlen-copystr.bad.al
Normal file
9
custom/knowledge/patterns/cmfrt-maxstrlen-copystr.bad.al
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
codeunit 55028 "CMFRT AQ FS Item Attr Push"
|
||||
{
|
||||
local procedure DoCMFRTAQInitAPILogEntry(var CMFRTAQAPILog: Record "CMFRT AQ API Log"; ErrorText: Text)
|
||||
begin
|
||||
// Literal lengths diverge from the field definition on the first schema change.
|
||||
CMFRTAQAPILog."CMFRT AQ User ID" := CopyStr(UserId(), 1, 50);
|
||||
CMFRTAQAPILog."CMFRT AQ Error Message" := CopyStr(ErrorText, 1, 250);
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
codeunit 55028 "CMFRT AQ FS Item Attr Push"
|
||||
{
|
||||
local procedure DoCMFRTAQInitAPILogEntry(var CMFRTAQAPILog: Record "CMFRT AQ API Log"; ErrorText: Text)
|
||||
begin
|
||||
CMFRTAQAPILog."CMFRT AQ User ID" := CopyStr(UserId(), 1, MaxStrLen(CMFRTAQAPILog."CMFRT AQ User ID"));
|
||||
CMFRTAQAPILog."CMFRT AQ Error Message" := CopyStr(ErrorText, 1, MaxStrLen(CMFRTAQAPILog."CMFRT AQ Error Message"));
|
||||
end;
|
||||
}
|
||||
26
custom/knowledge/patterns/cmfrt-maxstrlen-copystr.md
Normal file
26
custom/knowledge/patterns/cmfrt-maxstrlen-copystr.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: patterns
|
||||
keywords: [maxstrlen, copystr, truncation, magic-number, field-length, overflow, string]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# CMFRT use MaxStrLen in CopyStr, never a literal length
|
||||
|
||||
## Description
|
||||
|
||||
When CMFRT code truncates a text value to fit a field, the length argument of `CopyStr` must be `MaxStrLen(TargetRecord.TargetField)`, never a numeric literal. The field is the single source of truth for its own length; a hardcoded number silently diverges the moment the field is widened or the code is copied to a field of a different size, producing either needless truncation or a runtime overflow error.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Write `Rec."CMFRT AQ User ID" := CopyStr(UserId(), 1, MaxStrLen(Rec."CMFRT AQ User ID"));`. The expression stays correct through any future field-length change and documents intent: truncate to whatever fits the target.
|
||||
|
||||
See sample: `cmfrt-maxstrlen-copystr.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`CopyStr(UserId(), 1, 50)` — the literal encodes the field length at the time of writing. Widening the field leaves data truncated at the stale length; narrowing it turns the assignment into a runtime "length exceeds" error that only fires on long values in production.
|
||||
|
||||
See sample: `cmfrt-maxstrlen-copystr.bad.al`.
|
||||
14
custom/knowledge/patterns/cmfrt-validate-not-assign.bad.al
Normal file
14
custom/knowledge/patterns/cmfrt-validate-not-assign.bad.al
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
codeunit 55029 "CMFRT AQ FS Job Creator"
|
||||
{
|
||||
local procedure CMFRTAQFillJobFromBuffer(var WorkOrderBuffer: Record "CMFRT AQ FS WO Buffer"; var Job: Record Job)
|
||||
begin
|
||||
Job.Init();
|
||||
Job."No." := WorkOrderBuffer."CMFRT AQ No.";
|
||||
// Direct assignment skips OnValidate: dependent fields stay empty,
|
||||
// posting-group checks and field subscribers never run.
|
||||
Job.Description := WorkOrderBuffer."CMFRT AQ Description";
|
||||
Job."Sell-to Customer No." := WorkOrderBuffer."CMFRT AQ Sell-to Customer No.";
|
||||
Job."Ship-to Address" := WorkOrderBuffer."CMFRT AQ Ship-to Address";
|
||||
Job."Location Code" := WorkOrderBuffer."CMFRT AQ Location Code";
|
||||
end;
|
||||
}
|
||||
14
custom/knowledge/patterns/cmfrt-validate-not-assign.good.al
Normal file
14
custom/knowledge/patterns/cmfrt-validate-not-assign.good.al
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
codeunit 55029 "CMFRT AQ FS Job Creator"
|
||||
{
|
||||
local procedure CMFRTAQFillJobFromBuffer(var WorkOrderBuffer: Record "CMFRT AQ FS WO Buffer"; var Job: Record Job)
|
||||
begin
|
||||
Job.Init();
|
||||
// Primary key: direct assignment, never Validate.
|
||||
Job."No." := WorkOrderBuffer."CMFRT AQ No.";
|
||||
// Business fields: Validate so OnValidate logic runs.
|
||||
Job.Validate(Description, WorkOrderBuffer."CMFRT AQ Description");
|
||||
Job.Validate("Sell-to Customer No.", WorkOrderBuffer."CMFRT AQ Sell-to Customer No.");
|
||||
Job.Validate("Ship-to Address", WorkOrderBuffer."CMFRT AQ Ship-to Address");
|
||||
Job.Validate("Location Code", WorkOrderBuffer."CMFRT AQ Location Code");
|
||||
end;
|
||||
}
|
||||
26
custom/knowledge/patterns/cmfrt-validate-not-assign.md
Normal file
26
custom/knowledge/patterns/cmfrt-validate-not-assign.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: patterns
|
||||
keywords: [validate, assignment, onvalidate, field-population, buffer, creator, direct-assignment]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# CMFRT populate real tables via Validate, not direct assignment
|
||||
|
||||
## Description
|
||||
|
||||
When CMFRT code populates fields on a real (non-buffer) table — Job, Sales Header, Sales Line, Job Planning Line, Ship-to Address, Transfer Header and the like — each field must be set with `Record.Validate(Field, Value)` so the field's `OnValidate` trigger logic runs. Direct assignment (`Record.Field := Value`) silently skips validation, dependent-field copying, posting-group checks, and any subscriber logic attached to the field. Two exceptions apply: primary-key and document-number fields that are set as part of record identity are assigned directly (validating them can renumber or re-key the record), and buffer/staging tables are always filled by direct assignment because they carry no business logic.
|
||||
|
||||
## Best Practice
|
||||
|
||||
In creator and method codeunits that transfer buffer values into real tables, call `Validate` for every business field: `Job.Validate(Description, Buffer."CMFRT AQ Description");`. Keep primary-key fields (`Job."No." := ...`, `SalesLine."Document No." := ...`) as direct assignments, set immediately after `Init()`. Fill buffer tables by direct assignment.
|
||||
|
||||
See sample: `cmfrt-validate-not-assign.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Filling a real table field-by-field with `:=`. The record is inserted with unvalidated data: posting-group checks never run, dependent fields (ship-to copies, cost fields, status transitions) stay empty or stale, and downstream extensions subscribed to `OnValidate` never fire. The defect surfaces later as inconsistent data that is hard to trace back to the skipped trigger.
|
||||
|
||||
See sample: `cmfrt-validate-not-assign.bad.al`.
|
||||
Loading…
Add table
Add a link
Reference in a new issue