Create proper installation and upgrade codeunits
Creating code for different situations can vary and this unit provides guidance on some of the different scenarios that you'll encounter when creating or updating an app.
Writing extension install code
There might be certain operations outside of the extension code itself that you want performed when an extension is installed. These operations could include, for example, populating empty records with data, service callbacks and telemetry, version checks, and messages to app users. To perform these types of operations, you write extension install code. Extension install code is run when:
An extension is installed for the very first time.
An uninstalled version is installed again.
This enables you to write different code for initial installation and re-installation.
You write install logic in an install codeunit. This is a codeunit that has the SubType property set to Install. An install codeunit supports two system triggers on which you can add the install code.
OnInstallAppPerCompany()
- Includes code for company-related operations. Runs once for each company in the database.
OnInstallAppPerDatabase()
- Includes code for database-related operations. Runs once in the entire install process.
The install codeunit becomes an integral part of the extension version. You can have more than one install codeunit. However, be aware that there is no guarantee on the order of execution of the different codeunits. If you do use multiple install units, make sure that they can run independent of each other.
The following code illustrates the basic syntax and structure of an install codeunit.
codeunit [ID] [NAME]
{
Subtype=Install;
trigger OnInstallAppPerCompany()
begin
// Code for company related operations
end;
trigger OnInstallAppPerDatabase()
begin
// Code for database related operations
end;
}
Each extension version has a set of properties that contain information about the extension, including: AppVersion, DataVersion, Dependencies, ID, Name, and Publisher. This information can be useful when installing. For example, one of the more important properties is the DataVersion property, which tells you what version of data you are dealing with. These properties are encapsulated in a ModuleInfo data type. You can access these properties through the NAVApp.GetCurrentModuleInfo() and NAVAPP.GetModuleInfo() methods.
This example uses the OnInstallAppPerDatabase() trigger to check whether the data version of the previous extension version is compatible for the upgrade.
codeunit 50100 MyInstallCodeunit
{
Subtype=Install;
trigger OnInstallAppPerDatabase();
var
myAppInfo : ModuleInfo;
begin
NavApp.GetCurrentModuleInfo(myAppInfo); // Get info about the currently executing module
if myAppInfo.DataVersion = Version.Create(0,0,0,0) then // A 'DataVersion' of 0.0.0.0 indicates a 'fresh/new' install
HandleFreshInstall
else
HandleReinstall; // If not a fresh install, then we are Re-installing the same version of the extension
end;
local procedure HandleFreshInstall();
begin
// Do work needed the first time this extension is ever installed for this tenant.
// Some possible usages:
// - Service callback/telemetry indicating that extension was installed
// - Initial data setup for use
end;
local procedure HandleReinstall();
begin
// Do work needed when reinstalling the same version of this extension back on this tenant.
// Some possible usages:
// - Service callback/telemetry indicating that extension was reinstalled
// - Data 'patchup' work, for example, detecting if new 'base' records have been changed while you have been working 'offline'.
// - Setup 'welcome back' messaging for next user access.
end;
}
Writing upgrade code
When you develop a new extension version, you must consider the data from the previous version. You have to determine the modifications that must be made to the data to make it compatible with the current version. For example, maybe the new version adds a new field that needs default values set for existing records. Or, the new version adds new tables that must be linked to existing records. To address this type of data handling, you must write upgrade code for the extension version. If there are no data changes between the extension versions, you don't need to write upgrade code. All data that isn't modified by upgrade code will automatically be available when the process completes.
An upgrade is defined as enabling an extension that has a greater version number, as defined in the app.json file, than the current installed extension version.
You write upgrade logic in an upgrade codeunit, which is a codeunit whose SubType property is set to Upgrade. An upgrade codeunit supports several system triggers on which you can add data upgrade code. These triggers are invoked when you run the data upgrade process on the new extension.
The upgrade codeunit becomes an integral part of the extension and may be modified as needed for later versions. You can have more than one upgrade codeunit. There's a set order to the sequence of the upgrade triggers, but the execution order of the different codeunits isn't guaranteed. If you do use multiple upgrade units, make sure that they can run independent of each other.
The following list describes the upgrade triggers and lists them in the order in which they're invoked.
OnCheckPreconditionsPerCompany() and OnCheckPreconditionsPerDatabase()
Used to check that certain requirements are met before the upgrade can be run.
Fails the upgrade on error.
OnUpgradePerCompany() and OnUpgradePerDatabase()
Used to do the actual upgrade.
Fails the upgrade on error.
OnValidateUpgradePerCompany() and OnValidateUpgradePerDatabase()
Used to check that the upgrade was successful.
Fails the upgrade on error.
PerCompany triggers are run once for each company in the database, where each trigger is executed within its own system session for the company.
PerDatabase triggers are run once in the entire upgrade process, in a single system session that doesn't open any company.
These triggers are also available in upgrade codeunits for the base application, not just for extensions.
The following code illustrates the basic syntax and structure of an upgrade codeunit:
codeunit [ID] [NAME]
{
Subtype=Upgrade;
trigger OnCheckPreconditionsPerCompany()
begin
// Code to make sure company is OK to upgrade.
end;
trigger OnUpgradePerCompany()
begin
// Code to perform company related table upgrade tasks
end;
trigger OnValidateUpgradePerCompany()
begin
// Code to make sure that upgrade was successful for each company
end;
}
Use the shortcuts tcodeunit and ttrigger to create the basic structure for the codeunit and trigger.
Controlling when upgrade code runs
In most cases, it's important, that upgrade code isn't run more than once. There are a couple ways that you can control when upgrade code runs. You can either use extension version data or upgrade tags. These two methods are described in the sections that follow. The method you select will depend on the complexity of your solution. Use the following table as a guideline.
Version number
Version is set manually
When checking whether it's a first-time installation. In this case, comparing with 0.0.0.0
Upgrade tags
Large application with many versions
Extension version changes frequently, for example more than once a year
Fixing a broken upgrade
Using extension version data to control upgrade code
Each extension version has a set of properties that contain information about the extension, including: AppVersion, DataVersion, Dependencies, ID, Name, and Publisher. This information can be useful when upgrading.
The AppVersion is one of the available properties, and its value differs depending on the context of the code being run.
Normal operation - AppVersion represents the value of the currently installed extension.
Installation code - AppVersion represents the version of the extension you're trying to install.
Upgrade code - AppVersion represents the version of the extension that you're upgrading to (in other words, the newer version).
Another one of the more important properties is the DataVersion property, that represents the value of most recently installed, uninstalled, or upgraded version of the extension. This means that it reflects the most recent version of the data on the system, be that from the currently installed, or a previously uninstalled extension. The DataVersion property value differs depending on the context of the code being run.
Normal operation:
- DataVersion represents the version of the currently installed extension, in which case it's identical to the AppVersion property.
Installation code:
Reinstallation (applying the same version):
- DataVersion represents the version of the extension you're trying to install (identical to the AppVersion property).
New installation:
- DataVersion represents the value of '0.0.0.0' that's used to indicate there's no data.
Upgrade code:
The version of the extension you're upgrading from.
Either what was last uninstalled, or what is currently installed.
All these properties are encapsulated in a ModuleInfo data type. You can access these properties through the NAVApp.GetCurrentModuleInfo() and NAVApp.GetModuleInfo() methods.
This example uses the OnCheckPreconditionsPerDatabase() trigger to check whether the data version of the previous extension version is compatible for the upgrade before restoring the archived data of the old extension.
codeunit 50100 MyUpgradeCodeunit
{
Subtype=Upgrade;
trigger OnCheckPreconditionsPerDatabase();
var
myInfo : ModuleInfo;
begin
if NavApp.GetCurrentModuleInfo(myInfo) then
if myInfo.DataVersion = Version.Create(1, 0, 0, 1) then
error('The upgrade isn't compatible');
end;
trigger OnUpgradePerDatabase()
begin
NavApp.RestoreArchiveData(Database::"TableName");
end;
}
Using upgrade tags to control upgrade code
Although you can control upgrade code by checking versions, this pattern becomes difficult with larger applications that have many versions. The risk of something going wrong increases. If your solution includes the Microsoft system application, another way is to use upgrade tags. Upgrade tags provide a more robust and resilient way of controlling the upgrade process. They provide a way to track upgrade methods have been run to prevent executing the same upgrade code twice. Tags can also be used to skip the upgrade methods for a specific company or to fix an upgrade that went wrong.
Upgrade tags are implemented as part the Upgrade Tags module of the system application. The module provides an API that you can code against to control your upgrade code.
The Upgrade Tags module consists of several AL objects. It includes the codeunit 9999 Upgrade Tag for creating and handling upgrade tags and table 9999 Upgrade Tags for storing them.
Codeunit 9999 Upgrade Tag includes the following methods:
HasUpgradeTag(Tag: Code[250]; TagCompanyName: Code[30]): Boolean
Verifies whether a specific upgrade tag exists.
Tag is the code to check.
TagCompanyName is the name of the company for which to check the existence of the tag.
Returns true if the Tag with the given code exists.
SetUpgradeTag(NewTag: Code[250])
- Specifies the tag to save.
SetAllUpgradeTags()
Sets all upgrade tags in a new company.
This method is called from codeunit 2 Company Initialize in the base application.
SetAllUpgradeTags(NewCompanyName: Code[30])
Sets all upgrade tags in a new company.
This method is called from report 357 Copy Company in the base application.
The codeunit also publishes the following events:
OnGetPerCompanyUpgradeTags(var PerCompanyUpgradeTags: List of [Code[250]])
Subscribe to this event to register an upgrade tag for OnUpgradePerCompany upgrade method for a new company. PerCompanyUpgradeTags specifies a list of tags to insert.
Tags that already exist will not be inserted.
OnGetPerDatabaseUpgradeTags(var PerDatabaseUpgradeTags: List of [Code[250]])
Subscribe to this event to register an upgrade tag for OnUpgradePerDatabase upgrade method for a new company. PerDatabaseUpgradeTags specifies a list of tags to insert.
Tags that already exist will not be inserted.
The following steps provide the general pattern for using an upgrade tag on upgrade code.
Use the following construct around the upgrade code to check for and add an upgrade tag.
// Check whether upgrade tag exists if UpgradeTag.HasUpgradeTag(UpgradeTagValue) then exit; // Upgrade code // Add the new upgrade tag using SetUpgradeTag or SetAllUpgradeTags UpgradeTag.SetUpgradeTag(UpgradeTagValue);You can use any value for the upgrade tag, but we recommend that you use the convention [CompanyPrefix]-[ID]-[Description]-[YYYYMMDD].
Add code to register the upgrade tag for new companies that might eventually be created. This step ensures that the upgrade code isn't on the first upgrade because of a missing tag.
To register the tag, subscribe to the OnGetPerCompanyUpgradeTags or OnGetPerDatabaseUpgradeTags events.
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Upgrade Tag", 'OnGetPerCompanyUpgradeTags', '', false, false)] local procedure OnGetPerCompanyTags(var PerCompanyUpgradeTags: List of [Code[250]]); begin PerCompanyUpgradeTags.Add(UpgradeTagValue); end;This step is not necessary if the tag is added by calling the SetAllUpgradeTags method.
Add code to register the upgrade tag for first-time installations of the extension. The step ensures that the upgrade code isn't on first upgrade, because of a missing tag.
To register the tag, call the SetUpgradeTag method on the OnInstallAppPerCompany and OnInstallAppPerDatabase triggers in the extension's install codeunit.
codeunit 50100 InstallCodeunit { Subtype=Install; trigger OnInstallAppPerCompany() var UpgradeTag: Codeunit "Upgrade Tag"; begin if not UpgradeTag.HasUpgradeTag(UpgradeTagValue) then UpgradeTag.SetUpgradeTag(UpgradeTagValue); end; }Instead of registering tags individually, call the SetAllUpgradeTags method as opposed to SetUpgradeTag. This will register automatically register new tags as they're added.
Some design considerations to keep in mind:
Keep tags simple by limiting nesting tags to two levels. Complicated if statements can lead to problems.
Implement additional safety checks to avoid data corruption, even though you're using upgrade tags. For example, when copying obsolete fields to new fields, make sure the new fields have a default or blank. This check adds safety if upgrade tags introduce data issues.
The following code is a simple example of an upgrade codeunit. For this example, the original extension extended the Customer table with a Shoesize field. In the new version of the extension, the Shoesize field has been removed (ObsoleteState=removed) and replaced by a new field, ABC - Customer Shoesize. The upgrade code will copy data from Shoesize field to the ABC - Customer Shoesize. An upgrade tag ensures that code doesn't run more than once, and data is not overwritten on future upgrades. The example also uses a separate codeunit to define the upgrade tag so that they aren't hard-coded, but within methods.
codeunit 50100 "ABC Upgrade Shoe Size"
{
Subtype = Upgrade;
trigger OnUpgradePerCompany()
var
ABCUpgradeTagDefinitions: Codeunit "ABC Upgrade Tag Definitions";
UpgradeTagMgt: Codeunit "Upgrade Tag";
begin
// Check whether the tag has been used before, and if so, don't run upgrade code
if UpgradeTagMgt.HasUpgradeTag(ABCUpgradeTagDefinitions.GetABCShoeSizeUpgradeTag()) then
exit;
// Run upgrade code
UpgradeShoeSize();
// Insert the upgrade tag in table 9999 "Upgrade Tags" for future reference
UpgradeTagMgt.SetUpgradeTag(ABCUpgradeTagDefinitions.GetABCShoeSizeUpgradeTag());
end;
local procedure UpgradeShoeSize()
var
Customer: Record Customer;
begin
if not Customer.FindSet() then
exit;
repeat
// Make sure that target field is blank because you're copying obsolete=removed field to new field
// Additional safety check
if Customer."ABC - Customer Shoesize" <> 0 then
Error('ShoeSize must be blank, the value is already assigned');
//This code avoids blank modifies because they're they down the upgrade
if Customer."ABC - Customer Shoesize" <> Customer.Shoesize then begin
Customer."ABC - Customer Shoesize" := Customer.Shoesize;
Customer.Modify();
end;
until Customer.Next() = 0;
end;
}
codeunit 50101 "ABC Upgrade Tag Definitions"
{
// Register the new upgrade tag for new companies when they are created.
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Upgrade Tag", 'OnGetPerCompanyUpgradeTags', '', false, false)]
local procedure OnGetPerCompanyTags(var PerCompanyUpgradeTags: List of [Code[250]]);
begin
PerCompanyUpgradeTags.Add(GetABCShoeSizeUpgradeTag());
end;
// Use methods to avoid hard-coding the tags. It is easy to remove afterwards because it's compiler-driven.
procedure GetABCShoeSizeUpgradeTag(): Text
begin
exit('ABC-1234-ShoeSizeUpgrade-20201125');
end;
}
Protecting sensitive code from running during upgrade
The extension might initiate code that you don't want to run during upgrade. The changes done to the data stored in the database will be rolled back. However, things like calls to external web services or physical printing can't be rolled back. Also, some code, like scheduling tasks, might throw an error and cause the upgrade to fail.
For example, let's say the extension runs code that prints a check after a purchase invoice is posted for buying shoes. If the upgrade fails, the purchase invoice is rolled back. But the check will still be printed unless you have implemented a mechanism to prevent printing.
To avoid this situation, use the session ExecutionContext. Depending on the scenario, the system runs a session in a special context for a limited time, which can be either Normal, Install, Uninstall, or Upgrade. You get the ExecutionContext by calling the GETEXECUTIONCONTEXT method. For example, referring the example for printing checks, you could add something like the following code to verify the ExecutionContent before printing the check.
EventSubscriber(ObjectType::Codeunit, Codeunit::"Purch.-Post", 'OnAfterPurchInvHeaderInsert', '', false, false)]
local procedure PrintCheckWhenPurchasingShoes(var PurchHeader: Record "Purchase Header"; var PurchInvHeader: Record "Purch. Inv. Header")
begin
// Check whether global context is upgrade or installation of extension
if Session.GetExecutionContext() <> ExecutionContext::Normal then
// Check whether code is triggered by the extension
if Session.GetCurrentModuleExecutionContext() <> ExecutionContext::Normal then
// Something is wrong, so you want to abort here because the code doesn't raise the EnqueuePrintingCheck trigger
Error('Check can't be printed')
else begin
// Other code is invoking the upgrade, so use Job queue or similar mechanism to roll back if upgrade fails
EnqueuePrintingCheck(PurchInvHeader);
exit;
end;
CallWebServiceToPrintCheck(PurchInvHeader);
end;
Important
We have seen a lot of issues around apps failing during installation and upgrade for specific tenant scenarios. It has almost always been due to them making external type calls in their installation and upgrade codeunits. Apps should never run external type calls in installation or upgrade codeunits. Instead they should do them on first launch after installation or upgrade.