How to Choose What Happens on Delete in EF Core: Cascade or Restrict
What should happen to a category's products when the category is deleted? EF Core answers for you, unless you answer first: for a required relationship, its default is to delete the products too, silently. This guide shows that default happening in MySQL, switches it to Restrict so the database refuses instead, and shows the two different errors you get afterwards, one from MySQL and one from EF Core itself.
The Choices
When you delete a row that other rows point to, three things can happen to those other rows: delete them too (cascade), clear their pointer (set null), or refuse the delete (restrict). EF Core sets this with OnDelete and a DeleteBehavior value, and it writes the choice into the database's foreign key as ON DELETE ….
EF Core's cascade delete page gives the defaults: "Required relationships are configured to use cascade deletes by default", and "The default for optional relationships like this is ClientSetNull." A relationship is required when the foreign key can't be empty, such as an int CategoryId.
Prerequisites
- A .NET C# project using EF Core with MySQL. The examples use EF Core 10 with Oracle's
MySql.EntityFrameworkCoreand MySQL 9.7, from How to Connect a .NET C# Web API to a MySQL Database — Part 1. - The EF Core tools: How to Install dotnet-ef as a Local Tool in a .NET C# Project.
Step 1: See the Default, Cascade
A product belongs to a category, and the foreign key is a non-nullable int:
public int CategoryId { get; set; }
public Category Category { get; set; } = null!;
With no configuration, EF Core's first migration created the foreign key with cascade:
table.ForeignKey(
name: "FK_Products_Categories_CategoryId",
column: x => x.CategoryId,
principalTable: "Categories",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
What that means in practice, from the mysql client. A category with two products, then the category is deleted (the tables still had EF Core's default names, Categories and Products, at this point):
INSERT INTO Categories (Name, CreatedAtUtc) VALUES ('Cables', UTC_TIMESTAMP(6));
INSERT INTO Products (Sku, Name, CategoryId, UnitPrice, QuantityOnHand, ReorderLevel, IsArchived, CreatedAtUtc, UpdatedAtUtc)
VALUES ('CBL-USBC-1M', 'USB-C cable, 1 m', 1, 9.99, 25, 5, 0, UTC_TIMESTAMP(6), UTC_TIMESTAMP(6)),
('CBL-USBC-2M', 'USB-C cable, 2 m', 1, 12.49, 10, 5, 0, UTC_TIMESTAMP(6), UTC_TIMESTAMP(6));
DELETE FROM Categories WHERE Id = 1;
SELECT COUNT(*) FROM Products;
Query OK, 1 row affected (0.003 sec)
+----------+
| COUNT(*) |
+----------+
| 0 |
+----------+
1 row in set (0.000 sec)

MySQL reported 1 row affected, the category, and the two products were gone too. Nothing warned anyone. For data like products, which have stock, prices and history, that's rarely what you want.
Step 2: Switch to Restrict
Configure the relationship explicitly, with OnDelete(DeleteBehavior.Restrict):
builder.HasOne(p => p.Category)
.WithMany(c => c.Products)
.HasForeignKey(p => p.CategoryId)
.OnDelete(DeleteBehavior.Restrict);
This reads like a sentence: a product has one category, a category has many products, CategoryId holds the link, and deleting a category that still has products is refused. EF Core's one-to-many page shows this exact use: "It's possible to configure EF to throw an exception instead of automatically deleting dependent rows that can no longer exist".
Create and apply the migration:
dotnet ef migrations add RestrictCategoryDelete --project src/Dewiride.Inventory.Data --startup-project src/Dewiride.Inventory.Api
dotnet ef database update --project src/Dewiride.Inventory.Data --startup-project src/Dewiride.Inventory.Api
migrationBuilder.DropForeignKey(
name: "FK_products_categories_CategoryId",
table: "products");
migrationBuilder.AddForeignKey(
name: "FK_products_categories_CategoryId",
table: "products",
column: "CategoryId",
principalTable: "categories",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
Applying migration '20261103044319_RestrictCategoryDelete'.
ALTER TABLE `products` DROP CONSTRAINT `FK_products_categories_CategoryId`;
ALTER TABLE `products` ADD CONSTRAINT `FK_products_categories_CategoryId` FOREIGN KEY (`CategoryId`) REFERENCES `categories` (`Id`) ON DELETE RESTRICT;
Done.
MySQL can't change a foreign key's action in place, so the migration drops it and adds it again with ON DELETE RESTRICT. On a table with a lot of data, that takes a moment, during which the foreign key isn't checked.
Step 3: See the Two Refusals
From MySQL, when the products aren't loaded in the DbContext. Deleting the Adapters category, which has two products:
DELETE FROM categories WHERE Id = 2;
ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key constraint fails (`inventory`.`products`, CONSTRAINT `FK_products_categories_CategoryId` FOREIGN KEY (`CategoryId`) REFERENCES `categories` (`Id`) ON DELETE RESTRICT)
Through EF Core, the same refusal arrives as a DbUpdateException from SaveChangesAsync, which the docs predict: "SaveChanges then wraps the database exception in a DbUpdateException." Its first lines, from a temporary API endpoint that loads a category and removes it:
Microsoft.EntityFrameworkCore.DbUpdateException: Could not save changes. Please configure your entity type accordingly.
---> MySql.Data.MySqlClient.MySqlException (0x80004005): Cannot delete or update a parent row: a foreign key constraint fails (`inventory`.`products`, CONSTRAINT `FK_products_categories_CategoryId` FOREIGN KEY (`CategoryId`) REFERENCES `categories` (`Id`) ON DELETE RESTRICT)
From EF Core itself, when the products are loaded. The same endpoint with Include(c => c.Products):
System.InvalidOperationException: The association between entity types 'Category' and 'Product' has been severed, but the relationship is either marked as required or is implicitly required because the foreign key is not nullable. If the dependent/child entity should be deleted when a required relationship is severed, configure the relationship to use cascade deletes. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the key values.

EF Core noticed in memory that the loaded products would lose their required category, and refused before sending anything to MySQL. The docs explain both cases: "EF Core always applies configured cascading behaviors to tracked entities", and with a non-cascade behavior on a required relationship, "Typically, this is an InvalidOperationException from EF Core since the invalid state is detected in the loaded children/dependents." The severed-association error has its own post, and so does MySQL's error 1451, including how to return 409 Conflict from an API.
Which to Choose
DeleteBehavior | Database | Use it when |
|---|---|---|
Cascade | ON DELETE CASCADE | The children can't exist without the parent and have no value on their own, such as an order's lines |
Restrict | ON DELETE RESTRICT | The children matter on their own, such as products in a category |
SetNull | ON DELETE SET NULL | The children can exist without a parent; the foreign key must be nullable |
ClientSetNull (default for optional) | No cascade in the database | Like SetNull, but only for loaded entities |
The docs make a point about the database side: "The only values that will cause cascading behaviors on the database are Cascade and SetNull. All other values will configure the database to not cascade any changes."
Common Mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
| Leaving the default on every relationship | Deleting one row can silently delete many | Choose OnDelete for each relationship |
SetNull on a non-nullable foreign key | MySQL refuses the foreign key | Make the foreign key nullable, or use Restrict |
| Expecting the same error whether children are loaded or not | Sometimes InvalidOperationException, sometimes DbUpdateException | Handle both |
| Relying on cascade for important data | One mistaken delete loses the history | Archive instead of deleting |
FAQ
Do I need to configure every relationship?
Only the ones where the default is wrong. Required relationships cascade by default, so review each one where the children matter.
What's the difference between Restrict and NoAction?
In MySQL, none in practice. EF Core's docs note that "The behaviors of ON DELETE NO ACTION (the database default) and ON DELETE RESTRICT in relational databases are typically either identical or very similar."
Conclusion
EF Core gives a required relationship ON DELETE CASCADE unless you choose otherwise, and in MySQL that silently deletes the children with the parent. OnDelete(DeleteBehavior.Restrict) makes the database refuse instead. After the change, a delete with children fails in one of two ways: DbUpdateException wrapping MySQL's error 1451 when the children aren't loaded, or EF Core's own InvalidOperationException when they are. Choose per relationship, and keep cascade for children that mean nothing without their parent.
Additional Resources
- Cascade Delete — EF Core's delete behaviors and defaults.
- One-to-many relationships — configuring
HasOne,WithManyandOnDelete. - FOREIGN KEY Constraints — MySQL's referential actions.
