FeatureVersionChanged Event
Summary
The FeatureVersionChanged event supports versioned data upgrades that are independent of schema changes. Each upgrade method is tagged with a feature name and version number. Versions are tracked in the __FeatureVersion database table, and each upgrade runs exactly once — only when the stored version is lower than the method's declared version.
When does it fire?
FeatureVersionChanged runs after schema migration completes. The FeatureVersionUpgradeManager discovers all methods with the [FeatureVersionChanged] attribute, loads the current version for each feature from the __FeatureVersion table, and invokes any methods whose version is higher than the stored version — in ascending order.
Schema migration complete → Load feature versions from DB → Discover subscribers
→ For each feature (ordered by version): skip if already applied, invoke if new → Persist new version
All feature version upgrades run within a single transaction. If any upgrade fails, all are rolled back.
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.
- Subscribers are discovered from all loaded assemblies — you can place them in any project that is referenced by the API host.
- 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.