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 ReadFromDatabase for row-by-row data migration and UpgradeScript for 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.