mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-08-06 17:36:53 +01:00
Add P0 event and interface compatibility knowledge (#98)
* Add P0 extensibility compatibility knowledge Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 645349fd-1892-48f3-8a84-db77d6abd1c3 * Correct event compatibility guidance Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 645349fd-1892-48f3-8a84-db77d6abd1c3 --------- Co-authored-by: Jesper Schulz-Wedde <jesper.schulzwedde@microsoft.com>
This commit is contained in:
parent
078b869e33
commit
363f08f47e
19 changed files with 331 additions and 32 deletions
|
|
@ -1,21 +1,14 @@
|
|||
// Demonstration-only AL. Not compiled by CI; illustrates the article.
|
||||
// Demonstration-only AL. Version 1 exposed PostDocument(SalesHeader).
|
||||
codeunit 50251 "Param Append Bad Sample"
|
||||
{
|
||||
procedure PostDocument(var SalesHeader: Record "Sales Header"; CalledFromBatch: Boolean)
|
||||
var
|
||||
IsHandled: Boolean;
|
||||
procedure PostDocument(var SalesHeader: Record "Sales Header")
|
||||
begin
|
||||
IsHandled := false;
|
||||
// Anti-pattern: 'CalledFromBatch' was inserted before the existing
|
||||
// IsHandled parameter, shifting it and breaking the argument positions
|
||||
// every existing subscriber relied on.
|
||||
OnBeforePostDocument(SalesHeader, CalledFromBatch, IsHandled);
|
||||
if IsHandled then
|
||||
exit;
|
||||
// Existing callers cannot supply the newly required argument.
|
||||
OnBeforePostDocument(SalesHeader);
|
||||
end;
|
||||
|
||||
[IntegrationEvent(false, false)]
|
||||
local procedure OnBeforePostDocument(var SalesHeader: Record "Sales Header"; CalledFromBatch: Boolean; var IsHandled: Boolean)
|
||||
procedure OnBeforePostDocument(var SalesHeader: Record "Sales Header"; CalledFromBatch: Boolean)
|
||||
begin
|
||||
end;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Demonstration-only AL. Not compiled by CI; illustrates the article.
|
||||
// Demonstration-only AL. Version 1 had SalesHeader and IsHandled parameters.
|
||||
codeunit 50250 "Param Append Good Sample"
|
||||
{
|
||||
procedure PostDocument(var SalesHeader: Record "Sales Header"; CalledFromBatch: Boolean)
|
||||
|
|
@ -6,15 +6,24 @@ codeunit 50250 "Param Append Good Sample"
|
|||
IsHandled: Boolean;
|
||||
begin
|
||||
IsHandled := false;
|
||||
// The new 'CalledFromBatch' parameter was appended at the end of the
|
||||
// existing signature, so existing subscribers needed no re-mapping.
|
||||
OnBeforePostDocument(SalesHeader, IsHandled, CalledFromBatch);
|
||||
// Subscribers bind by name, so the new parameter can sit between the
|
||||
// existing parameters without breaking subscribers that omit it.
|
||||
OnBeforePostDocument(SalesHeader, CalledFromBatch, IsHandled);
|
||||
if IsHandled then
|
||||
exit;
|
||||
end;
|
||||
|
||||
[IntegrationEvent(false, false)]
|
||||
local procedure OnBeforePostDocument(var SalesHeader: Record "Sales Header"; var IsHandled: Boolean; CalledFromBatch: Boolean)
|
||||
local procedure OnBeforePostDocument(var SalesHeader: Record "Sales Header"; CalledFromBatch: Boolean; var IsHandled: Boolean)
|
||||
begin
|
||||
end;
|
||||
}
|
||||
|
||||
codeunit 50252 "Existing Param Subscriber"
|
||||
{
|
||||
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Param Append Good Sample", 'OnBeforePostDocument', '', false, false)]
|
||||
local procedure OnBeforePostDocument(var SalesHeader: Record "Sales Header"; var IsHandled: Boolean)
|
||||
begin
|
||||
IsHandled := SalesHeader."No." = '';
|
||||
end;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: events
|
||||
keywords: [event-parameters, signature, backward-compatibility, append, onbefore, integration-event, versioning]
|
||||
keywords: [event-parameters, signature, backward-compatibility, public-event, local-event, internal-event, appsourcecop, as0024, as0025]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Add new event parameters at the end
|
||||
# Event parameter additions depend on publisher access, not position
|
||||
|
||||
## Description
|
||||
|
||||
Adding a parameter to an existing event publisher changes its signature. Appending the new parameter at the end of the parameter list keeps the change easy to review and track: existing subscribers still bind to the leading parameters, and the diff is a single clean addition. Inserting a parameter in the middle makes diffs noisy and harder to review, and obscures the history of how the signature evolved. New parameters belong after the existing ones.
|
||||
Event subscribers bind publisher parameters by name and can omit parameters they do not use. A `local` or `internal` Business or Integration event can therefore gain a parameter at any position without breaking subscriber-only consumers; appending is not a compatibility requirement. A public event is also a public procedure that dependent extensions can raise, so adding a required parameter anywhere breaks callers under AppSourceCop AS0024.
|
||||
|
||||
## Best Practice
|
||||
|
||||
When extending an existing publisher, append the new parameter after all existing ones, including after a trailing `var IsHandled: Boolean` when present. Subscribers that already match keep working against the leading parameters, and the change stays a one-line addition that is trivial to review.
|
||||
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`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Inserting a new parameter in the middle of an existing event's signature, shifting every subsequent parameter and making the change noisy and harder to review. Detection: a changed event signature where an added parameter appears before existing parameters rather than at the tail of the list.
|
||||
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`.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
// Demonstration-only AL. Version 1 used [IntegrationEvent(true, true, false)].
|
||||
codeunit 50531 "Shipment Events Bad"
|
||||
{
|
||||
procedure NotifyShipment(ShipmentNo: Code[20])
|
||||
begin
|
||||
OnShipmentCreated(ShipmentNo);
|
||||
end;
|
||||
|
||||
// Version 2 mutates all three contract-significant arguments in place.
|
||||
[IntegrationEvent(false, false, true)]
|
||||
local procedure OnShipmentCreated(ShipmentNo: Code[20])
|
||||
begin
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
// Demonstration-only AL. The Isolated argument requires runtime 9.0 / BC20.
|
||||
codeunit 50530 "Shipment Events"
|
||||
{
|
||||
procedure NotifyShipment(ShipmentNo: Code[20])
|
||||
begin
|
||||
OnShipmentCreated(ShipmentNo);
|
||||
OnShipmentCreatedIsolated(ShipmentNo);
|
||||
end;
|
||||
|
||||
// Preserve the shipped attribute contract.
|
||||
[IntegrationEvent(true, true, false)]
|
||||
local procedure OnShipmentCreated(ShipmentNo: Code[20])
|
||||
begin
|
||||
end;
|
||||
|
||||
// Publish a new event for different isolation and sender semantics.
|
||||
[IntegrationEvent(false, false, true)]
|
||||
local procedure OnShipmentCreatedIsolated(ShipmentNo: Code[20])
|
||||
begin
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: events
|
||||
keywords: [event-attribute, includesender, globalvaraccess, isolated-event, compatibility, integration-event, business-event, appsourcecop, as0021, as0101]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Do not change shipped event attribute flags
|
||||
|
||||
## Description
|
||||
|
||||
`IncludeSender` and, on Integration events, `GlobalVarAccess` have been event-contract flags since runtime 1.0. Removing sender or global access breaks subscribers, so AppSourceCop AS0021 prevents changing those flags from `true` to `false`. On runtime 9.0 and later (Business Central 2022 release wave 1, BC20), `Isolated` also controls transaction, error, and rollback behavior; AS0101 prevents adding, removing, or changing that argument.
|
||||
|
||||
## Best Practice
|
||||
|
||||
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`.
|
||||
|
||||
## 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`.
|
||||
|
|
@ -7,20 +7,20 @@ countries: [w1]
|
|||
application-area: [all]
|
||||
---
|
||||
|
||||
# Prefer this over IncludeSender in codeunit events
|
||||
# Prefer this over IncludeSender in new codeunit events
|
||||
|
||||
## Description
|
||||
|
||||
Some publishers set `IncludeSender` to `true` on `[IntegrationEvent]` or `[BusinessEvent]` so subscribers receive the publishing object as an implicit sender parameter. From Business Central 2024 release wave 2, a codeunit can instead pass itself explicitly with the `this` keyword as a normal, strongly-typed `Sender` parameter. Explicit passing is clearer at both the publisher and the subscriber: the sender appears in the signature, it is concretely typed to the publishing codeunit, and it avoids the implicit-parameter mechanics of `IncludeSender`. Reserve `IncludeSender = true` for cases where the sender genuinely cannot be passed explicitly. This guidance applies to code targeting Business Central 2024 release wave 2 or later, where the `this` keyword is available.
|
||||
When designing a new publisher, setting `IncludeSender` to `true` on `[IntegrationEvent]` or `[BusinessEvent]` gives subscribers the publishing object as an implicit sender parameter. From Business Central 2024 release wave 2, a codeunit can instead pass itself explicitly with the `this` keyword as a normal, strongly-typed `Sender` parameter. Explicit passing makes the sender visible and typed in the signature. This is new-event design guidance only: never change `IncludeSender` on an event that has already shipped.
|
||||
|
||||
## Best Practice
|
||||
|
||||
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.
|
||||
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`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Relying on `[IntegrationEvent(true, …)]` solely to hand subscribers the publisher instance, where a codeunit could pass `this` explicitly as a typed parameter. Detection: `IncludeSender = true` on a codeunit event whose only purpose is to expose the sender, in code targeting Business Central 2024 release wave 2 or later.
|
||||
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`.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
// Demonstration-only AL. Version 1 exposed var Score as an Integer.
|
||||
codeunit 50521 "Customer Scoring Events Bad"
|
||||
{
|
||||
procedure ScoreCustomer(CustomerNo: Code[20]; ScoreText: Text)
|
||||
begin
|
||||
OnCustomerScored(CustomerNo, ScoreText);
|
||||
end;
|
||||
|
||||
// 'local' limits raising, not subscription. Renaming Score to ScoreText,
|
||||
// changing its type, and removing var all break existing subscribers.
|
||||
[IntegrationEvent(false, false)]
|
||||
local procedure OnCustomerScored(CustomerNo: Code[20]; ScoreText: Text)
|
||||
begin
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
// Demonstration-only AL. Version 1 had CustomerNo and var Score parameters.
|
||||
codeunit 50520 "Customer Scoring Events"
|
||||
{
|
||||
procedure ScoreCustomer(CustomerNo: Code[20]; Reason: Text; var Score: Integer)
|
||||
begin
|
||||
OnCustomerScored(CustomerNo, Reason, Score);
|
||||
end;
|
||||
|
||||
// Adding Reason between existing parameters preserves subscriber bindings.
|
||||
[IntegrationEvent(false, false)]
|
||||
local procedure OnCustomerScored(CustomerNo: Code[20]; Reason: Text; var Score: Integer)
|
||||
begin
|
||||
end;
|
||||
}
|
||||
|
||||
codeunit 50522 "Existing Scoring Subscriber"
|
||||
{
|
||||
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Customer Scoring Events", 'OnCustomerScored', '', false, false)]
|
||||
local procedure OnCustomerScored(CustomerNo: Code[20]; var Score: Integer)
|
||||
begin
|
||||
if CustomerNo = '' then
|
||||
Score := 0;
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: events
|
||||
keywords: [local-event, internal-event, event-subscriber, compatibility, access-modifier, integration-event, business-event, parameter-name, var-parameter, appsourcecop]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Treat local and internal events as subscriber contracts
|
||||
|
||||
## Description
|
||||
|
||||
The `local` and `internal` access modifiers on Business and Integration event publishers restrict who can raise the procedure; they do not prevent dependent extensions from subscribing. Once shipped, the event name and each existing parameter's name, type/subtype, and value-versus-`var` passing mode are compatibility contracts even when the publisher is not public. Parameter order is not a subscriber contract because subscribers bind the parameters they use by name. This differs from `[InternalEvent]`, which is module-only except for modules named by `internalsVisibleTo`.
|
||||
|
||||
## Best Practice
|
||||
|
||||
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`.
|
||||
|
||||
## 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`.
|
||||
Loading…
Add table
Add a link
Reference in a new issue