Fixed: "The association between entity types 'Category' and 'Product' has been severed, but the relationship is either marked as required" in EF Core
Deleting a category through EF Core worked as designed until the request loaded the category's products first. Then the delete failed with 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. It came from Remove(), before any SQL ran, so the error handling around SaveChangesAsync never saw it.
What I Was Doing
In a .NET 10 Web API with EF Core 10 and MySQL 9.7, every product belongs to a category. Product.CategoryId is an int, so the relationship is required: a product can't exist without a category. And a category that still has products must not be deleted, so the relationship uses Restrict:
builder.HasOne(p => p.Category)
.WithMany(c => c.Products)
.HasForeignKey(p => p.CategoryId)
.OnDelete(DeleteBehavior.Restrict);
In MySQL, that's a foreign key with ON DELETE RESTRICT. A temporary endpoint deletes a category, optionally loading its products first with Include, and turns MySQL's refusal, error 1451, into 409 Conflict:
app.MapDelete("/try/categories/{id:int}", async (int id, bool? withProducts, InventoryDbContext db) =>
{
IQueryable<Category> query = withProducts == true ? db.Categories.Include(c => c.Products) : db.Categories;
var category = await query.SingleAsync(c => c.Id == id);
db.Categories.Remove(category);
try
{
await db.SaveChangesAsync();
}
catch (DbUpdateException ex) when (ex.InnerException is MySqlException { Number: 1451 })
{
// 1451: products still point at this category.
return Results.Conflict(new { error = "This category still has products. Move or archive them first." });
}
return Results.NoContent();
});
Without Include, deleting a category that has products returned the 409, as planned. Then I sent the same delete with the products loaded, as a page that shows a category with its products would. Category 1, Desks, had two products:
curl -s -X DELETE "http://localhost:5190/try/categories/1?withProducts=true"
What I Expected
The same 409 Conflict: the products are still there, whether or not they're loaded.
What I Got
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.
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.HandleConceptualNulls(Boolean sensitiveLoggingEnabled, Boolean force, Boolean isCascadeDelete)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.CascadeDelete(InternalEntityEntry entry, Boolean force, IEnumerable`1 foreignKeys)

An unhandled exception instead of the 409, although the endpoint has a catch for exactly this situation.
What I Checked
Where it was thrown. The API's log has the full stack trace. Its last frames before my code:
at Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry.set_State(EntityState value)
at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.Remove(TEntity entity)
at Program.<>c.<<<Main>$>b__0_5>d.MoveNext() in /Users/you/projects/dewiride-inventory-api/src/Dewiride.Inventory.Api/Program.cs:line 127
Line 127 is db.Categories.Remove(category);. The exception came from Remove, one line before the try, and it's an InvalidOperationException, not the DbUpdateException the catch filters on. The log also showed no DELETE statement at all: MySQL was never asked.
What the documentation says. EF Core's cascade delete page covers this case, a required relationship with the children loaded: "Using anything other than cascade delete for required relationships will result in an exception when SaveChanges is called." And which exception: "Typically, this is an InvalidOperationException from EF Core since the invalid state is detected in the loaded children/dependents."
The page says "when SaveChanges is called", but my stack trace shows Remove. The EF Core 3.0 breaking changes explain the difference: "Starting with 3.0, EF Core applies cascading actions as soon as the triggering condition is detected. For example, calling context.Remove() to delete a principal entity will result in all tracked related required dependents also being set to Deleted immediately." The default timing, ChangeTracker.CascadeDeleteTiming, is Immediate.
Why It Happens
When Remove marks the category as deleted, EF Core has to decide, right away, what happens to the products it's tracking in memory:
- With
Cascade, it would mark them deleted too. - With
Restrict, it may not delete them, and it can't clear theirCategoryIdeither, because anintcan't be null.
There's no valid state left for the loaded products, so EF Core throws. The stack trace's HandleConceptualNulls names that state: a required foreign key that would have to become null. Without Include, EF Core tracks no products, so it has nothing to check, and the database's ON DELETE RESTRICT refuses instead, with error 1451. Same rule, two different places that enforce it, two different exceptions.
The Fix
The message suggests cascade deletes, but that's only right when the products should disappear with their category. Decide what the products need:
| The products should… | Do this |
|---|---|
| be deleted with the category | OnDelete(DeleteBehavior.Cascade) and a migration that changes the foreign key to ON DELETE CASCADE |
| stay, and block the delete | Check for products before removing the category, and answer 409 |
| move to another category | Change their CategoryId, save, then delete the category |
Here the products must stay, so the endpoint checks first:
app.MapDelete("/try/categories/{id:int}", async (int id, bool? withProducts, InventoryDbContext db) =>
{
// A category that still has products can't be deleted: say so before EF Core or MySQL refuses.
if (await db.Products.AnyAsync(p => p.CategoryId == id))
{
return Results.Conflict(new { error = "This category still has products. Move or archive them first." });
}
IQueryable<Category> query = withProducts == true ? db.Categories.Include(c => c.Products) : db.Categories;
var category = await query.SingleAsync(c => c.Id == id);
db.Categories.Remove(category);
try
{
await db.SaveChangesAsync();
}
catch (DbUpdateException ex) when (ex.InnerException is MySqlException { Number: 1451 })
{
// 1451: a product was added to this category after the check above.
return Results.Conflict(new { error = "This category still has products. Move or archive them first." });
}
return Results.NoContent();
});
-
AnyAsync(p => p.CategoryId == id)asks MySQL whether any product uses the category, without loading the products. EF Core sends a single small query:SQL from the API's logSELECT EXISTS (SELECT 1FROM `products` AS `p`WHERE `p`.`CategoryId` = @id) -
The check runs before anything is loaded or removed, so it works the same with or without
Include, and EF Core never reaches the invalid state. -
The 1451 catch stays. Another request could add a product between the check and the delete; the foreign key still catches that, and the endpoint answers the same 409.
After the Fix
The two deletes for category 1, with and without the products loaded, and one for category 7, which has no products (-i prints the status line; the Date and Server headers are left out here):
curl -s -i -X DELETE "http://localhost:5190/try/categories/1?withProducts=true"
curl -s -i -X DELETE "http://localhost:5190/try/categories/1"
curl -s -i -X DELETE "http://localhost:5190/try/categories/7?withProducts=true"
HTTP/1.1 409 Conflict
Content-Type: application/json; charset=utf-8
{"error":"This category still has products. Move or archive them first."}
HTTP/1.1 409 Conflict
Content-Type: application/json; charset=utf-8
{"error":"This category still has products. Move or archive them first."}
HTTP/1.1 204 No Content

Both requests for category 1 now get the same answer, and an empty category is still deleted, with Include and all: for category 7, the log shows the check, the query that loads the category, then DELETE FROM categories WHERE Id = @p0;.
Common Mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
Switching to Cascade because the message suggests it | Deleting a category silently deletes its products | Choose the behaviour from what the data needs |
Wrapping only SaveChangesAsync in try | The exception from Remove escapes | Check first, so the invalid state never happens |
Catching InvalidOperationException around Remove | Hides real bugs that throw the same type | Prevent the state with the check |
Making CategoryId nullable to get rid of the error | Products can now exist without a category | Keep the relationship required |
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
The association between entity types … has been severed when deleting a parent | Its children are loaded, and the required relationship uses Restrict | Check for children before removing the parent |
The exception comes from Remove, not from SaveChangesAsync | EF Core applies cascade rules as soon as an entity is marked deleted | Put the check before Remove; a try around SaveChangesAsync can't catch it |
The same delete returns 409 without Include and throws with it | Without loaded children, MySQL's foreign key refuses instead, with error 1451 | Use one check for both paths |
FAQ
Why does the message mention EnableSensitiveDataLogging?
With sensitive data logging turned on, EF Core puts the key values into messages like this one, so you can see which rows were involved. It also logs every parameter value, so use it only on your own machine; the check in the fix makes it unnecessary here.
Does this happen with optional relationships?
Not in the same way: when the foreign key is nullable, EF Core's default for optional relationships is to set it to null in the loaded children. Whether that's what you want is a separate decision.
Conclusion
"The association between entity types … has been severed, but the relationship is either marked as required…" means you removed a parent while EF Core was tracking children that can't live without it, and the relationship doesn't cascade. EF Core throws from Remove itself, before any SQL, so error handling around SaveChangesAsync misses it. Decide what should happen to the children: cascade them, move them, or, as here, check for them first and refuse with a clear 409 while the database's foreign key covers the rare race.
Additional Resources
- Cascade Delete — what each
DeleteBehaviordoes to tracked entities and in the database. - Breaking changes in EF Core 3.x: Cascade deletions now happen immediately by default
- Changing Foreign Keys and Navigations — severing, orphans and re-parenting.
