Skip to main content

Fixed: "DbUpdateException: Could not save changes. Please configure your entity type accordingly." with EF Core and MySQL

· 12 min read
Jagdish Kumawat
Founder @ Dewiride

Four different mistakes, one message: Microsoft.EntityFrameworkCore.DbUpdateException: Could not save changes. Please configure your entity type accordingly. A duplicate name, a negative price, a missing category and a blocked delete all produced it, through Oracle's MySQL provider for EF Core. Nothing was wrong with any entity type's configuration. The real reason sits one line lower, in the inner MySqlException, and its error number is what your code should handle.

What I Was Doing​

In a .NET 10 Web API with EF Core 10, Oracle's MySql.EntityFrameworkCore 10.0.12 provider and MySQL 9.7, the database protects itself with constraints:

  • a unique index on categories.Name, so two categories can't share a name;
  • a CHECK constraint, CK_products_UnitPrice, so a price can't be negative;
  • a foreign key from products.CategoryId to categories.Id, with ON DELETE RESTRICT, so a product can't point at a missing category, and a category with products can't be deleted.

The API's endpoints saved through EF Core, with no error handling yet. The one for products:

src/Dewiride.Inventory.Api/Program.cs (temporary endpoint)
app.MapPost("/try/products", async (string sku, string name, int categoryId, decimal unitPrice, InventoryDbContext db) =>
{
var product = new Product
{
Sku = sku,
Name = name,
CategoryId = categoryId,
UnitPrice = unitPrice,
CreatedAtUtc = DateTime.UtcNow,
UpdatedAtUtc = DateTime.UtcNow,
};
db.Products.Add(product);
await db.SaveChangesAsync();
return Results.Ok(new { product.Id });
});

Then I sent these four requests:

Terminal
curl -s -X POST "http://localhost:5190/try/categories?name=Chargers"
curl -s -X POST "http://localhost:5190/try/products?sku=CHG-USBC-20W&name=USB-C%20charger,%2020%20W&categoryId=5&unitPrice=-15"
curl -s -X POST "http://localhost:5190/try/products?sku=CHG-USBC-30W&name=USB-C%20charger,%2030%20W&categoryId=99&unitPrice=15"
curl -s -X DELETE "http://localhost:5190/try/categories/5"

A category named Chargers already existed, and it had products; there is no category 99.

What I Expected​

For each request, a saved row or an error that says what was wrong with it.

What I Got​

Four times the same first line (the stack traces are cut here):

Output
Microsoft.EntityFrameworkCore.DbUpdateException: Could not save changes. Please configure your entity type accordingly.
---> MySql.Data.MySqlClient.MySqlException (0x80004005): Duplicate entry 'Chargers' for key 'categories.IX_categories_Name'

Microsoft.EntityFrameworkCore.DbUpdateException: Could not save changes. Please configure your entity type accordingly.
---> MySql.Data.MySqlClient.MySqlException (0x80004005): Check constraint 'CK_products_UnitPrice' is violated.

Microsoft.EntityFrameworkCore.DbUpdateException: Could not save changes. Please configure your entity type accordingly.
---> MySql.Data.MySqlClient.MySqlException (0x80004005): Cannot add or update a child row: a foreign key constraint fails (`inventory`.`products`, CONSTRAINT `FK_products_categories_CategoryId` FOREIGN KEY (`CategoryId`) REFERENCES `categories` (`Id`) ON DELETE RESTRICT)

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)

A terminal with four curl requests to the temporary endpoints, each answered with DbUpdateException: Could not save changes. Please configure your entity type accordingly, followed by a different inner MySqlException: duplicate entry, check constraint violated, child row foreign key failure and parent row foreign key failure.

"Please configure your entity type accordingly" sent me to the entity configuration first, but none of these four had anything to do with it: the configuration was what created the constraints that worked.

What I Checked​

The line with the arrow. In .NET's exception text, ---> introduces the inner exception, the one that caused the outer one. Each inner one is a MySqlException with MySQL's own message, and those four messages are four different problems. The API's log showed the same pair, from EF Core's Update category:

