Add P0 extensibility compatibility knowledge

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 645349fd-1892-48f3-8a84-db77d6abd1c3
This commit is contained in:
Jesper Schulz-Wedde 2026-07-14 11:43:55 +02:00
parent 9214f73819
commit 23af51e02d
18 changed files with 307 additions and 15 deletions

View file

@ -6,13 +6,13 @@ 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.
// This local event can gain an optional trailing subscriber parameter.
OnBeforePostDocument(SalesHeader, IsHandled, CalledFromBatch);
if IsHandled then
exit;
end;
// Public events cannot use this evolution: dependent apps may raise them.
[IntegrationEvent(false, false)]
local procedure OnBeforePostDocument(var SalesHeader: Record "Sales Header"; var IsHandled: Boolean; CalledFromBatch: Boolean)
begin

View file

@ -11,16 +11,16 @@ application-area: [all]
## 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.
Adding a parameter to an existing event publisher changes its signature. For a `local` or `internal` Business or Integration event, subscribers may omit parameters, so a new parameter can be compatible when appended at the end. A public event is also a public procedure that dependent extensions can raise; adding a parameter to it is breaking and requires a new event. Existing parameters must never be renamed, removed, reordered, or have their type changed.
## 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.
When extending an existing `local` or `internal` Business or Integration event, append the new parameter after all existing ones, including after a trailing `var IsHandled: Boolean` when present. Existing subscribers can continue omitting the new trailing parameter. Create a new event instead when the publisher procedure is public.
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.
Inserting a new parameter before an existing parameter of a `local` or `internal` event, or adding any parameter to a public event. Detection: a changed event signature where a new parameter is not a compatible trailing addition.
See sample: `add-new-event-parameters-at-the-end.bad.al`.

View file

@ -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;
}

View file

@ -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, false, 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;
}

View file

@ -0,0 +1,26 @@
---
bc-version: [20..]
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`, `GlobalVarAccess`, and `Isolated` affect a subscriber contract, not just publisher implementation. Removing sender or global access breaks subscribers; changing `Isolated` changes transaction, error, and rollback behavior. AppSourceCop AS0021 prevents changing exposed sender or globals from `true` to `false`, while AS0101 prevents adding, removing, or changing `Isolated`. The three-argument event form with `Isolated` is available from runtime 9.0 (Business Central 2022 release wave 1, BC20).
## Best Practice
Keep every attribute argument exactly as shipped. If new subscribers need different sender/global exposure or isolation semantics, publish a new event with the desired flags and raise both events while the original contract is supported. 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 attribute arguments to modernize its design, remove `GlobalVarAccess`, replace `IncludeSender` with an explicit parameter, or make the event isolated. 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`.

View file

@ -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`.

View file

@ -0,0 +1,15 @@
// Demonstration-only AL. Version 1 exposed Score as an Integer named Score.
codeunit 50521 "Customer Scoring Events Bad"
{
procedure ScoreCustomer(CustomerNo: Code[20]; ScoreText: Text)
begin
OnCustomerScored(CustomerNo, ScoreText);
end;
// 'local' limits raising, not subscription. This type/name change breaks
// subscribers compiled against the shipped event.
[IntegrationEvent(false, false)]
local procedure OnCustomerScored(CustomerNo: Code[20]; ScoreText: Text)
begin
end;
}

View file

@ -0,0 +1,19 @@
// Demonstration-only AL. Version 2 keeps the shipped local event unchanged.
codeunit 50520 "Customer Scoring Events"
{
procedure ScoreCustomer(CustomerNo: Code[20]; Score: Integer; Reason: Text)
begin
OnCustomerScored(CustomerNo, Score);
OnCustomerScoredV2(CustomerNo, Score, Reason);
end;
[IntegrationEvent(false, false)]
local procedure OnCustomerScored(CustomerNo: Code[20]; Score: Integer)
begin
end;
[IntegrationEvent(false, false)]
local procedure OnCustomerScoredV2(CustomerNo: Code[20]; Score: Integer; Reason: Text)
begin
end;
}

View file

@ -0,0 +1,26 @@
---
bc-version: [all]
domain: events
keywords: [local-event, internal-event, event-subscriber, compatibility, access-modifier, integration-event, business-event, appsourcecop, as0025]
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 existing parameter names, types, order, and passing modes are compatibility contracts even when the publisher is not public. 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 existing parameters regardless of its procedure access modifier. A compatible trailing parameter may be added to a `local` or `internal` event as described by `add-new-event-parameters-at-the-end`; for an incompatible signature or a public publisher, add a new event and keep the original.
See sample: `treat-local-and-internal-events-as-subscriber-contracts.good.al`.
## Anti Pattern
Renaming, removing, reordering, or changing an existing parameter because the event publisher procedure is `local` or `internal`. AppSourceCop AS0025 checks these subscriber-breaking changes because dependent event subscribers can still bind to the event.
See sample: `treat-local-and-internal-events-as-subscriber-contracts.bad.al`.