FeatureVersionChanged Event
Summary
The FeatureVersionChanged event is for versioned data upgrades that do not depend on a schema change. Each upgrade method has a feature name and version number. Core stores versions in the __FeatureVersion database table. An upgrade runs when its feature is enabled for the tenant and its stored version is lower than the method's version.
When does it fire?
FeatureVersionChanged runs after the schema upgrade. Core finds and checks every method with the [FeatureVersionChanged] attribute. Invalid methods still fail startup, even when their feature is disabled. Core does not run or save versions for disabled features. For enabled features, it runs newer methods from the lowest version to the highest.
Schema migration complete → Discover and validate subscribers → Filter by tenant features
→ Load stored feature versions from DB → Invoke newer versions in order → Persist new version
All feature version upgrades run within a single transaction. If any upgrade fails, all are rolled back.
A database created from blank never runs them. Its schema comes from the current model, so there is no
older data for the upgrade methods to transform; Core records each feature enabled for the tenant at its
highest version at creation time, with UpdatedByMethod set to CreatedAtCurrentVersion. A feature that
is disabled for the tenant is left unrecorded, so its upgrades still run when the tenant enables it later.
Syntax
[FeatureVersionChanged("Namespace.FeatureName", 1)]
public void MethodName(FeatureVersionChangedEventArgs args)
{
args.UpgradeScript("SQL script here");
}
Attribute
| Parameter | Type | Description |
|---|---|---|
featureName |
string |
The feature identifier — must match the declaring class's namespace |
version |
int |
The version number (must be ≥ 1, executed in ascending order) |
Event Args (FeatureVersionChangedEventArgs)
| Property/Method | Description |
|---|---|
FeatureName |
The feature name being upgraded |
Version |
The version number of this upgrade |
UpgradeScript(sql) |
Adds a SQL script to execute during the upgrade |
ReadFromDatabase(sql) |
Reads data from the database to help generate migration scripts |
Scenarios
1. Simple data correction
Fix null values in an existing column as a one-time versioned migration.
namespace Benevia.ERP.Model;
public class FeatureVersionDataUpgrade
{
[FeatureVersionChanged("Benevia.ERP.Model", 1)]
public void FixNullUsernames(FeatureVersionChangedEventArgs args)
{
args.UpgradeScript("""UPDATE "Users" SET "Username" = 'migrated' WHERE "Username" IS NULL;""");
}
}
2. Sequential versioned upgrades
Apply multiple upgrades in order. Version 1 runs first, then version 2, regardless of method declaration order.
namespace Benevia.ERP.Hatchery;
public class HatcheryDataUpgrade
{
[FeatureVersionChanged("Benevia.ERP.Hatchery", 1)]
public void InitializeFlockStatus(FeatureVersionChangedEventArgs args)
{
args.UpgradeScript("""UPDATE "Flock" SET "Status" = 0 WHERE "Status" IS NULL;""");
}
[FeatureVersionChanged("Benevia.ERP.Hatchery", 2)]
public void RecalculateFlockAges(FeatureVersionChangedEventArgs args)
{
args.UpgradeScript("""
UPDATE "Flock"
SET "AgeInWeeks" = EXTRACT(DAY FROM NOW() - "HatchDate"::timestamp) / 7
WHERE "HatchDate" IS NOT NULL;
""");
}
}
3. Data migration using ReadFromDatabase
Read existing data to build row-specific migration scripts.
namespace Benevia.ERP.Sales;
public class SalesDataUpgrade
{
[FeatureVersionChanged("Benevia.ERP.Sales", 1)]
public void MigrateCustomerCodes(FeatureVersionChangedEventArgs args)
{
var customers = args.ReadFromDatabase(
"""SELECT "Guid", "Name" FROM "Customer" WHERE "Code" IS NULL""");
foreach (System.Data.DataRow row in customers.Rows)
{
var guid = row["Guid"];
var name = row["Name"]?.ToString()?.ToUpperInvariant().Replace(" ", "");
var code = name?.Length > 6 ? name[..6] : name;
args.UpgradeScript(
$"""UPDATE "Customer" SET "Code" = '{code}' WHERE "Guid" = '{guid}';""");
}
}
}
FeatureVersionChanged vs. PropertyAdded
| FeatureVersionChanged | PropertyAdded | |
|---|---|---|
| Trigger | Explicit version number | Schema change (new column detected) |
| Runs | Once per version, tracked in __FeatureVersion table |
Every time the column addition is detected |
| Use for | Data corrections, computed backfills, one-time migrations | Populating new columns with initial values |
| Ordering | Versions execute in ascending order per feature | No guaranteed order between subscribers |
Constraints
- The
featureNameparameter must exactly match the namespace of the declaring class. If they differ, the application will throw anInvalidOperationExceptionat startup. - Version numbers must be ≥ 1.
- The method must accept exactly one parameter of type
FeatureVersionChangedEventArgs. - Upgraded versions are persisted in the
__FeatureVersiontable with the method name and timestamp. - All feature upgrades run in a single transaction — if one fails, all are rolled back.
Notes
- Feature version upgrades run after schema migration, so new columns and tables are already available.
- Core does not run or save an upgrade while its feature is disabled. It runs later when the feature is enabled.
- Core logs each version upgrade that it skips because its feature is disabled.
- Core uses the subscriber class's namespace to find its feature. It must match the feature name in the attribute.
- Core runs every subscriber if the host has no feature catalog or current tenant feature scope.
- Subscribers are discovered only in assemblies marked
[assembly: DataMigrationAssembly]. An unmarked assembly is never scanned, so its subscriber never runs and the upgrade fails silently. Add the marker in anAssemblyInfo.csnext to the project file. - Use feature versions for data transformations that are not tied to a specific schema change, or when you need guaranteed ordering of multiple migration steps.
- Each version runs exactly once per database. Re-deploying the same version has no effect.