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 featureName parameter must exactly match the namespace of the declaring class. If they differ, the application will throw an InvalidOperationException at startup.
  • Version numbers must be ≥ 1.
  • The method must accept exactly one parameter of type FeatureVersionChangedEventArgs.
  • Upgraded versions are persisted in the __FeatureVersion table 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.