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);
}
}
4. Populating a new column from a related table
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. PropertyAddedfollows 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.TableNameandargs.ColumnNameinstead 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
PropertyAddedsubscriber to populate it, the upgrade will fail when the constraint is applied. - Use
WHERE ... IS NULLguards in UPDATE statements to avoid overwriting any values that may already exist.