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 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.
  • 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 an AssemblyInfo.cs next 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.