diff --git a/community/knowledge/events/avoid-raising-events-inside-try-functions.bad.al b/community/knowledge/events/avoid-raising-events-inside-try-functions.bad.al new file mode 100644 index 0000000..4dfce97 --- /dev/null +++ b/community/knowledge/events/avoid-raising-events-inside-try-functions.bad.al @@ -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; +} diff --git a/community/knowledge/events/avoid-raising-events-inside-try-functions.good.al b/community/knowledge/events/avoid-raising-events-inside-try-functions.good.al new file mode 100644 index 0000000..7c76303 --- /dev/null +++ b/community/knowledge/events/avoid-raising-events-inside-try-functions.good.al @@ -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; +} diff --git a/community/knowledge/events/avoid-raising-events-inside-try-functions.md b/community/knowledge/events/avoid-raising-events-inside-try-functions.md new file mode 100644 index 0000000..7e791fe --- /dev/null +++ b/community/knowledge/events/avoid-raising-events-inside-try-functions.md @@ -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`. diff --git a/community/knowledge/events/integration-event-parameter-is-a-breaking-change.bad.al b/community/knowledge/events/integration-event-parameter-is-a-breaking-change.bad.al new file mode 100644 index 0000000..92c72e1 --- /dev/null +++ b/community/knowledge/events/integration-event-parameter-is-a-breaking-change.bad.al @@ -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; +} diff --git a/community/knowledge/events/integration-event-parameter-is-a-breaking-change.good.al b/community/knowledge/events/integration-event-parameter-is-a-breaking-change.good.al new file mode 100644 index 0000000..c8c8a35 --- /dev/null +++ b/community/knowledge/events/integration-event-parameter-is-a-breaking-change.good.al @@ -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; +} diff --git a/community/knowledge/events/integration-event-parameter-is-a-breaking-change.md b/community/knowledge/events/integration-event-parameter-is-a-breaking-change.md new file mode 100644 index 0000000..1d64783 --- /dev/null +++ b/community/knowledge/events/integration-event-parameter-is-a-breaking-change.md @@ -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`.