Upgrade migrations

New installations get their data from the installer. Existing installations are brought up to date by upgrade migrations: small classes that run once, at startup, when a store starts a newer GrandNode version against an older database. Whenever a change adds localization resources, permissions, admin menu entries, scheduled tasks, settings or indexes, it needs both — seed data for new installs and a migration for existing ones.

How migrations run

  1. At startup the Grand.Module.Migration module (enabled by FeatureManagement:Grand.Module.Migration) reads the version stamp stored in the database.
  2. It collects every class implementing IMigration whose Version is greater than that stamp, ordered by version and then by Priority.
  3. It skips migrations whose Identity is already recorded in the migrations collection, runs the others and records each success.
  4. The last migration of every version is the version stamp itself (MigrationUpgradeDbVersion_24 for 2.4), which moves the database to that version.
  5. If a migration returns false, the process stops before the stamp, logs the error and retries on the next start.

The consequence: a migration placed in the folder of the version a database is already stamped with never runs there. Migrations reach installations that upgrade from an older version.

The shape of a migration

Migrations live in src/Modules/Grand.Module.Migration/Migrations/<major.minor>/. The folder 2.4 maps to the namespace segment _2._4.

using Grand.Infrastructure.Migrations;

namespace Grand.Module.Migration.Migrations._2._4;

public class MigrationUpdateResourceString : IMigration
{
    public int Priority => 0;
    public DbVersion Version => new(2, 4);
    public Guid Identity => new("6C0F2E4B-9A17-4D65-B3C8-51E0A9D77B42");
    public string Name => "Update resource string for english language 2.4";

    public bool UpgradeProcess(IServiceProvider serviceProvider)
    {
        return serviceProvider.ImportLanguageResourcesFromXml("App_Data/Resources/Upgrade/en_240.xml");
    }
}
MemberRule
VersionThe release the change ships in. It must match the folder.
IdentityA freshly generated GUID. The runner uses it to know the migration already ran; a GUID copied from another migration makes one of them silently never run.
PriorityOrder inside the version. Resources, permissions and menu migrations conventionally use 0; migrations that depend on them run higher.
NameHuman-readable, including the version. It appears in the log.

Writing UpgradeProcess

Resolve what you need from the serviceProvider. There is no HTTP request, so there is no current store, customer or language — pass store ids explicitly.

public bool UpgradeProcess(IServiceProvider serviceProvider)
{
    try
    {
        var repository = serviceProvider.GetRequiredService<IRepository<ScheduleTask>>();
        if (repository.Table.Any(x => x.ScheduleTaskName == "Example task"))
            return true;   // already there: running twice must be a no-op

        repository.Insert(new ScheduleTask {
            ScheduleTaskName = "Example task",
            Enabled = false,
            TimeInterval = 60
        });
        return true;
    }
    catch (Exception ex)
    {
        serviceProvider.GetRequiredService<ILogger<MigrationExample>>()
            .LogError(ex, "Example migration failed");
        return false;
    }
}
  • Never throw. Catch, log and return false.
  • Be idempotent. Check before inserting; a migration can run again after a partial upgrade.
  • Do not destroy operator data. Change only values an operator cannot have customised.
  • Preserve behaviour. A new setting gets the default that keeps an upgraded store working as before.

Localization resources

Do not write resource strings in C#. Add each new key twice: to App_Data/Resources/DefaultLanguage.xml for new installations and to the upgrade file of the release, for example App_Data/Resources/Upgrade/en_240.xml, which the version's MigrationUpdateResourceString imports. DefaultLanguage.xml is saved as UTF-16, so plain grep finds nothing in it; convert it first or use an editor.

Other kinds of migration

Copy the file of the same name from the newest version folder rather than inventing a shape:

ChangeFollow
PermissionsMigrationSystemPermission.cs, MigrationUpdateStandardPermissionNames.cs
Admin navigationMigrationUpdateAdminSiteMap.cs
Scheduled tasksMigrationScheduleTasks.cs
Settings and store dataMigrationUpdateMediaSettings.cs, MigrationStoreSingleUrl.cs
IndexesMigrationUniqueOrderNumberIndex.cs
A new version folderNeeds its own MigrationUpgradeDbVersion_XX deriving from MigrationUpgradeDbVersionBase.

The runner also picks up IMigration classes from installed plugins, but they are gated by the same GrandNode database version, so they fire only when the store upgrades GrandNode itself. A plugin that changes its own data between plugin versions should handle that in its own code.

Testing a migration locally

  1. Back up the development database.
  2. In the GrandNodeVersion collection, set InstalledVersion and DataBaseVersion back one minor version (for example from 2.4 to 2.3).
  3. Restart the application. Migrations already recorded are skipped by Identity, so only new ones run; the log shows each one.
  4. Check the result, then make sure the version stamp is back at the current version.

Question not answered here? Ask the community on GitHub Discussions ↗