Skip to content

Commit e8618bf

Browse files
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
1 parent 91681fe commit e8618bf

1 file changed

Lines changed: 35 additions & 50 deletions

File tree

dev-itpro/developer/devenv-upgrading-extensions.md

Lines changed: 35 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -241,85 +241,70 @@ The following steps provide the general pattern for using an upgrade tag on upgr
241241
242242
### Example
243243
244-
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.
244+
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.
245+
246+
> Source: [`SalesForecastUpgrade.Codeunit.al`](https://github.com/microsoft/ALAppExtensions/blob/main/Apps/W1/SalesAndInventoryForecast/app/src/codeunits/SalesForecastUpgrade.Codeunit.al) in `microsoft/ALAppExtensions`
245247
246248
```AL
247-
namespace Contoso.ABCShoeExtension;
249+
namespace Microsoft.Inventory.InventoryForecast;
248250
249-
using Microsoft.Sales.Customer;
251+
using System.Threading;
250252
using System.Upgrade;
251253
252-
codeunit 50100 "ABC Upgrade Shoe Size"
254+
codeunit 1851 "Sales Forecast Upgrade"
253255
{
254256
Access = Internal;
255257
Subtype = Upgrade;
256258
257259
trigger OnUpgradePerCompany()
258260
var
259-
ABCUpgradeTagDefinitions: Codeunit "ABC Upgrade Tag Definitions";
260-
UpgradeTagMgt: Codeunit "Upgrade Tag";
261+
ModuleInfo: ModuleInfo;
261262
begin
262-
263-
// Check whether the tag has been used before, and if so, don't run upgrade code
264-
if UpgradeTagMgt.HasUpgradeTag(ABCUpgradeTagDefinitions.GetABCShoeSizeUpgradeTag()) then
265-
exit;
266-
267-
// Run upgrade code
268-
UpgradeShoeSize();
269-
270-
// Insert the upgrade tag in table 9999 "Upgrade Tags" for future reference
271-
UpgradeTagMgt.SetUpgradeTag(ABCUpgradeTagDefinitions.GetABCShoeSizeUpgradeTag());
263+
if NavApp.GetCurrentModuleInfo(ModuleInfo) then
264+
SetConsentIfForecastAlreadyScheduled();
272265
end;
273266
274-
local procedure UpgradeShoeSize()
267+
local procedure SetConsentIfForecastAlreadyScheduled()
275268
var
276-
Customer: Record Customer;
269+
SalesForecastSetup: Record "MS - Sales Forecast Setup";
270+
JobQueueEntry: Record "Job Queue Entry";
271+
UpgradeTag: Codeunit "Upgrade Tag";
277272
begin
278-
279-
if not Customer.FindSet() then
273+
if UpgradeTag.HasUpgradeTag(GetForecastCustomerConsentTag(), '') then
280274
exit;
281275
282-
repeat
283-
// Make sure that target field is blank because you're copying obsolete=removed field to new field
284-
// Additional safety check
285-
if Customer."ABC - Customer Shoesize" <> 0 then
286-
Error('ShoeSize must be blank, the value is already assigned');
276+
if UpgradeTag.HasUpgradeTag(GetForecastCustomerConsentTag()) then
277+
exit;
287278
288-
// Avoid blank modifies - it is a performance hit and slows down the upgrade
289-
if Customer."ABC - Customer Shoesize" <> Customer.Shoesize then begin
290-
Customer."ABC - Customer Shoesize" := Customer.Shoesize;
291-
Customer.Modify();
279+
if SalesForecastSetup.Get() then
280+
if not SalesForecastSetup.Enabled then begin
281+
JobQueueEntry.SetRange("Object Type to Run", JobQueueEntry."Object Type to Run"::Codeunit);
282+
JobQueueEntry.SetRange("Object ID to Run", Codeunit::"Sales Forecast Update");
283+
if not JobQueueEntry.IsEmpty() then begin
284+
SalesForecastSetup.Enabled := true;
285+
SalesForecastSetup.Modify();
286+
end;
292287
end;
293-
until Customer.Next() = 0;
294-
end;
295-
}
296288
297-
namespace Contoso.ABCShoeExtension;
298-
299-
using System.Upgrade;
300-
301-
codeunit 50101 "ABC Upgrade Tag Definitions"
302-
{
303-
Access = Internal;
304-
305-
// Register the new upgrade tag for new companies when they are created.
306-
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Upgrade Tag", 'OnGetPerCompanyUpgradeTags', '', false, false)]
307-
local procedure OnGetPerCompanyTags(var PerCompanyUpgradeTags: List of [Code[250]])
308-
begin
309-
PerCompanyUpgradeTags.Add(GetABCShoeSizeUpgradeTag());
289+
UpgradeTag.SetUpgradeTag(GetForecastCustomerConsentTag());
310290
end;
311291
312-
// Use methods to avoid hard-coding the tags. It is easy to remove afterwards because it's compiler-driven.
313-
procedure GetABCShoeSizeUpgradeTag(): Code[250]
292+
internal procedure GetForecastCustomerConsentTag(): Code[250]
314293
begin
315-
exit('ABC-1234-ShoeSizeUpgrade-20201125');
294+
exit('MS-474737-SalesForecastCustomerConsent-20230607');
316295
end;
317296
318297
}
319298
```
320299

321-
> [!TIP]
322-
> 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.
300+
Key points from this production example:
301+
302+
- `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.
303+
- The upgrade tag procedure returns `Code[250]`, matching the parameter type of `HasUpgradeTag` and `SetUpgradeTag` exactly.
304+
- `Access = Internal` prevents external callers from triggering the upgrade codeunit directly.
305+
- `NavApp.GetCurrentModuleInfo()` retrieves app metadata (including data version) to gate whether migration is needed.
306+
307+
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.
323308

324309
## Protecting sensitive code from running during upgrade
325310

0 commit comments

Comments
 (0)