These instructions cover how to write and review Business Central AL upgrade code following best practices for performance, reliability, and maintainability.
All upgrade codeunits must follow this exact structure:
codeunit [ID] [CodeunitName]
{
Subtype = Upgrade;
trigger OnCheckPreconditionsPerCompany()
begin
// Your code here
end;
trigger OnCheckPreconditionsPerDatabase()
begin
// Your code here
end;
trigger OnUpgradePerCompany()
begin
// Your code here
end;
trigger OnUpgradePerDatabase()
begin
// Your code here
end;
trigger OnValidateUpgradePerCompany()
begin
// Your code here
end;
trigger OnValidateUpgradePerDatabase()
begin
// Your code here
end;
}
OnValidateUpgradePerCompany()
and OnValidateUpgradePerDatabase()
triggersOnCheckPreconditionsPerCompany()
and OnCheckPreconditionsPerDatabase()
triggersOnUpgrade triggers should only contain method calls, never direct implementation:
INCORRECT Example:
trigger OnUpgradePerCompany()
begin
// Direct implementation code here - WRONG!
Customer.ModifyAll("Some Field", true);
end;
CORRECT Example:
codeunit 4123 UpgradeMyFeature
{
Subtype = Upgrade;
trigger OnUpgradePerCompany()
begin
UpgradeMyFeature();
UpgradeSecondFeature();
end;
local procedure UpgradeMyFeature()
begin
Customer.ModifyAll("Some Field", true);
// Other upgrade code here
end;
local procedure UpgradeSecondFeature()
begin
// Your upgrade implementation here
end;
}
Item.Get();
Customer.FindSet();
Vendor.FindLast();
GOOD EXAMPLE
if Item.Get() then
// CustomCode;
if Customer.FindSet() then;
if not Vendor.FindLast() then
exit;
Example:
// GOOD - Handle gracefully
if not Customer.Get(CustomerNo) then begin
// Log telemetry about missing customer
Session.LogMessage('0000ABC', 'Customer not found during upgrade', Verbosity::Warning, DataClassification::SystemMetadata);
exit; // Continue with upgrade
end;
// BAD - Blocks upgrade
Customer.Get(CustomerNo); // Will throw error if not found
Every GET, FIND, FINDSET, FINDLAST operation MUST be within IF-THEN structure:
CORRECT Examples:
if MyTable.Get(CustomerNo) then
MyTable.Modify();
if MyTable.FindSet() then
repeat
// Process records
until MyTable.Next() = 0;
if MyTable.FindLast() then
// Process record
INCORRECT Examples:
MyTable.Get(CustomerNo); // WRONG - not protected
MyTable.FindLast(); // WRONG - not protected
BAD Examples (Do Not Use):
// WRONG - Version check approach
if MyApplication.DataVersion().Major > 14 then
exit;
// WRONG - Complex version structure
if MyApplication.DataVersion().Major < 14 then
UpgradeFeatureA()
else if MyApplicationDataVersion().Major < 17 then
UpgradeFeatureB()
else
exit;
ONLY acceptable use - checking for first installation:
trigger OnInstallAppPerCompany()
var
AppInfo: ModuleInfo;
begin
NavApp.GetCurrentModuleInfo(AppInfo);
if (AppInfo.DataVersion() <> Version.Create('0.0.0.0')) then
exit;
// Insert installation code here
end;
// Alternative approach
trigger OnInstallAppPerCompany()
var
AppInfo: ModuleInfo;
begin
if AppInfo.DataVersion().Major() = 0 then
SetAllUpgradeTags();
CompanyInitialize();
end;
CORRECT Implementation:
local procedure UpgradeMyFeature()
var
UpgradeTag: Codeunit "Upgrade Tag";
begin
if UpgradeTag.HasUpgradeTag(MyUpgradeTag()) then
exit;
// Your upgrade code here
UpgradeTag.SetUpgradeTag(MyUpgradeTag());
end;
// Register PerCompany tags
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Upgrade Tag", 'OnGetPerCompanyUpgradeTags', '', false, false)]
local procedure RegisterPerCompanyTags(var PerCompanyUpgradeTags: List of [Code[250]])
begin
PerCompanyUpgradeTags.Add(MyUpgradeTag());
end;
// Register PerDatabase tags
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Upgrade Tag", 'OnGetPerDatabaseUpgradeTags', '', false, false)]
local procedure RegisterPerDatabaseTags(var PerDatabaseUpgradeTags: List of [Code[250]])
begin
PerDatabaseUpgradeTags.Add(MyUpgradeTag());
end;
FORBIDDEN during upgrade:
These operations can fail and block the upgrade process. If they succeed and the upgrade fails, it may not be possible to roll changes back.
It’s acceptable to skip code execution during upgrade using ExecutionContext:
CORRECT Example:
// Don't add report selection entries during upgrade
if GetExecutionContext() = ExecutionContext::Upgrade then
exit;
Requirements:
MUST use DataTransfer when:
MUST use ONLY for:
IMPORTANT
BAD Example (Loop/Modify - Avoid for Large Data):
local procedure UpdatePriceSourceGroupInPriceListLines()
var
PriceListLine: Record "Price List Line";
UpgradeTag: Codeunit "Upgrade Tag";
UpgradeTagDefinitions: Codeunit "Upgrade Tag Definitions";
begin
if UpgradeTag.HasUpgradeTag(UpgradeTagDefinitions.GetPriceSourceGroupUpgradeTag()) then
exit;
PriceListLine.SetRange("Source Group", "Price Source Group"::All);
if PriceListLine.FindSet(true) then
repeat
if PriceListLine."Source Type" in
["Price Source Type"::"All Jobs",
"Price Source Type"::Job,
"Price Source Type"::"Job Task"]
then
PriceListLine."Source Group" := "Price Source Group"::Job
else
case PriceListLine."Price Type" of
"Price Type"::Purchase:
PriceListLine."Source Group" := "Price Source Group"::Vendor;
"Price Type"::Sale:
PriceListLine."Source Group" := "Price Source Group"::Customer;
end;
if PriceListLine."Source Group" <> "Price Source Group"::All then
PriceListLine.Modify();
until PriceListLine.Next() = 0;
UpgradeTag.SetUpgradeTag(UpgradeTagDefinitions.GetPriceSourceGroupFixedUpgradeTag());
end;
GOOD Example (DataTransfer - Use for Large Data):
local procedure UpdatePriceSourceGroupInPriceListLines()
var
PriceListLine: Record "Price List Line";
UpgradeTag: Codeunit "Upgrade Tag";
UpgradeTagDefinitions: Codeunit "Upgrade Tag Definitions";
PriceListLineDataTransfer: DataTransfer;
begin
if UpgradeTag.HasUpgradeTag(UpgradeTagDefinitions.GetPriceSourceGroupUpgradeTag()) then
exit;
// Update Job-related records
PriceListLineDataTransfer.SetTables(Database::"Price List Line", Database::"Price List Line");
PriceListLineDataTransfer.AddSourceFilter(PriceListLine.FieldNo("Source Group"), '=%1', "Price Source Group"::All);
PriceListLineDataTransfer.AddSourceFilter(PriceListLine.FieldNo("Source Type"), '%1|%2|%3',
"Price Source Type"::"All Jobs", "Price Source Type"::Job, "Price Source Type"::"Job Task");
PriceListLineDataTransfer.AddConstantValue("Price Source Group"::Job, PriceListLine.FieldNo("Source Group"));
PriceListLineDataTransfer.CopyFields();
Clear(PriceListLineDataTransfer);
// Update Vendor-related records
PriceListLineDataTransfer.SetTables(Database::"Price List Line", Database::"Price List Line");
PriceListLineDataTransfer.AddSourceFilter(PriceListLine.FieldNo("Source Group"), '=%1', "Price Source Group"::All);
PriceListLineDataTransfer.AddSourceFilter(PriceListLine.FieldNo("Source Type"), '<>%1&<>%2&<>%3',
"Price Source Type"::"All Jobs", "Price Source Type"::Job, "Price Source Type"::"Job Task");
PriceListLineDataTransfer.AddSourceFilter(PriceListLine.FieldNo("Price Type"), '=%1', "Price Type"::Purchase);
PriceListLineDataTransfer.AddConstantValue("Price Source Group"::Vendor, PriceListLine.FieldNo("Source Group"));
PriceListLineDataTransfer.CopyFields();
Clear(PriceListLineDataTransfer);
// Update Customer-related records
PriceListLineDataTransfer.SetTables(Database::"Price List Line", Database::"Price List Line");
PriceListLineDataTransfer.AddSourceFilter(PriceListLine.FieldNo("Source Group"), '=%1', "Price Source Group"::All);
PriceListLineDataTransfer.AddSourceFilter(PriceListLine.FieldNo("Source Type"), '<>%1&<>%2&<>%3',
"Price Source Type"::"All Jobs", "Price Source Type"::Job, "Price Source Type"::"Job Task");
PriceListLineDataTransfer.AddSourceFilter(PriceListLine.FieldNo("Price Type"), '=%1', "Price Type"::Sale);
PriceListLineDataTransfer.AddConstantValue("Price Source Group"::Customer, PriceListLine.FieldNo("Source Group"));
PriceListLineDataTransfer.CopyFields();
UpgradeTag.SetUpgradeTag(UpgradeTagDefinitions.GetPriceSourceGroupFixedUpgradeTag());
end;
BAD Example (Loop/Modify - Avoid for Large Data):
ItemJournalLine.SetLoadFields("Cross-Reference No.", "Item Reference No.");
ItemJournalLine.SetFilter("Cross-Reference No.", '<>%1', '');
if ItemJournalLine.FindSet() then
repeat
ItemJournalLine."Item Reference No." := ItemJournalLine."Cross-Reference No.";
ItemJournalLine.Modify();
until ItemJournalLine.Next() = 0;
GOOD Example (DataTransfer - Use for Large Data):
ItemJournalLine.SetFilter("Item Reference No.", '<>%1', '');
if ItemJournalLine.IsEmpty() then begin
ItemJournalLineDataTransfer.SetTables(Database::"Item Journal Line", Database::"Item Journal Line");
ItemJournalLineDataTransfer.AddSourceFilter(ItemJournalLine.FieldNo("Cross-Reference No."), '<>%1', '');
ItemJournalLineDataTransfer.AddFieldValue(ItemJournalLine.FieldNo("Cross-Reference No."), ItemJournalLine.FieldNo("Item Reference No."));
ItemJournalLineDataTransfer.CopyFields();
end;
When a field is added with InitValue:
Example Field Addition:
field(100; "New Field"; Boolean)
{
DataClassification = CustomerContent;
Caption = 'New Field';
InitValue = true;
}
field(101; "New Field 2"; Integer)
{
DataClassification = CustomerContent;
Caption = 'New Field 2';
InitValue = 5;
}
Required Upgrade Code:
local procedure UpgradeMyTables()
var
BlankMyTable: Record "My Table";
UpgradeTag: Codeunit "Upgrade Tag";
UpgradeTagDefinitions: Codeunit "Upgrade Tag Definitions";
MyTableDataTransfer: DataTransfer;
begin
if UpgradeTag.HasUpgradeTag(UpgradeTagDefinitions.GetUpgradeMyTablesTag()) then
exit;
MyTableDataTransfer.SetTables(Database::"My Table", Database::"My Table");
MyTableDataTransfer.AddConstantValue(true, BlankMyTable.FieldNo("New Field"));
MyTableDataTransfer.AddConstantValue(5, BlankMyTable.FieldNo("New Field 2"));
MyTableDataTransfer.CopyFields();
UpgradeTag.SetUpgradeTag(UpgradeTagDefinitions.GetUpgradeMyTablesTag());
end;
When reviewing upgrade code, verify:
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.