From 91681fe44341ac8febd56cf6379e8e23c4f42dd6 Mon Sep 17 00:00:00 2001 From: Jeffrey Bulanadi <41933086+jeffreybulanadi@users.noreply.github.com> Date: Thu, 7 May 2026 09:00:00 +0800 Subject: [PATCH 1/3] fix(devenv-upgrading-extensions): add namespaces, fix return type Code[250], add Access=Internal - Add namespace Contoso.ABCShoeExtension to ABC Upgrade Shoe Size, ABC Upgrade Tag Definitions, InstallCodeunit - Add namespace Contoso.MyExtension to MyUpgradeCodeunit preconditions example - Fix GetABCShoeSizeUpgradeTag return type: Text -> Code[250] (HasUpgradeTag/SetUpgradeTag take Code[250]) - Add Access = Internal to all non-template codeunits (upgrade/install codeunits should not be public) - Add using Microsoft.Sales.Customer and using System.Upgrade where needed - Fix Subtype=Upgrade -> Subtype = Upgrade (AL spacing convention) - Remove erroneous trailing semicolons from trigger declarations - Fix error() -> Error() (built-in procedure capitalization) - Fix myInfo : ModuleInfo -> myInfo: ModuleInfo (AL variable spacing) - Add TIP note linking to real SalesForecastUpgrade.Codeunit.al in microsoft/ALAppExtensions Source: ALAppExtensions/Apps/W1/SalesAndInventoryForecast/app/src/codeunits/SalesForecastUpgrade.Codeunit.al --- .../developer/devenv-upgrading-extensions.md | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/dev-itpro/developer/devenv-upgrading-extensions.md b/dev-itpro/developer/devenv-upgrading-extensions.md index 8a6864d4dd..cd4c0d09af 100644 --- a/dev-itpro/developer/devenv-upgrading-extensions.md +++ b/dev-itpro/developer/devenv-upgrading-extensions.md @@ -110,17 +110,20 @@ All these properties are encapsulated in a `ModuleInfo` data type. You can acces 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. ```AL +namespace Contoso.MyExtension; + codeunit 50100 MyUpgradeCodeunit { - Subtype=Upgrade; + Access = Internal; + Subtype = Upgrade; - trigger OnCheckPreconditionsPerDatabase(); + trigger OnCheckPreconditionsPerDatabase() var - myInfo : ModuleInfo; + myInfo: ModuleInfo; begin if NavApp.GetCurrentModuleInfo(myInfo) then if myInfo.DataVersion = Version.Create(1, 0, 0, 1) then - error('The upgrade is not compatible'); + Error('The upgrade is not compatible'); end; trigger OnUpgradePerDatabase() @@ -207,9 +210,14 @@ The following steps provide the general pattern for using an upgrade tag on upgr To register the tag, call the `SetUpgradeTag` method on the `OnInstallAppPerCompany` and `OnInstallAppPerDatabase` triggers in the extension's install codeunit. ```AL + namespace Contoso.ABCShoeExtension; + + using System.Upgrade; + codeunit 50100 InstallCodeunit { - Subtype=Install; + Access = Internal; + Subtype = Install; trigger OnInstallAppPerCompany() var @@ -236,8 +244,14 @@ The following steps provide the general pattern for using an upgrade tag on upgr 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](properties/devenv-obsoletestate-property.md)=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 isn't 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. ```AL +namespace Contoso.ABCShoeExtension; + +using Microsoft.Sales.Customer; +using System.Upgrade; + codeunit 50100 "ABC Upgrade Shoe Size" { + Access = Internal; Subtype = Upgrade; trigger OnUpgradePerCompany() @@ -280,8 +294,14 @@ codeunit 50100 "ABC Upgrade Shoe Size" end; } +namespace Contoso.ABCShoeExtension; + +using System.Upgrade; + codeunit 50101 "ABC Upgrade Tag Definitions" { + Access = Internal; + // 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]]) @@ -290,7 +310,7 @@ codeunit 50101 "ABC Upgrade Tag Definitions" end; // Use methods to avoid hard-coding the tags. It is easy to remove afterwards because it's compiler-driven. - procedure GetABCShoeSizeUpgradeTag(): Text + procedure GetABCShoeSizeUpgradeTag(): Code[250] begin exit('ABC-1234-ShoeSizeUpgrade-20201125'); end; @@ -298,6 +318,9 @@ codeunit 50101 "ABC Upgrade Tag Definitions" } ``` +> [!TIP] +> This pattern is used verbatim in production Microsoft extensions. For example, see [`SalesForecastUpgrade.Codeunit.al`](https://github.com/microsoft/ALAppExtensions/blob/main/Apps/W1/SalesAndInventoryForecast/app/src/codeunits/SalesForecastUpgrade.Codeunit.al) in `microsoft/ALAppExtensions` for a real-world upgrade codeunit with namespace declarations and upgrade tags. + ## Protecting sensitive code from running during upgrade The extension might start 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. Some code, like scheduling tasks, might also throw an error and fail the upgrade. From e8618bfdb26a040239d2db7be2ddf491c827ad5d Mon Sep 17 00:00:00 2001 From: Jeffrey Bulanadi <41933086+jeffreybulanadi@users.noreply.github.com> Date: Thu, 7 May 2026 11:00:00 +0800 Subject: [PATCH 2/3] Replace fictional ABC Shoe Size example with real SalesForecastUpgrade.Codeunit.al Port actual production code from microsoft/ALAppExtensions instead of invented tutorial code. Source: Apps/W1/SalesAndInventoryForecast/app/src/codeunits/SalesForecastUpgrade.Codeunit.al - Remove fictional Customer.Shoesize field migration (ABC-1234 invented tag) - Add real codeunit 1851 Sales Forecast Upgrade with namespace Microsoft.Inventory.InventoryForecast - Real tag MS-474737-SalesForecastCustomerConsent-20230607 - Real HasUpgradeTag double-check pattern (database-scope '' + per-company) - Add explanation of key patterns observed in the real code - Link to ReviewGLEntries Upgrade.Codeunit.al for DataTransfer bulk migration pattern --- .../developer/devenv-upgrading-extensions.md | 85 ++++++++----------- 1 file changed, 35 insertions(+), 50 deletions(-) diff --git a/dev-itpro/developer/devenv-upgrading-extensions.md b/dev-itpro/developer/devenv-upgrading-extensions.md index cd4c0d09af..afc9af3be0 100644 --- a/dev-itpro/developer/devenv-upgrading-extensions.md +++ b/dev-itpro/developer/devenv-upgrading-extensions.md @@ -241,85 +241,70 @@ The following steps provide the general pattern for using an upgrade tag on upgr ### Example -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](properties/devenv-obsoletestate-property.md)=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 isn't 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. +The following code is taken directly from the `Sales Forecast Upgrade` codeunit in `microsoft/ALAppExtensions`. It demonstrates the canonical upgrade tag pattern: check the tag first, run conditional data migration, then set the tag to prevent the code from running again on future upgrades. + +> Source: [`SalesForecastUpgrade.Codeunit.al`](https://github.com/microsoft/ALAppExtensions/blob/main/Apps/W1/SalesAndInventoryForecast/app/src/codeunits/SalesForecastUpgrade.Codeunit.al) in `microsoft/ALAppExtensions` ```AL -namespace Contoso.ABCShoeExtension; +namespace Microsoft.Inventory.InventoryForecast; -using Microsoft.Sales.Customer; +using System.Threading; using System.Upgrade; -codeunit 50100 "ABC Upgrade Shoe Size" +codeunit 1851 "Sales Forecast Upgrade" { Access = Internal; Subtype = Upgrade; trigger OnUpgradePerCompany() var - ABCUpgradeTagDefinitions: Codeunit "ABC Upgrade Tag Definitions"; - UpgradeTagMgt: Codeunit "Upgrade Tag"; + ModuleInfo: ModuleInfo; 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()); + if NavApp.GetCurrentModuleInfo(ModuleInfo) then + SetConsentIfForecastAlreadyScheduled(); end; - local procedure UpgradeShoeSize() + local procedure SetConsentIfForecastAlreadyScheduled() var - Customer: Record Customer; + SalesForecastSetup: Record "MS - Sales Forecast Setup"; + JobQueueEntry: Record "Job Queue Entry"; + UpgradeTag: Codeunit "Upgrade Tag"; begin - - if not Customer.FindSet() then + if UpgradeTag.HasUpgradeTag(GetForecastCustomerConsentTag(), '') 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'); + if UpgradeTag.HasUpgradeTag(GetForecastCustomerConsentTag()) then + exit; - // Avoid blank modifies - it is a performance hit and slows down the upgrade - if Customer."ABC - Customer Shoesize" <> Customer.Shoesize then begin - Customer."ABC - Customer Shoesize" := Customer.Shoesize; - Customer.Modify(); + if SalesForecastSetup.Get() then + if not SalesForecastSetup.Enabled then begin + JobQueueEntry.SetRange("Object Type to Run", JobQueueEntry."Object Type to Run"::Codeunit); + JobQueueEntry.SetRange("Object ID to Run", Codeunit::"Sales Forecast Update"); + if not JobQueueEntry.IsEmpty() then begin + SalesForecastSetup.Enabled := true; + SalesForecastSetup.Modify(); + end; end; - until Customer.Next() = 0; - end; -} -namespace Contoso.ABCShoeExtension; - -using System.Upgrade; - -codeunit 50101 "ABC Upgrade Tag Definitions" -{ - Access = Internal; - - // 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()); + UpgradeTag.SetUpgradeTag(GetForecastCustomerConsentTag()); end; - // Use methods to avoid hard-coding the tags. It is easy to remove afterwards because it's compiler-driven. - procedure GetABCShoeSizeUpgradeTag(): Code[250] + internal procedure GetForecastCustomerConsentTag(): Code[250] begin - exit('ABC-1234-ShoeSizeUpgrade-20201125'); + exit('MS-474737-SalesForecastCustomerConsent-20230607'); end; } ``` -> [!TIP] -> This pattern is used verbatim in production Microsoft extensions. For example, see [`SalesForecastUpgrade.Codeunit.al`](https://github.com/microsoft/ALAppExtensions/blob/main/Apps/W1/SalesAndInventoryForecast/app/src/codeunits/SalesForecastUpgrade.Codeunit.al) in `microsoft/ALAppExtensions` for a real-world upgrade codeunit with namespace declarations and upgrade tags. +Key points from this production example: + +- `HasUpgradeTag` is called twice — once with an empty company string `''` (database-level scope) and once without (per-company scope). The double-check prevents re-execution across both scopes. +- The upgrade tag procedure returns `Code[250]`, matching the parameter type of `HasUpgradeTag` and `SetUpgradeTag` exactly. +- `Access = Internal` prevents external callers from triggering the upgrade codeunit directly. +- `NavApp.GetCurrentModuleInfo()` retrieves app metadata (including data version) to gate whether migration is needed. + +For bulk field migration across large tables (for example, copying data from an obsolete field to a replacement field), use `DataTransfer` instead of row-by-row `Modify`. For a real-world example of `DataTransfer` combined with upgrade tags, see [`Upgrade.Codeunit.al`](https://github.com/microsoft/ALAppExtensions/blob/main/Apps/W1/ReviewGLEntries/app/src/codeunits/Upgrade.Codeunit.al) in the Review GL Entries extension. ## Protecting sensitive code from running during upgrade From e6cea1ab1aeefdafee678e9f56e6538da44e3a70 Mon Sep 17 00:00:00 2001 From: Jeffrey Bulanadi <41933086+jeffreybulanadi@users.noreply.github.com> Date: Thu, 7 May 2026 13:00:00 +0800 Subject: [PATCH 3/3] Remove attribution sentences and source links from upgrade extension examples --- dev-itpro/developer/devenv-upgrading-extensions.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/dev-itpro/developer/devenv-upgrading-extensions.md b/dev-itpro/developer/devenv-upgrading-extensions.md index afc9af3be0..68be3e68df 100644 --- a/dev-itpro/developer/devenv-upgrading-extensions.md +++ b/dev-itpro/developer/devenv-upgrading-extensions.md @@ -241,10 +241,6 @@ The following steps provide the general pattern for using an upgrade tag on upgr ### Example -The following code is taken directly from the `Sales Forecast Upgrade` codeunit in `microsoft/ALAppExtensions`. It demonstrates the canonical upgrade tag pattern: check the tag first, run conditional data migration, then set the tag to prevent the code from running again on future upgrades. - -> Source: [`SalesForecastUpgrade.Codeunit.al`](https://github.com/microsoft/ALAppExtensions/blob/main/Apps/W1/SalesAndInventoryForecast/app/src/codeunits/SalesForecastUpgrade.Codeunit.al) in `microsoft/ALAppExtensions` - ```AL namespace Microsoft.Inventory.InventoryForecast; @@ -304,7 +300,7 @@ Key points from this production example: - `Access = Internal` prevents external callers from triggering the upgrade codeunit directly. - `NavApp.GetCurrentModuleInfo()` retrieves app metadata (including data version) to gate whether migration is needed. -For bulk field migration across large tables (for example, copying data from an obsolete field to a replacement field), use `DataTransfer` instead of row-by-row `Modify`. For a real-world example of `DataTransfer` combined with upgrade tags, see [`Upgrade.Codeunit.al`](https://github.com/microsoft/ALAppExtensions/blob/main/Apps/W1/ReviewGLEntries/app/src/codeunits/Upgrade.Codeunit.al) in the Review GL Entries extension. +For bulk field migration across large tables (for example, copying data from an obsolete field to a replacement field), use `DataTransfer` instead of row-by-row `Modify`. ## Protecting sensitive code from running during upgrade