mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-10-05 06:36:55 +01:00
Improve partner onboarding and documentation navigation (#174)
Lead with a complete plugin quick start and add task-oriented usage, troubleshooting, customization, and contribution guides. Preserve the broader plugin framing, correct conflicting contract guidance, support Agents folder reviews, and align repository validation. Convert existing sample references to clickable links without changing knowledge rules. Co-authored-by: Jesper Schulz-Wedde <jesper.schulzwedde@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
a21edfec46
commit
2b5550c346
276 changed files with 1287 additions and 756 deletions
|
|
@ -17,10 +17,10 @@ Event subscribers bind publisher parameters by name and can omit parameters they
|
|||
|
||||
Add a parameter directly only when the shipped event publisher is `local` or `internal`. Place it where the signature is clearest; existing subscribers continue binding the parameters they name. For a public event, keep the original publisher unchanged and introduce a new event with the expanded contract.
|
||||
|
||||
See sample: `add-new-event-parameters-at-the-end.good.al`.
|
||||
See sample: [`add-new-event-parameters-at-the-end.good.al`](add-new-event-parameters-at-the-end.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Appending a parameter to a public event and assuming its position makes the change compatible. Existing external callers still lack the new required argument. Conversely, do not flag a parameter inserted among existing parameters on a `local` or `internal` Business or Integration event merely because it was not appended.
|
||||
|
||||
See sample: `add-new-event-parameters-at-the-end.bad.al`.
|
||||
See sample: [`add-new-event-parameters-at-the-end.bad.al`](add-new-event-parameters-at-the-end.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ Passing `RecordRef` or `xRec` as event parameters weakens the contract. A `Recor
|
|||
|
||||
Give events concrete record types and explicit values, such as `(SalesLine: Record "Sales Line"; PreviousQuantity: Decimal)`, instead of a `RecordRef` or an `xRec` parameter. Subscribers then get type safety, field access, and an unambiguous contract.
|
||||
|
||||
See sample: `avoid-loosely-typed-event-parameters.good.al`.
|
||||
See sample: [`avoid-loosely-typed-event-parameters.good.al`](avoid-loosely-typed-event-parameters.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Event parameters typed as `RecordRef` (no table type) or an `xRec`-style "previous record" (ambiguous, possibly stale) without strong justification. Detection: an event signature containing a `RecordRef` parameter, or a passed-through `xRec` record, where a concrete typed record and explicit values would serve.
|
||||
|
||||
See sample: `avoid-loosely-typed-event-parameters.bad.al`.
|
||||
See sample: [`avoid-loosely-typed-event-parameters.bad.al`](avoid-loosely-typed-event-parameters.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ A `TryFunction` catches all errors — including errors thrown by event subscrib
|
|||
|
||||
Raise the integration event before entering the TryFunction scope. The event and its subscribers execute outside the error boundary, so subscriber errors propagate normally to the caller. Move only the operation that genuinely needs error isolation (such as an HTTP call or a posting step) inside the TryFunction.
|
||||
|
||||
See sample: `avoid-raising-events-inside-try-functions.good.al`.
|
||||
See sample: [`avoid-raising-events-inside-try-functions.good.al`](avoid-raising-events-inside-try-functions.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Raising an integration event inside a TryFunction body. Subscriber failures are caught and discarded by the TryFunction. The subscriber contract — that a subscriber can signal failure to the caller — is silently broken.
|
||||
|
||||
See sample: `avoid-raising-events-inside-try-functions.bad.al`.
|
||||
See sample: [`avoid-raising-events-inside-try-functions.bad.al`](avoid-raising-events-inside-try-functions.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ An `[EventSubscriber]` codeunit is static by default (`EventSubscriberInstance =
|
|||
|
||||
Use a static subscriber for behaviour that genuinely applies all the time. For anything scoped, mark the codeunit `EventSubscriberInstance = Manual`, call `BindSubscription(SubscriberInstance)` at the start of the scope and `UnbindSubscription(SubscriberInstance)` at the end. A manual subscriber held only in a local variable unbinds automatically when that variable leaves scope, which suits test setup/teardown; a binding you intend to outlive a single call must be unbound explicitly. Keep subscriber methods `local` per CodeCop AA0207.
|
||||
|
||||
See sample: `choose-static-vs-manual-subscribers-deliberately.good.al`.
|
||||
See sample: [`choose-static-vs-manual-subscribers-deliberately.good.al`](choose-static-vs-manual-subscribers-deliberately.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Two shapes. First, a static subscriber used for behaviour that should be scoped — an always-on side effect (sending mail, writing extra records) that now fires for every event in every session and test with no way to disable it. Second, a manual subscriber that is bound with `BindSubscription` and never unbound: when the instance is held beyond the intended scope (for example on a `SingleInstance` codeunit), the binding leaks for the whole session and later unrelated operations keep hitting it. Detection: scoped side effects on a static subscriber, or a `BindSubscription` call with no matching `UnbindSubscription` and no scope that releases the instance.
|
||||
|
||||
See sample: `choose-static-vs-manual-subscribers-deliberately.bad.al`.
|
||||
See sample: [`choose-static-vs-manual-subscribers-deliberately.bad.al`](choose-static-vs-manual-subscribers-deliberately.bad.al).
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ Give an event publisher the narrowest access modifier that still lets the code o
|
|||
|
||||
Subscribers are unaffected by any of these choices. A non-public publisher also keeps the freedom to add a parameter later, which a public publisher gives up — see `add-new-event-parameters-at-the-end`.
|
||||
|
||||
See sample: `declare-event-publishers-local-or-internal.good.al`.
|
||||
See sample: [`declare-event-publishers-local-or-internal.good.al`](declare-event-publishers-local-or-internal.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
|
|
@ -39,4 +39,4 @@ Detection: an `[IntegrationEvent]` or `[BusinessEvent]` publisher that is public
|
|||
|
||||
The mirror-image anti-pattern belongs to the reviewer, human or agent: recommending that a publisher be made public so extensions can subscribe, or reporting a `local`/`internal` publisher as unreachable dead code. Both readings mistake raising for subscribing. Neither should be raised as a finding.
|
||||
|
||||
See sample: `declare-event-publishers-local-or-internal.bad.al`.
|
||||
See sample: [`declare-event-publishers-local-or-internal.bad.al`](declare-event-publishers-local-or-internal.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ Adding a `var IsHandled: Boolean` parameter to an event that already shipped wit
|
|||
|
||||
Keep the existing event as-is and add a separate `OnBeforeX(…; var IsHandled: Boolean)` before the logic you want to make overridable. Two events with distinct, stable contracts are safer than one event whose meaning and signature were changed under its subscribers.
|
||||
|
||||
See sample: `do-not-add-ishandled-to-an-existing-event.good.al`.
|
||||
See sample: [`do-not-add-ishandled-to-an-existing-event.good.al`](do-not-add-ishandled-to-an-existing-event.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Mutating a shipped event — for example adding `var IsHandled` to `OnAfterCalculateTotal` — to retrofit override behaviour, which overloads the event's meaning and undermines existing subscribers. Detection: an `IsHandled` parameter added to a pre-existing event signature rather than introduced through a new dedicated `OnBefore` publisher.
|
||||
|
||||
See sample: `do-not-add-ishandled-to-an-existing-event.bad.al`.
|
||||
See sample: [`do-not-add-ishandled-to-an-existing-event.bad.al`](do-not-add-ishandled-to-an-existing-event.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ The IsHandled override pattern lets a subscriber skip the guarded code entirely.
|
|||
|
||||
Scope IsHandled to a safe value-calculation block and run the critical operations unconditionally afterwards; or expose a positive `OnAfter…` event for subscribers to adjust results, rather than a bypass around the commit.
|
||||
|
||||
See sample: `do-not-bypass-critical-operations-with-ishandled.good.al`.
|
||||
See sample: [`do-not-bypass-critical-operations-with-ishandled.good.al`](do-not-bypass-critical-operations-with-ishandled.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
An `OnBefore…` IsHandled guard wrapping a posting or ledger routine — `if IsHandled then exit;` around the code that creates ledger entries and updates document status — letting subscribers skip the commit. Detection: an `if IsHandled then exit;` whose skipped body performs posting, ledger writes, number-series consumption, or integrity and permission validation.
|
||||
|
||||
See sample: `do-not-bypass-critical-operations-with-ishandled.bad.al`.
|
||||
See sample: [`do-not-bypass-critical-operations-with-ishandled.bad.al`](do-not-bypass-critical-operations-with-ishandled.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ application-area: [all]
|
|||
|
||||
Keep every available attribute argument exactly as shipped. If new subscribers need different sender/global exposure, publish a new event with the desired flags. Apply the same rule to `Isolated` only on BC20 or later, where that argument exists. Raise both events while the original contract is supported, and choose preferred flags only when designing a new event.
|
||||
|
||||
See sample: `do-not-change-shipped-event-attribute-flags.good.al`.
|
||||
See sample: [`do-not-change-shipped-event-attribute-flags.good.al`](do-not-change-shipped-event-attribute-flags.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Changing a shipped event's `IncludeSender` or `GlobalVarAccess` to modernize its design, including replacing `IncludeSender` with an explicit parameter. On BC20 or later, adding, removing, or toggling `Isolated` is equally contract-significant. Even a change that leaves old subscribers compiling can alter observable execution or exposure; version the event instead.
|
||||
|
||||
See sample: `do-not-change-shipped-event-attribute-flags.bad.al`.
|
||||
See sample: [`do-not-change-shipped-event-attribute-flags.bad.al`](do-not-change-shipped-event-attribute-flags.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ Raising an event on every iteration of a loop multiplies the cost of every subsc
|
|||
|
||||
Raise `OnBeforeProcessLines` before the loop and `OnAfterProcessLines` after it, outside the `repeat … until`, so each subscriber runs once per batch rather than once per row. Give those events the record or filters they need to operate on the whole set.
|
||||
|
||||
See sample: `do-not-publish-events-inside-loops.good.al`.
|
||||
See sample: [`do-not-publish-events-inside-loops.good.al`](do-not-publish-events-inside-loops.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
An event raised inside the loop body, fired once per iteration, so subscriber cost scales with the row count and large batches slow down or time out. Detection: an `OnBefore…`/`OnAfter…`/`On…` raise located between `repeat` and `until` in a record loop.
|
||||
|
||||
See sample: `do-not-publish-events-inside-loops.bad.al`.
|
||||
See sample: [`do-not-publish-events-inside-loops.bad.al`](do-not-publish-events-inside-loops.bad.al).
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ Publish the context as a query and let the binding itself be the state. One proc
|
|||
|
||||
Bind a fresh instance per run rather than reusing one: the platform refuses to bind the same instance twice but accepts several instances of the same codeunit, so nesting and re-entrancy need no counter. The binding is session-scoped, so work the process starts in another session — a background session, a page background task, a job queue entry — cannot see it; pass the context explicitly there.
|
||||
|
||||
See sample: `expose-process-context-via-manually-bound-flag.good.al`.
|
||||
See sample: [`expose-process-context-via-manually-bound-flag.good.al`](expose-process-context-via-manually-bound-flag.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
|
|
@ -37,4 +37,4 @@ Second, the context kept private: the driving app arranges its own marker — ty
|
|||
|
||||
The mirror-image anti-pattern belongs to the reviewer: flagging the `BindSubscription` here as a leaked binding because no `UnbindSubscription` follows it. Scope release is the mechanism, not an omission — see `microsoft/knowledge/events/choose-static-vs-manual-subscribers-deliberately.md`, whose leak case is an instance parked on a `SingleInstance` global that never leaves scope.
|
||||
|
||||
See sample: `expose-process-context-via-manually-bound-flag.bad.al`.
|
||||
See sample: [`expose-process-context-via-manually-bound-flag.bad.al`](expose-process-context-via-manually-bound-flag.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ Event parameter names are part of the public contract a subscriber codes against
|
|||
|
||||
Use full, unabbreviated names: `(SalesHeader: Record "Sales Header"; DocumentNo: Code[20]; Amount: Decimal)`. Record parameters mirror the table name without spaces, and value parameters read as whole words so the contract is unambiguous.
|
||||
|
||||
See sample: `name-event-parameters-without-abbreviations.good.al`.
|
||||
See sample: [`name-event-parameters-without-abbreviations.good.al`](name-event-parameters-without-abbreviations.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Abbreviated parameter names (`SalesHdr`, `DocNo`, `Amt`) that obscure meaning and vary across publishers, so subscribers must guess what each one holds. Detection: event parameters whose names are truncated forms of the table name or contracted words rather than the full term.
|
||||
|
||||
See sample: `name-event-parameters-without-abbreviations.bad.al`.
|
||||
See sample: [`name-event-parameters-without-abbreviations.bad.al`](name-event-parameters-without-abbreviations.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ An event name should tell a subscriber where in the publisher the event fires. T
|
|||
|
||||
Name by position: `OnBeforePostSalesLine` and `OnAfterPostSalesLine` at the routine boundaries, and `OnPostSalesLineOnAfterCalcAmounts` for an event raised partway through `PostSalesLine` after an amount calculation. The name alone then tells a subscriber both the host routine and the exact point it runs.
|
||||
|
||||
See sample: `name-events-by-publisher-position.good.al`.
|
||||
See sample: [`name-events-by-publisher-position.good.al`](name-events-by-publisher-position.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Ad-hoc event names that omit the host routine or the before/after position (`MyCustomSalesEvent`, `BeforePost`, `SalesLineEvent`), leaving subscribers unable to tell when the event fires relative to the publisher's logic. Detection: publisher names that do not follow the `OnBefore`/`OnAfter<Routine>` or `On<Routine>OnBefore`/`OnAfter<Context>` patterns.
|
||||
|
||||
See sample: `name-events-by-publisher-position.bad.al`.
|
||||
See sample: [`name-events-by-publisher-position.bad.al`](name-events-by-publisher-position.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ Before adding a publisher, check whether an event already fires at that point in
|
|||
|
||||
When the data you need is already exposed at an existing event, subscribe to it. When the event lacks a parameter, extend that event by appending the parameter at the end — one publisher, one raise — rather than adding a second event beside it.
|
||||
|
||||
See sample: `prefer-reusing-or-extending-existing-events.good.al`.
|
||||
See sample: [`prefer-reusing-or-extending-existing-events.good.al`](prefer-reusing-or-extending-existing-events.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Adding a second event raise immediately after an existing one, or creating `OnBeforeProcessOrderWithCustomer` next to `OnBeforeProcessOrder` just to add a single parameter. Detection: two consecutive `OnBefore…`/`OnAfter…` raises with no logic between them, or near-duplicate event names differing only by a parameter-describing suffix.
|
||||
|
||||
See sample: `prefer-reusing-or-extending-existing-events.bad.al`.
|
||||
See sample: [`prefer-reusing-or-extending-existing-events.bad.al`](prefer-reusing-or-extending-existing-events.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ When designing a new publisher, setting `IncludeSender` to `true` on `[Integrati
|
|||
|
||||
For a new event, declare the publisher `[IntegrationEvent(false, false)]` with an explicit `Sender: Codeunit "…"` parameter and raise it with `this`, for example `OnBeforeProcessOrder(OrderNo, this);`. Subscribers then receive a typed sender they can call directly.
|
||||
|
||||
See sample: `prefer-this-over-includesender-in-codeunit-events.good.al`.
|
||||
See sample: [`prefer-this-over-includesender-in-codeunit-events.good.al`](prefer-this-over-includesender-in-codeunit-events.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Designing a new codeunit event with `[IntegrationEvent(true, …)]` solely to hand subscribers the publisher instance, where `this` could be passed explicitly as a typed parameter. Do not apply this rule by mutating a shipped event's attribute flags.
|
||||
|
||||
See sample: `prefer-this-over-includesender-in-codeunit-events.bad.al`.
|
||||
See sample: [`prefer-this-over-includesender-in-codeunit-events.bad.al`](prefer-this-over-includesender-in-codeunit-events.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ When a record passed to an event is a temporary record — an in-memory buffer n
|
|||
|
||||
Name temporary record parameters with a `Temp` prefix, for example `var TempSalesLineBuffer: Record "Sales Line" temporary`, so every subscriber sees immediately that the record is an in-memory buffer and treats writes accordingly.
|
||||
|
||||
See sample: `prefix-temporary-record-event-parameters-with-temp.good.al`.
|
||||
See sample: [`prefix-temporary-record-event-parameters-with-temp.good.al`](prefix-temporary-record-event-parameters-with-temp.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
A temporary record parameter named without the `Temp` prefix (`var SalesLineBuffer: Record "Sales Line" temporary`), so subscribers cannot tell the record is non-persistent and may rely on writes that are silently discarded. Detection: an event parameter declared `temporary` whose name does not start with `Temp`.
|
||||
|
||||
See sample: `prefix-temporary-record-event-parameters-with-temp.bad.al`.
|
||||
See sample: [`prefix-temporary-record-event-parameters-with-temp.bad.al`](prefix-temporary-record-event-parameters-with-temp.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ A routine that exposes both an `OnBefore…` event (with `var IsHandled`) and a
|
|||
|
||||
Wrap only the default work in `if not IsHandled then begin … end;` and keep the `OnAfterX(…)` raise after that block, outside the guard, so it always fires regardless of whether a subscriber handled the OnBefore. This keeps the override seam and the after-notification independent, which is what subscribers expect.
|
||||
|
||||
See sample: `preserve-onafter-execution-when-ishandled-skips-the-body.good.al`.
|
||||
See sample: [`preserve-onafter-execution-when-ishandled-skips-the-body.good.al`](preserve-onafter-execution-when-ishandled-skips-the-body.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Guarding with `if IsHandled then exit;` and placing the `OnAfterX` raise later in the same routine, so handling the OnBefore short-circuits the whole procedure and the OnAfter event is skipped along with the body. Detection: an `if IsHandled then exit;` in a routine that also raises a paired `OnAfter…` event after that point.
|
||||
|
||||
See sample: `preserve-onafter-execution-when-ishandled-skips-the-body.bad.al`.
|
||||
See sample: [`preserve-onafter-execution-when-ishandled-skips-the-body.bad.al`](preserve-onafter-execution-when-ishandled-skips-the-body.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ A key operation — a posting, release, or validation routine — becomes a hard
|
|||
|
||||
Wrap the operation's core with events: raise `OnBeforeX(var Rec, var IsHandled)` before the default work and `OnAfterX(var Rec)` once it succeeds, at the natural boundaries of the routine. Declare each publisher `[IntegrationEvent(false, false)] local procedure` with an empty body and let the calling routine — never the publisher — own the logic. Pass records by `var` so subscribers can read and adjust them, and include the parameters a subscriber would need to act. This gives partners a stable seam without touching base code.
|
||||
|
||||
See sample: `publish-thin-onbefore-onafter-integration-events.good.al`.
|
||||
See sample: [`publish-thin-onbefore-onafter-integration-events.good.al`](publish-thin-onbefore-onafter-integration-events.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Business logic placed inside an `[IntegrationEvent]` publisher method, so the "event" actually mutates state every time it is raised — defeating the hook and surprising every reader — or a core operation that exposes no extension points at all, forcing partners to overwrite or duplicate it. Detection: an `[IntegrationEvent]`/`[BusinessEvent]` method whose body contains statements rather than being empty, or a posting/validation routine with no surrounding `OnBefore`/`OnAfter` publishers.
|
||||
|
||||
See sample: `publish-thin-onbefore-onafter-integration-events.bad.al`.
|
||||
See sample: [`publish-thin-onbefore-onafter-integration-events.bad.al`](publish-thin-onbefore-onafter-integration-events.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ A routine that raises an `OnBefore…` integration event with a `var IsHandled:
|
|||
|
||||
Reset `IsHandled := false;` before a raise only when the value might otherwise carry over as `true`: the same variable is reused after an earlier raise without a control-flow proof that it is false, a raise is re-entered by a loop, the value comes from an input parameter, field, or global, or earlier code seeds it. Prefer separate fresh locals when independent event seams need independent handled state. A reset on a guaranteed-false fresh local used by one non-looping raise, or before a later raise reached only after a semantically valid `if IsHandled then exit;`, can be retained for readability, but its absence is not a correctness finding.
|
||||
|
||||
See sample: `reset-ishandled-only-when-the-value-can-carry-over.good.al`.
|
||||
See sample: [`reset-ishandled-only-when-the-value-can-carry-over.good.al`](reset-ishandled-only-when-the-value-can-carry-over.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Raising `OnBeforeX(…, IsHandled)` when the variable can still be `true` from an earlier raise, an earlier loop iteration, or another source, so the publisher call starts with stale state. Do not match a single non-looping raise using a fresh local Boolean, or a later raise reached only after a semantically valid `if IsHandled then exit;` proves the value is false.
|
||||
|
||||
See sample: `reset-ishandled-only-when-the-value-can-carry-over.bad.al`.
|
||||
See sample: [`reset-ishandled-only-when-the-value-can-carry-over.bad.al`](reset-ishandled-only-when-the-value-can-carry-over.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ The `local` and `internal` access modifiers on Business and Integration event pu
|
|||
|
||||
Preserve a shipped Business or Integration event's identity and every existing parameter's name, type/subtype, and passing mode regardless of the procedure access modifier. AS0025 protects names and types, while AS0063 and AS0077 protect removal and addition of `var`. New parameters may be added at any position on a `local` or `internal` event because subscribers can omit them; public event procedures follow the stricter caller contract described by `add-new-event-parameters-at-the-end`.
|
||||
|
||||
See sample: `treat-local-and-internal-events-as-subscriber-contracts.good.al`.
|
||||
See sample: [`treat-local-and-internal-events-as-subscriber-contracts.good.al`](treat-local-and-internal-events-as-subscriber-contracts.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Renaming or removing an existing parameter, changing its type/subtype, or adding/removing its `var` modifier because the event publisher procedure is `local` or `internal`. AppSourceCop checks these subscriber-breaking changes because dependent event subscribers can still bind to the event. Reordering unchanged parameters, or inserting a new parameter among them, is not this anti-pattern.
|
||||
|
||||
See sample: `treat-local-and-internal-events-as-subscriber-contracts.bad.al`.
|
||||
See sample: [`treat-local-and-internal-events-as-subscriber-contracts.bad.al`](treat-local-and-internal-events-as-subscriber-contracts.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ AL has no method overriding, so a `procedure` that runs its body unconditionally
|
|||
|
||||
Raise `OnBeforeX(…, IsHandled)` as the first step of the routine and guard with `if IsHandled then exit;` before any default logic runs. Declare the publisher `[IntegrationEvent(false, false)] local procedure OnBeforeX(…; var IsHandled: Boolean)` with an empty body, and keep `IsHandled` a `var` parameter so a subscriber can write to it. A subscriber that replaces the behaviour does its work and sets `IsHandled := true`; one that only augments leaves it untouched and guards with `if IsHandled then exit;` itself. Reserve the override hook for cases where a partner genuinely needs to replace logic — when the goal is only to react, a positive `OnAfter` event is the better seam.
|
||||
|
||||
See sample: `use-ishandled-to-make-base-behaviour-overridable.good.al`.
|
||||
See sample: [`use-ishandled-to-make-base-behaviour-overridable.good.al`](use-ishandled-to-make-base-behaviour-overridable.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Two shapes. First, a routine whose default logic always runs because there is no `OnBefore…`/`IsHandled` hook at all — extensions cannot change it without overwriting base code. Second, a routine that raises `OnBeforeX(IsHandled)` but omits the `if IsHandled then exit;` guard, so the default logic still executes after a subscriber set `IsHandled := true`, duplicating work and side effects. Detection: an `OnBefore` publisher with a `var IsHandled: Boolean` parameter whose caller never tests `IsHandled`, or a public routine doing non-trivial work with no overridable seam.
|
||||
|
||||
See sample: `use-ishandled-to-make-base-behaviour-overridable.bad.al`.
|
||||
See sample: [`use-ishandled-to-make-base-behaviour-overridable.bad.al`](use-ishandled-to-make-base-behaviour-overridable.bad.al).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue