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 dropped.

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 dropped, so data can still be read from it.

Schema comparison → Table removed detected → EntityDeleted subscribers → Table dropped

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 dropped, so you can still query data from it using ReadFromDatabase.
  • Use ReadFromDatabase for row-by-row data migration and UpgradeScript for bulk SQL operations.
  • If no subscriber handles the entity deletion, the table is simply dropped and all data is lost.
  • EntityDeleted is typically used together with EntityAdded when replacing one table with another.