API log
fail: Microsoft.EntityFrameworkCore.Update[10000]
An exception occurred in the database while saving changes for context type 'Dewiride.Inventory.Data.InventoryDbContext'.
Microsoft.EntityFrameworkCore.DbUpdateException: Could not save changes. Please configure your entity type accordingly.
---> MySql.Data.MySqlClient.MySqlException (0x80004005): Duplicate entry 'Chargers' for key 'categories.IX_categories_Name'

What DbUpdateException means. Its API reference says only: "An exception that is thrown when an error is encountered while saving to the database." EF Core's cascade delete page describes the wrapping: "SaveChanges then wraps the database exception in a DbUpdateException." So the outer exception says that saving failed; the inner one says why.

Where the misleading sentence comes from. EF Core's own message for this case is different. It's listed in the API reference as RelationalStrings.UpdateStoreException: "An error occurred while saving the entity changes. See the inner exception for details." I searched both assemblies for the two texts with strings, which prints the readable text inside a binary file:

Terminal
strings ~/.nuget/packages/mysql.entityframeworkcore/10.0.12/lib/net10.0/MySql.EntityFrameworkCore.dll | grep 'Could not save changes'
strings ~/.nuget/packages/microsoft.entityframeworkcore.relational/10.0.14/lib/net10.0/Microsoft.EntityFrameworkCore.Relational.dll | grep 'See the inner exception for details'
Output
FCould not save changes. Please configure your entity type accordingly.
WAn error occurred while saving the entity changes. See the inner exception for details.

(The F and W are length bytes that .NET stores in front of each resource string: 70 and 87 characters.) The first text is in Oracle's provider, which uses it instead of EF Core's. EF Core's version gives the right advice: see the inner exception.

Why It Happens​

MySQL refused each statement, with an error number that identifies the reason; the MySQL connector raised it as a MySqlException; EF Core wrapped that in a DbUpdateException; and Oracle's provider gave the wrapper a generic message. The number is in the MySQL error reference:

Inner messageError numberSymbol in the reference
Duplicate entry … for key …1062ER_DUP_ENTRY
Check constraint … is violated.3819ER_CHECK_CONSTRAINT_VIOLATED
Cannot add or update a child row: a foreign key constraint fails …1452ER_NO_REFERENCED_ROW_2
Cannot delete or update a parent row: a foreign key constraint fails …1451ER_ROW_IS_REFERENCED_2

The Fix​

Catch DbUpdateException only for the MySQL errors you expect, and answer each with a clear message and the right status code. C#'s exception filter, catch (…) when (…), does exactly that: the catch block runs only when the condition is true, and every other error keeps flowing up as before.

src/Dewiride.Inventory.Api/Program.cs (the products endpoint, with handling)
db.Products.Add(product);
try
{
await db.SaveChangesAsync();
}
catch (DbUpdateException ex) when (ex.InnerException is MySqlException { Number: 3819 })
{
// 3819: a CHECK constraint refused the row.
return Results.BadRequest(new { error = "Prices and quantities can't be negative." });
}
catch (DbUpdateException ex) when (ex.InnerException is MySqlException { Number: 1452 })
{
// 1452: the foreign key found no category with this Id.
return Results.BadRequest(new { error = $"There is no category with Id {categoryId}." });
}

return Results.Ok(new { product.Id });
  • ex.InnerException is MySqlException { Number: 3819 } is a pattern: it's true when the inner exception is a MySqlException whose Number is 3819. MySqlException is in using MySql.Data.MySqlClient;.
  • Match the number, not the message. The message contains values and names that change from row to row; the number is the error's identity.
  • Write your own message. MySQL's text names tables, constraints and values. That's useful in your logs, but it's a description of your schema that API clients don't need.

The other two endpoints follow the same pattern:

src/Dewiride.Inventory.Api/Program.cs (excerpts)
// POST /try/categories
catch (DbUpdateException ex) when (ex.InnerException is MySqlException { Number: 1062 })
{
// 1062: a unique index refused a duplicate value.
return Results.Conflict(new { error = $"A category named '{name}' already exists." });
}

// DELETE /try/categories/{id}
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." });
}

A duplicate and a blocked delete are conflicts with what's already stored, so they get 409 Conflict; a negative price and an unknown category are bad input, so they get 400 Bad Request.

