knowledge(upgrade): upgrade code must not use ChangeCompany

Addresses ADO bug 651092. Upgrade and feature data update code must run in the
context of the company being upgraded; ChangeCompany leaves triggers, events and
upgrade tags in the calling company and races the target company's own upgrade.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Jesper Schulz-Wedde 2026-10-02 10:11:49 +02:00
parent 0867171b1a
commit 4e33ca87e2
5 changed files with 96 additions and 2 deletions

View file

@ -0,0 +1,24 @@
codeunit 50261 "Upgrade All Companies"
{
Subtype = Upgrade;
trigger OnUpgradePerDatabase()
var
Company: Record Company;
SalesOrderExt: Record "Sales Order Ext";
begin
// Reaches into every company from one session. Each company also has its own
// upgrade session, triggers and subscribers run in the calling context, and a
// data error in any company aborts the whole upgrade.
if Company.FindSet() then
repeat
SalesOrderExt.ChangeCompany(Company.Name);
SalesOrderExt.SetRange("Shipping Agent Code", '');
if SalesOrderExt.FindSet(true) then
repeat
SalesOrderExt.Validate("Shipping Agent Code", SalesOrderExt."Legacy Carrier Code");
SalesOrderExt.Modify(true);
until SalesOrderExt.Next() = 0;
until Company.Next() = 0;
end;
}

View file

@ -0,0 +1,34 @@
codeunit 50260 "Upgrade Current Company"
{
Subtype = Upgrade;
// The platform runs this trigger once per company, in that company's own session.
trigger OnUpgradePerCompany()
begin
UpgradeShippingAgentCodes();
end;
local procedure UpgradeShippingAgentCodes()
var
SalesOrderExt: Record "Sales Order Ext";
UpgradeTag: Codeunit "Upgrade Tag";
begin
if UpgradeTag.HasUpgradeTag(ShippingAgentUpgradeTag()) then
exit;
// Only the current company's rows; triggers, events, and the tag all apply here.
SalesOrderExt.SetRange("Shipping Agent Code", '');
if SalesOrderExt.FindSet(true) then
repeat
SalesOrderExt.Validate("Shipping Agent Code", SalesOrderExt."Legacy Carrier Code");
SalesOrderExt.Modify(true);
until SalesOrderExt.Next() = 0;
UpgradeTag.SetUpgradeTag(ShippingAgentUpgradeTag());
end;
local procedure ShippingAgentUpgradeTag(): Code[250]
begin
exit('CONTOSO-1001-ShippingAgentCode-20260101');
end;
}

View file

@ -0,0 +1,35 @@
---
bc-version: [all]
domain: upgrade
keywords: [changecompany, cross-company, onupgradepercompany, onupgradeperdatabase, feature-data-update, taskscheduler, multi-company, company-context]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Upgrade code must not use ChangeCompany
## Description
The platform upgrades each company in its own context: `OnUpgradePerCompany` (like the other `PerCompany` upgrade and install triggers) runs once per company, in a separate system session opened for that company, while `PerDatabase` triggers run in a session that opens no company. Calling `ChangeCompany(<name>)` in upgrade code reaches from the company being upgraded into another company. That company has its own upgrade session, so the work is either duplicated or racing it. The platform does not document an order between companies, so a read can see data in the pre-upgrade or post-upgrade shape. Per-company upgrade tags are set for the current company only, so a tag set after writing to company B is recorded for company A, and B's own session runs the migration again. Execution context does not move with `ChangeCompany`: table triggers and trigger-event subscribers still run in the calling company (see `changecompany-runs-triggers-in-the-calling-company`). Data and side effects then land in different companies, and an error in another company's data aborts the current company's upgrade. The same applies to feature-switch data updates, the `UpdateData` implementation of the `Feature Data Update` interface. Feature Management schedules that update as one task per company.
## Best Practice
Upgrade code operates only on the company it is running in. Put per-company data migration in `OnUpgradePerCompany` (or a helper reachable only from it), guard it with a per-company upgrade tag, and let the platform invoke it for every company. Reserve `OnUpgradePerDatabase` for tables with `DataPerCompany = false` and other database-wide state; it needs no `ChangeCompany`. A feature data update implements `UpdateData` against the current company only. When code outside the upgrade pipeline must start work in other companies, schedule it in each company with `TaskScheduler.CreateTask` and the company name, as Feature Management does, rather than writing there through `ChangeCompany`. The scheduled task runs in the target company, so triggers, events, permissions, and upgrade tags all use that company.
See sample: [`no-changecompany-in-upgrade.good.al`](no-changecompany-in-upgrade.good.al).
## Anti Pattern
A loop over the `Company` table in `OnUpgradePerDatabase` or `OnUpgradePerCompany` that calls `ChangeCompany(Company.Name)` on a record or `RecordRef` and then reads, inserts, modifies, or deletes data. Another form is a `Feature Data Update` implementation that does the same to update every company from one task. Reads are not exempt: they observe another company whose upgrade state is unknown.
Detection signal: `Record.ChangeCompany(<name>)` or `RecordRef.ChangeCompany(<name>)` with a company-name argument in a codeunit with `Subtype = Upgrade`, in any procedure transitively reachable from its upgrade triggers, or in a codeunit implementing the `Feature Data Update` interface. Do not flag the parameterless `ChangeCompany()`, which only points the variable back at the current company, or `ChangeCompany` in ordinary runtime code; `changecompany-runs-triggers-in-the-calling-company` governs that.
See sample: [`no-changecompany-in-upgrade.bad.al`](no-changecompany-in-upgrade.bad.al).
## References
- Upgrading extensions, Upgrade triggers: `PerCompany` triggers run once per company, each in its own system session for that company — https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-upgrading-extensions
- Record.ChangeCompany method, Remarks: triggers still run in the current company — https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/methods-auto/record/record-changecompany-method
- Interface "Feature Data Update" — https://learn.microsoft.com/en-us/dynamics365/business-central/application/system-application/interface/system.environment.configuration.feature-data-update
- `FeatureManagementImpl.CreateTask` schedules `Update Feature Data` with `TaskScheduler.CreateTask` for the status row's company — https://github.com/microsoft/BCApps/blob/main/src/System%20Application/App/Feature%20Key/src/FeatureManagementImpl.Codeunit.al