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
- At startup the
Grand.Module.Migrationmodule (enabled byFeatureManagement:Grand.Module.Migration) reads the version stamp stored in the database. - It collects every class implementing
IMigrationwhoseVersionis greater than that stamp, ordered by version and then byPriority. - It skips migrations whose
Identityis already recorded in the migrations collection, runs the others and records each success. - The last migration of every version is the version stamp itself (
MigrationUpgradeDbVersion_24for 2.4), which moves the database to that version. - 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");
}
}
| Member | Rule |
|---|---|
Version | The release the change ships in. It must match the folder. |
Identity | A 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. |
Priority | Order inside the version. Resources, permissions and menu migrations conventionally use 0; migrations that depend on them run higher. |
Name | Human-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:
| Change | Follow |
|---|---|
| Permissions | MigrationSystemPermission.cs, MigrationUpdateStandardPermissionNames.cs |
| Admin navigation | MigrationUpdateAdminSiteMap.cs |
| Scheduled tasks | MigrationScheduleTasks.cs |
| Settings and store data | MigrationUpdateMediaSettings.cs, MigrationStoreSingleUrl.cs |
| Indexes | MigrationUniqueOrderNumberIndex.cs |
| A new version folder | Needs 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
- Back up the development database.
- In the
GrandNodeVersioncollection, setInstalledVersionandDataBaseVersionback one minor version (for example from2.4to2.3). - Restart the application. Migrations already recorded are skipped by
Identity, so only new ones run; the log shows each one. - Check the result, then make sure the version stamp is back at the current version.
Related
- Architecture overview
- The AI Agent Kit —
.ai/templates/migration.mdand.ai/prompts/add-migration.md.
Question not answered here? Ask the community on GitHub Discussions ↗