mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-08-07 09:56:52 +01:00
Add CMFRT naming conventions, object ID ranges, and patterns documentation
- Introduced guidelines for using the "CMFRT" prefix in object names, fields, and procedures to avoid naming collisions and ensure clarity. - Established rules for object ID ranges to prevent conflicts with other extensions and maintain historical integrity. - Documented best practices and anti-patterns for various coding patterns, including case statements, interface injection, label usage, and validation methods. - Implemented a standards review skill to evaluate AL source changes against CMFRT company standards, ensuring compliance with naming, permissions, and architectural patterns.
This commit is contained in:
parent
d6ac005173
commit
afb1fa2883
53 changed files with 1288 additions and 0 deletions
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`.
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
// No integration events — dependent extensions cannot intercept or react
|
||||
// to the operation without using an AL override, which is a breaking pattern.
|
||||
codeunit 2045700 "CMFRT BA Item Price Impl"
|
||||
{
|
||||
procedure CMFRTBAUpdateItemPrice(ItemNo: Code[20]; UnitPrice: Decimal)
|
||||
var
|
||||
Item: Record Item;
|
||||
begin
|
||||
Item.SetLoadFields("Unit Price");
|
||||
if Item.Get(ItemNo) then begin
|
||||
Item.Validate("Unit Price", UnitPrice);
|
||||
Item.Modify();
|
||||
end;
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
codeunit 2045700 "CMFRT BA Item Price Impl"
|
||||
{
|
||||
procedure CMFRTBAUpdateItemPrice(ItemNo: Code[20]; UnitPrice: Decimal)
|
||||
var
|
||||
Item: Record Item;
|
||||
Handled: Boolean;
|
||||
begin
|
||||
OnBeforeCMFRTBAUpdateItemPrice(ItemNo, UnitPrice, Handled);
|
||||
if Handled then
|
||||
exit;
|
||||
|
||||
Item.SetLoadFields("Unit Price");
|
||||
if Item.Get(ItemNo) then begin
|
||||
Item.Validate("Unit Price", UnitPrice);
|
||||
Item.Modify();
|
||||
end;
|
||||
|
||||
OnAfterCMFRTBAUpdateItemPrice(ItemNo, UnitPrice);
|
||||
end;
|
||||
|
||||
[IntegrationEvent(false, false)]
|
||||
local procedure OnBeforeCMFRTBAUpdateItemPrice(ItemNo: Code[20]; UnitPrice: Decimal; var Handled: Boolean)
|
||||
begin
|
||||
end;
|
||||
|
||||
[IntegrationEvent(false, false)]
|
||||
local procedure OnAfterCMFRTBAUpdateItemPrice(ItemNo: Code[20]; UnitPrice: Decimal)
|
||||
begin
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: events
|
||||
keywords: [integration-event, on-before, on-after, extensibility, event-publisher, global-procedure, handled]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# CMFRT OnBefore and OnAfter for every global procedure
|
||||
|
||||
## Description
|
||||
|
||||
Every global procedure in a CMFRT extension must be bracketed by a paired `OnBefore<ProcedureName>` and `OnAfter<ProcedureName>` integration event declared in the same object. Both events are `[IntegrationEvent(false, false)]` local procedures. The `OnBefore` event passes the procedure's key input variables and a `var Handled: Boolean` parameter. The entry procedure checks `Handled` on return from `OnBefore` and exits without executing its body if a subscriber has already handled the operation. The `OnAfter` event passes the key output variables so subscribers can react to the completed result.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Declare both events at the time the global procedure is written, not as a later addition. Place the `OnBefore` call at the top of the procedure body before any logic, and the `OnAfter` call at the bottom after the last statement. Follow the naming convention `OnBefore<ProcedureName>` and `OnAfter<ProcedureName>` exactly so event consumers can locate publishers by convention.
|
||||
|
||||
See sample: `cmfrt-onbefore-onafter-all-globals.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
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`.
|
||||
Loading…
Add table
Add a link
Reference in a new issue