PropertyAdded Event

Summary

The PropertyAdded event fires during database upgrade when a new column is detected on an existing table. Use it to populate the new column with appropriate initial values — either static defaults, values derived from related tables, or computed from other columns on the same table.

When does it fire?

PropertyAdded fires when the schema comparison finds a new column in the tenant's EF Core model. The subscriber runs before any NOT NULL constraint is applied, so it can fill the column first. The subscriber class's namespace does not control whether it runs.

Schema comparison → Column added detected → PropertyAdded subscribers → Schema DDL applied → NOT NULL constraints applied

Syntax

[PropertyAdded(nameof(EntityName), nameof(EntityName.PropertyName))]
public void MethodName(PropertyAddedEventArgs args)
{
    args.UpgradeScript("SQL script here");
}

Attribute

Name Type Meaning
entityName string The entity (table) name
propertyName string The property (column) name being added
FeatureName string? The feature that the SQL needs

Set FeatureName when the SQL uses a table or column from another feature. Core skips this subscriber when that feature is off for the tenant.

[PropertyAdded(
    nameof(Product),
    "SalesAccountGuid",
    FeatureName = "Benevia.ERP.Model.Financials")]
public void BackfillSalesAccount(PropertyAddedEventArgs args)
{
    args.UpgradeScript("SQL that reads FinancialAccount");
}

Leave FeatureName empty when the SQL does not need another feature. The subscriber class's namespace does not set this value.

Event Args (PropertyAddedEventArgs)

Property/Method Description
TableName The database table name
ColumnName The database column name being added
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. Setting a simple default for a new numeric column

When a new MortalityRate property is added to Flock, set all existing rows to 0.

public class DataBaseUpgrade
{
    [PropertyAdded(nameof(Flock), nameof(Flock.MortalityRate))]
    public void AddFlockMortalityRateProperty(PropertyAddedEventArgs args)
    {
        var sql = $"UPDATE \"{args.TableName}\" SET \"MortalityRate\" = 0 WHERE \"MortalityRate\" IS NULL;";
        args.UpgradeScript(sql);
    }
}

2. Setting a default for a new boolean column

When a new Pickable property is added to Product, default all existing products to true.

public class DataBaseUpgrade
{
    [PropertyAdded(nameof(Product), nameof(Product.Pickable))]
    public void AddPickableProperty(PropertyAddedEventArgs args)
    {
        var sql = $"UPDATE \"{args.TableName}\" SET \"Pickable\" = true WHERE \"Pickable\" IS NULL OR \"Pickable\" = false;";
        args.UpgradeScript(sql);
    }
}

3. Setting a default for a new string column

When a new Description property is added, default it to an empty string.

public class DataBaseUpgrade
{
    [PropertyAdded(nameof(Flock), nameof(Flock.Description))]
    public void AddFlockDescriptionProperty(PropertyAddedEventArgs args)
    {
        var sql = $"UPDATE \"{args.TableName}\" SET \"Description\" = '' WHERE \"Description\" IS NULL;";
        args.UpgradeScript(sql);
    }
}

When BeginWeekDate is added to FlockProduction, compute its value from the parent Flock's LayStartDate and the production's Week number.

public class DataBaseUpgrade
{
    [PropertyAdded(nameof(FlockProduction), nameof(FlockProduction.BeginWeekDate))]
    public void AddFlockProductionBeginWeekDateProperty(PropertyAddedEventArgs args)
    {
        var sql = $@"
            -- Treat .NET default DateOnly (1970-01-01) as missing
            UPDATE ""{args.TableName}"" SET ""{args.ColumnName}"" = NULL WHERE ""{args.ColumnName}"" = DATE '1970-01-01';

            -- Set BeginWeekDate based on Flock.LayStartDate and Week
            UPDATE ""{args.TableName}"" AS fp
            SET ""{args.ColumnName}"" = f.""LayStartDate"" + ((CASE WHEN fp.""Week"" > 0 THEN fp.""Week"" - 1 ELSE 0 END) * 7)
            FROM ""Flock"" AS f
            WHERE fp.""FlockGuid"" = f.""Guid""
              AND f.""LayStartDate"" IS NOT NULL
              AND (fp.""{args.ColumnName}"" IS NULL OR EXTRACT(YEAR FROM fp.""{args.ColumnName}"") = 1);
        ";
        args.UpgradeScript(sql);
    }
}

