Add events knowledge files for integration event patterns

This commit is contained in:
Jeffrey Bulanadi 2026-06-15 08:11:43 +08:00
parent 822cae1b27
commit 2a44a63bb0
6 changed files with 193 additions and 0 deletions

View file

@ -0,0 +1,38 @@
// Demonstration only. Shows the wrong pattern: raising the integration event inside a TryFunction body.
codeunit 50116 "Payment Processor Bad"
{
[IntegrationEvent(false, false)]
procedure OnBeforeSubmitPayment(var PaymentAmount: Decimal; var Cancel: Boolean)
begin
end;
procedure SubmitPayment(PaymentAmount: Decimal)
var
Success: Boolean;
begin
// TryFunction wraps both the event raise and the gateway call.
Success := TrySubmitPaymentInternal(PaymentAmount);
if not Success then
Error('Payment gateway call failed. Check connectivity and retry.');
end;
[TryFunction]
local procedure TrySubmitPaymentInternal(PaymentAmount: Decimal)
var
Cancel: Boolean;
Client: HttpClient;
Response: HttpResponseMessage;
begin
Cancel := false;
// BAD: event raised inside TryFunction. Any Error() thrown by a subscriber is caught here
// and silently swallowed - the subscriber's error never reaches the caller.
// A subscriber setting Cancel := true is also lost when TryFunction returns false.
OnBeforeSubmitPayment(PaymentAmount, Cancel);
if Cancel then
exit;
Client.Get('https://payments.example.com/submit?amount=' + Format(PaymentAmount), Response);
if not Response.IsSuccessStatusCode() then
Error('HTTP %1', Response.HttpStatusCode());
end;
}

View file

@ -0,0 +1,38 @@
// Demonstration only. Shows the correct pattern: raise the integration event before entering TryFunction.
codeunit 50114 "Payment Processor"
{
[IntegrationEvent(false, false)]
procedure OnBeforeSubmitPayment(var PaymentAmount: Decimal; var Cancel: Boolean)
begin
end;
procedure SubmitPayment(PaymentAmount: Decimal)
var
Cancel: Boolean;
Success: Boolean;
begin
Cancel := false;
// Event raised outside the try scope - subscriber errors propagate normally to the caller.
OnBeforeSubmitPayment(PaymentAmount, Cancel);
if Cancel then
exit;
// Only the operation that can fail transiently lives inside TryFunction.
Success := TryCallPaymentGateway(PaymentAmount);
if not Success then
Error('Payment gateway call failed. Check connectivity and retry.');
end;
[TryFunction]
local procedure TryCallPaymentGateway(PaymentAmount: Decimal)
var
Client: HttpClient;
Response: HttpResponseMessage;
begin
// ... build request, set headers ...
Client.Get('https://payments.example.com/submit?amount=' + Format(PaymentAmount), Response);
if not Response.IsSuccessStatusCode() then
Error('HTTP %1', Response.HttpStatusCode());
end;
}

View file

@ -0,0 +1,26 @@
---
bc-version: [all]
domain: events
keywords: [tryfunction, integration-event, subscriber, error-handling, silent-failure, event-publisher]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Do not raise integration events inside a TryFunction
## Description
A `TryFunction` catches all errors — including errors thrown by event subscribers. When an `[IntegrationEvent]` is raised inside a `TryFunction` body, any error a subscriber raises is silently swallowed by the TryFunction's error boundary. The subscriber's logic fails, the caller sees no error, and the calling code continues as if nothing happened. Subscribers have no way to signal failure to the caller.
## Best Practice
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`.
## 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`.

View file

@ -0,0 +1,29 @@
// Demonstration only. Shows the wrong way: adding a parameter directly to an existing integration event.
codeunit 50112 "Sales Post Events Bad"
{
// BAD: ShipmentNo added directly to the existing event signature after subscribers already existed.
[IntegrationEvent(false, false)]
procedure OnAfterPostSalesOrder(var SalesHeader: Record "Sales Header"; ShipmentNo: Code[20])
begin
end;
procedure PostSalesOrder(var SalesHeader: Record "Sales Header"; ShipmentNo: Code[20])
begin
// ... posting logic ...
OnAfterPostSalesOrder(SalesHeader, ShipmentNo);
end;
}
codeunit 50113 "Existing Subscriber Bad"
{
// COMPILE ERROR (AL0306): subscriber signature no longer matches the publisher.
// "The event subscriber method signature does not match the event publisher method signature."
// ShipmentNo was not on the original event. Every extension that subscribed breaks immediately -
// no deprecation period, no warning, no grace. The parameter change is a breaking change.
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales Post Events Bad", 'OnAfterPostSalesOrder', '', false, false)]
local procedure HandleAfterPostSalesOrder(var SalesHeader: Record "Sales Header")
begin
// This procedure no longer compiles after ShipmentNo was added to the publisher.
end;
}

View file

@ -0,0 +1,36 @@
// Demonstration only. Shows the correct way to extend an integration event that already has subscribers.
codeunit 50110 "Sales Post Events"
{
// Original event kept and marked obsolete so existing subscribers continue to compile.
[Obsolete('Use OnAfterPostSalesOrderWithShipmentNo instead.', '26.0')]
[IntegrationEvent(false, false)]
procedure OnAfterPostSalesOrder(var SalesHeader: Record "Sales Header")
begin
end;
// New overload carries the extra parameter - existing subscribers on the old event still compile.
[IntegrationEvent(false, false)]
procedure OnAfterPostSalesOrderWithShipmentNo(var SalesHeader: Record "Sales Header"; ShipmentNo: Code[20])
begin
end;
procedure PostSalesOrder(var SalesHeader: Record "Sales Header"; ShipmentNo: Code[20])
begin
// ... posting logic ...
OnAfterPostSalesOrder(SalesHeader); // kept for backward compat
OnAfterPostSalesOrderWithShipmentNo(SalesHeader, ShipmentNo); // new callers migrate here
end;
}
codeunit 50111 "Shipment Notifier Subscriber"
{
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales Post Events", 'OnAfterPostSalesOrderWithShipmentNo', '', false, false)]
local procedure HandleAfterPostSalesOrderWithShipmentNo(var SalesHeader: Record "Sales Header"; ShipmentNo: Code[20])
begin
// Subscriber uses the new event; ShipmentNo is available without breaking old subscribers.
if ShipmentNo = '' then
exit;
// ... notify warehouse of shipment ...
end;
}

View file

@ -0,0 +1,26 @@
---
bc-version: [all]
domain: events
keywords: [integration-event, publisher, subscriber, breaking-change, parameter, obsolete]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Adding a parameter to an existing integration event is a breaking change
## Description
Every codeunit that subscribes to an `[IntegrationEvent]` must match the publisher's exact parameter signature. Adding a parameter to an existing event immediately breaks every subscriber — they fail to compile the moment the parameter is added to the publisher. This affects all consumers of the event, including those in other extensions the author does not control.
## Best Practice
Add a new event with the extended signature alongside the original. Keep the original event and mark it `[Obsolete(...)]` so existing subscribers continue to compile and authors have time to migrate. The new event name should reflect the addition (for example, append `WithShipmentNo` or increment a suffix). Raise both events during the transition period.
See sample: `integration-event-parameter-is-a-breaking-change.good.al`.
## Anti Pattern
Adding a new parameter directly to the existing `[IntegrationEvent]` declaration. Every subscriber codeunit, in every extension that subscribed to that event, stops compiling immediately. There is no safe rollout path once the breaking signature is published.
See sample: `integration-event-parameter-is-a-breaking-change.bad.al`.