EntityDeleted Event
Summary
The EntityDeleted event fires during database upgrade when a table exists in the current database but is no longer present in the EF Core model. Use it to migrate data from the table being removed to other tables before it is marked deleted.
When does it fire?
EntityDeleted fires when the schema comparison detects a table in the live database that does not exist in the new EF Core model. The subscriber runs before the table is marked deleted, so data can still be read from it.
Schema comparison → Table removed detected → EntityDeleted subscribers → Table marked deleted
Syntax
[EntityDeleted("EntityName")]
public void MethodName(EntityDeletedEventArgs args)
{
args.UpgradeScript("SQL script here");
}
Attribute
| Parameter | Type | Description |
|---|---|---|
entityName |
string |
The entity (table) name being removed |
Event Args (EntityDeletedEventArgs)
| Property/Method | Description |
|---|---|
TableName |
The database table 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 table
When the Feature table is removed and replaced by __FeatureVersion, read the existing data and insert it into the new table.
public class DataUpgrade
{
[EntityDeleted("Feature")]
public void OnFeaturesTableDeleted(EntityDeletedEventArgs args)
{
var features = args.ReadFromDatabase(
"""SELECT "Id", "Name", "DemoDataVersion", "BlankDataVersion" FROM "Feature" """);
foreach (DataRow row in features.Rows)
{
var id = row["Id"];
var name = row["Name"];
var demoDataVersion = row["DemoDataVersion"];
var blankDataVersion = row["BlankDataVersion"];
args.UpgradeScript($"""
INSERT INTO "__FeatureVersion" ("Id", "Name", "Version", "DemoDataVersion", "BlankDataVersion", "UpdatedAt", "UpdatedByMethod")
SELECT '{id}', '{name}', 0, {demoDataVersion}, {blankDataVersion}, NOW(), 'MigratedFromOldFeaturesTable'
WHERE NOT EXISTS (SELECT 1 FROM "__FeatureVersion" WHERE "Name" = '{name}')
""");
}
}
}
2. Archiving all data before table removal
When a table is being retired, copy its data to an archive table.
public class DataBaseUpgrade
{
[EntityDeleted("LegacyOrder")]
public void ArchiveLegacyOrders(EntityDeletedEventArgs args)
{
args.UpgradeScript($@"
INSERT INTO ""ArchivedOrders"" (""OriginalGuid"", ""Data"", ""ArchivedAt"")
SELECT ""Guid"", row_to_json(""{args.TableName}"")::text, NOW()
FROM ""{args.TableName}"";");
}
}
3. Moving child records to a new parent before deletion
When a junction table is being removed, reassign the relationships.
public class DataBaseUpgrade
{
[EntityDeleted("ProductCategoryLink")]
public void MigrateCategoryLinks(EntityDeletedEventArgs args)
{
args.UpgradeScript($@"
UPDATE ""Product""
SET ""CategoryGuid"" = pcl.""CategoryGuid""
FROM ""{args.TableName}"" AS pcl
WHERE ""Product"".""Guid"" = pcl.""ProductGuid""
AND ""Product"".""CategoryGuid"" IS NULL;");
}
}
Notes
- The
[EntityDeleted]attribute takes a string entity name (not a Type), since the entity class may no longer exist in the codebase. - The subscriber runs before the table is marked deleted, so you can still query data from it using
ReadFromDatabase. - Use
ReadFromDatabasefor row-by-row data migration andUpgradeScriptfor bulk SQL operations. - The table 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 working during a rolling deploy. Foreign keys referencing the table are dropped at mark time. - If the table returns to the model (rollback, channel move), the mark is cleared and the table is restored; dropped foreign keys are re-created in the same sync run.
- See Safe and Dangerous Schema Changes for which model edits are reversible and which lose data.
- EntityDeleted is typically used together with EntityAdded when replacing one table with another.