PropertyDeleted Event
Summary
The PropertyDeleted event fires during database upgrade when a column that exists in the current database is no longer present in the EF Core model. Use it to migrate data from the column being removed to another column or table before the column is marked deleted.
When does it fire?
PropertyDeleted fires when the schema comparison detects a column in the live database that does not exist in the new EF Core model. The subscriber runs before the column is marked deleted, so data can still be read from it.
Schema comparison → Column removed detected → PropertyDeleted subscribers → Column marked deleted
Syntax
[PropertyDeleted(typeof(EntityType), "PropertyName")]
public void MethodName(PropertyRemovedEventArgs args)
{
args.UpgradeScript("SQL script here");
}
Attribute
| Parameter | Type | Description |
|---|---|---|
entityType |
Type |
The entity type (used as typeof(Entity)) |
propertyName |
string |
The property (column) name being removed |
Event Args (PropertyRemovedEventArgs)
| Property/Method | Description |
|---|---|
TableName |
The database table name |
ColumnName |
The database column name being removed |
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. Migrating data to a replacement column
When an old Price column is replaced by UnitPrice, copy the data before Price is marked deleted.
public class DataBaseUpgrade
{
[PropertyDeleted(typeof(Product), "Price")]
public void MigratePriceToUnitPrice(PropertyRemovedEventArgs args)
{
var sql = $@"
UPDATE ""{args.TableName}""
SET ""UnitPrice"" = ""{args.ColumnName}""
WHERE ""UnitPrice"" IS NULL OR ""UnitPrice"" = 0;";
args.UpgradeScript(sql);
}
}
2. Archiving data before column removal
When a column is being removed, save the data to an audit table before it is marked deleted.
public class DataBaseUpgrade
{
[PropertyDeleted(typeof(Customer), "LegacyCode")]
public void ArchiveLegacyCode(PropertyRemovedEventArgs args)
{
var sql = $@"
INSERT INTO ""AuditLog"" (""TableName"", ""ColumnName"", ""EntityGuid"", ""OldValue"", ""ArchivedAt"")
SELECT '{args.TableName}', '{args.ColumnName}', ""Guid"", ""{args.ColumnName}""::text, NOW()
FROM ""{args.TableName}""
WHERE ""{args.ColumnName}"" IS NOT NULL;";
args.UpgradeScript(sql);
}
}
3. Reading data to inform migration logic
Use ReadFromDatabase to inspect existing data before generating the migration script.
public class DataBaseUpgrade
{
[PropertyDeleted(typeof(SalesOrder), "OldStatus")]
public void MigrateOldStatusValues(PropertyRemovedEventArgs args)
{
var data = args.ReadFromDatabase(
$"SELECT DISTINCT \"{args.ColumnName}\" FROM \"{args.TableName}\" WHERE \"{args.ColumnName}\" IS NOT NULL");
foreach (System.Data.DataRow row in data.Rows)
{
var oldValue = row[args.ColumnName]?.ToString();
var newValue = oldValue == "InProgress" ? "Active" : "Closed";
args.UpgradeScript(
$"UPDATE \"{args.TableName}\" SET \"Status\" = '{newValue}' WHERE \"{args.ColumnName}\" = '{oldValue}';");
}
}
}
Notes
- The
[PropertyDeleted]attribute takes aTypeparameter (not a string name) for the entity, unlike[PropertyAdded]which takes a string name. - The subscriber runs before the column is marked deleted, so you can still read from the column being removed.
- Use
ReadFromDatabasewhen you need to inspect the existing data to generate appropriate migration scripts. - The column is not dropped: it is marked deleted in place with a PostgreSQL comment (
benevia:deleted <utc-timestamp>). Name and data stay, so server versions still using the old model keep reading and writing it during a rolling deploy. - If the column returns to the model (rollback, channel move), the mark is cleared and the column is restored: NULL rows written while it was marked are backfilled with the type default, and NOT NULL is reinstated when the model requires it. Types without a usable default (e.g.
uuid,jsonb) skip the backfill, so a NOT NULL violation surfaces clearly instead of silently. - Dangerous changes — moving data between columns, making a column required, shrinking types — are not automated; handle them with subscribers like the ones above. See Safe and Dangerous Schema Changes.