5. Populating a new column from a foreign key relationship with a fallback

When BreedGuid is added to FlockProduction, copy it from the parent Flock.

public class DataBaseUpgrade
{
    [PropertyAdded(nameof(FlockProduction), nameof(FlockProduction.BreedGuid))]
    public void AddFlockProductionBreedAndFarmProperty(PropertyAddedEventArgs args)
    {
        var sql = $@"
            UPDATE ""{args.TableName}"" AS fp
            SET ""BreedGuid"" = f.""BreedGuid""
            FROM ""Flock"" AS f
            WHERE fp.""FlockGuid"" = f.""Guid""
              AND fp.""BreedGuid"" IS NULL
              AND f.""BreedGuid"" IS NOT NULL;

            UPDATE ""{args.TableName}"" AS fp
            SET ""FarmGuid"" = f.""FarmGuid""
            FROM ""Flock"" AS f
            WHERE fp.""FlockGuid"" = f.""Guid""
              AND fp.""FarmGuid"" IS NULL
              AND f.""FarmGuid"" IS NOT NULL;";
        args.UpgradeScript(sql);
    }
}

6. Complex multi-step migration with conditional column checks

When ProjectedHatchRate is added to ManufacturingOrderDetail, populate it from related FlockProduction data and optionally update a companion column if it also exists.

public class DataBaseUpgrade
{
    [PropertyAdded(nameof(ManufacturingOrderDetail), nameof(ManufacturingOrderDetail.ProjectedHatchRate))]
    public void AddManufacturingHatchRate(PropertyAddedEventArgs args)
    {
        var sql = $@"
            UPDATE ""{args.TableName}"" AS mod
            SET ""ProjectedHatchRate"" = COALESCE(fp.""ProjectedHatchRate"", 0)
            FROM ""FlockProduction"" AS fp
            WHERE mod.""FlockProductionGuid"" = fp.""Guid""
              AND mod.""FlockProductionGuid"" IS NOT NULL;
            
            UPDATE ""{args.TableName}""
            SET ""ProjectedHatchRate"" = 0
            WHERE ""FlockProductionGuid"" IS NULL
              AND ""ProjectedHatchRate"" IS NULL;
            
            DO $$
            BEGIN
                IF EXISTS (SELECT 1 FROM information_schema.columns 
                          WHERE table_name = '{args.TableName}' 
                          AND column_name = 'ProjectedHatchCount') THEN
                    UPDATE ""{args.TableName}""
                    SET ""ProjectedHatchCount"" = ROUND((""SetCount"" * ""ProjectedHatchRate"" / 100.0)::numeric, 0)::integer
                    WHERE ""SetCount"" > 0 AND ""ProjectedHatchRate"" > 0;
                    
                    UPDATE ""{args.TableName}""
                    SET ""ProjectedHatchCount"" = 0
                    WHERE ""ProjectedHatchCount"" IS NULL;
                END IF;
            END $$;";
        args.UpgradeScript(sql);
    }
}

Notes

  • The [PropertyAdded] attribute can be applied multiple times to the same method if the same migration logic applies to multiple properties.
  • PropertyAdded follows the tenant model. If the column is new for that tenant, the subscriber runs.
  • If feature trimming removes the column from the tenant model, there is no added-column event. If the column is added to the model later, the subscriber runs during that later schema upgrade.
  • Use args.TableName and args.ColumnName instead of hardcoding table/column names — the actual database names may differ from the entity/property names.
  • NOT NULL constraints are applied after all subscriber scripts run, so you can safely populate the column first.
  • If a new NOT NULL column is added without a PropertyAdded subscriber to populate it, the upgrade will fail when the constraint is applied.
  • Use WHERE ... IS NULL guards in UPDATE statements to avoid overwriting any values that may already exist.