After the Fix​

I ran the same kinds of request against the handled endpoints. The database held different rows by then: two categories, Desks with Id 1 and Shelves with Id 2. The first request gives Desks a product; -i prints the status line and headers:

Terminal
curl -s -i -X POST "http://localhost:5190/try/products?sku=DSK-STD-01&name=Standing%20desk&categoryId=1&unitPrice=349"
curl -s -i -X POST "http://localhost:5190/try/categories?name=desks"
curl -s -i -X POST "http://localhost:5190/try/products?sku=DSK-STD-02&name=Desk%20mat&categoryId=1&unitPrice=-5"
curl -s -i -X POST "http://localhost:5190/try/products?sku=DSK-STD-03&name=Desk%20lamp&categoryId=99&unitPrice=25"
curl -s -i -X DELETE "http://localhost:5190/try/categories/1"
curl -s -i -X DELETE "http://localhost:5190/try/categories/2"
Output
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"id":1}

HTTP/1.1 409 Conflict
Content-Type: application/json; charset=utf-8

{"error":"A category named 'desks' already exists."}

HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8

{"error":"Prices and quantities can't be negative."}

HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8

{"error":"There is no category with Id 99."}

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

A terminal where the handled endpoints answer a duplicate category with 409 Conflict, a negative price and an unknown category with 400 Bad Request, a category that still has products with 409 Conflict, and an empty category's delete with 204 No Content.

Each mistake now gets its own answer, a valid product is still saved, and deleting Shelves, which has no products, still works. desks was a duplicate of Desks because the column's collation ignores case; the 1062 post covers that.

Common Mistakes​

MistakeWhat happensDo this instead
Changing the entity configuration because the message says soNothing improves; the cause is in the dataRead the inner exception
catch (DbUpdateException) with no filterUnexpected errors are hidden behind a friendly messageFilter on the error numbers you expect
Matching ex.InnerException.Message.Contains("Duplicate")Breaks when the wording or language changesMatch MySqlException.Number
Returning MySQL's message to the clientIt exposes table, index and constraint namesReturn your own message; keep MySQL's for the logs
Checking for duplicates with a query before saving, without a unique indexTwo requests at the same moment both pass the checkKeep the index, and handle 1062

Troubleshooting​

SymptomLikely causeFix
DbUpdateException: Could not save changes. Please configure your entity type accordingly.MySQL refused the statement; Oracle's provider uses this text for every failed saveRead the inner MySqlException and its Number
The inner message starts with Duplicate entryA unique index refused a duplicate value (1062)Return 409 Conflict, or update the existing row
The inner message is Check constraint … is violated.A CHECK constraint refused the row (3819)Validate the input, and return 400 Bad Request
The inner message starts with Cannot add or update a child rowThe foreign key found no parent row (1452)Check that the Id exists, and return 400 Bad Request
The inner message starts with Cannot delete or update a parent rowChild rows still point at the row (1451)Move or archive the children first, and return 409 Conflict

FAQ​

Is something wrong with my entity configuration?​

Not for these errors. The configuration created the index, the CHECK constraint and the foreign key that refused the data; they worked. Look at the inner exception's message and number.

Should I validate input before saving instead?​

Both. Validation gives better messages for the common cases, such as a negative price, before anything reaches MySQL. The constraints and these catches cover what validation can't see, such as a duplicate created by another request a millisecond earlier.

Where do I find the number for another error?​

In the inner exception's message and in the MySQL server error reference, which lists every error as "Error number: …; Symbol: …; SQLSTATE: …".

Conclusion​

"Could not save changes. Please configure your entity type accordingly." is the generic message that Oracle's MySQL provider puts on every DbUpdateException; EF Core's own wording, "See the inner exception for details", is the better advice. The inner MySqlException carries MySQL's real message and an error number. Catch DbUpdateException with a when filter on MySqlException.Number for the errors your constraints produce, 1062, 3819, 1452 and 1451 here, and answer each with your own message and the right status code.

Additional Resources​

Stay Updated

Subscribe to our newsletter for the latest tutorials, tech insights, and developer news.

By subscribing, you agree to our privacy policy. Unsubscribe at any time.