Skip to main content

How to Add a CHECK Constraint with EF Core and MySQL

· 7 min read
Jagdish Kumawat
Founder @ Dewiride

Validation in your API stops most bad data, but not data from a script, a second app, or a bug that skips the validation. A CHECK constraint makes the database itself refuse a row that breaks a rule, such as a negative price. This guide adds three to a products table from EF Core's model, applies them with a migration, and proves MySQL enforces them on both inserts and updates.

What a CHECK Constraint Is​

EF Core's docs describe it: "Check constraints are a standard relational feature that allows you to define a condition that must hold for all rows in a table; any attempt to insert or modify data that violates the constraint will fail." MySQL has enforced them since 8.0.16; before that it accepted the syntax and ignored it.

Prerequisites​

Step 1: Add the Constraints to the Model​

Check constraints belong to a table, so EF Core configures them inside ToTable. In the product's configuration:

src/Dewiride.Inventory.Data/Configurations/ProductConfiguration.cs (excerpt)
builder.ToTable("products", table =>
{
// MySQL checks these on every INSERT and UPDATE, whatever program sends them.
table.HasCheckConstraint("CK_products_UnitPrice", "`UnitPrice` >= 0");
table.HasCheckConstraint("CK_products_QuantityOnHand", "`QuantityOnHand` >= 0");
table.HasCheckConstraint("CK_products_ReorderLevel", "`ReorderLevel` >= 0");
});

The ProductConfiguration ToTable call in a macOS-style code window, with three HasCheckConstraint rules keeping UnitPrice, QuantityOnHand and ReorderLevel at zero or above.

Each HasCheckConstraint takes two arguments:

  • A name, here CK_<table>_<column>. In MySQL, CHECK constraint names must be unique in the whole database, not just the table, so including the table name avoids clashes.
  • A SQL condition, written in MySQL's SQL. EF Core passes it through as-is: "You can use the Fluent API to specify a check constraint on a table, provided as a SQL expression". Quote column names with backticks, MySQL's quoting. EF Core's own example uses [Price], SQL Server's style, which MySQL doesn't accept.

As the docs say, "Multiple check constraints can be defined on the same table, each with their own name."

Step 2: Create the Migration​

Terminal
dotnet ef migrations add AddProductCheckConstraints --project src/Dewiride.Inventory.Data --startup-project src/Dewiride.Inventory.Api
Migrations/…_AddProductCheckConstraints.cs (Up)
migrationBuilder.AddCheckConstraint(
name: "CK_products_QuantityOnHand",
table: "products",
sql: "`QuantityOnHand` >= 0");

migrationBuilder.AddCheckConstraint(
name: "CK_products_ReorderLevel",
table: "products",
sql: "`ReorderLevel` >= 0");

migrationBuilder.AddCheckConstraint(
name: "CK_products_UnitPrice",
table: "products",
sql: "`UnitPrice` >= 0");

One AddCheckConstraint per rule, in alphabetical order of name.

Step 3: Apply It, After Checking the Data​

Adding a constraint checks the rows already in the table, and MySQL refuses to add a rule the table already breaks. On a table with data, look for offenders first, with the opposite of each rule:

mysql>
SELECT Id, Sku, QuantityOnHand FROM products
WHERE QuantityOnHand < 0 OR UnitPrice < 0 OR ReorderLevel < 0;

On this table, one product had -3 in stock, and the first attempt to apply the migration failed with Check constraint 'CK_products_QuantityOnHand' is violated. Fixed: ERROR 3819 (HY000): Check constraint … is violated walks through finding and correcting such rows. With the data fixed:

Terminal
dotnet ef database update --project src/Dewiride.Inventory.Data --startup-project src/Dewiride.Inventory.Api
Output (the lines that matter)
Applying migration '20261103044219_AddProductCheckConstraints'.
ALTER TABLE `products` ADD CONSTRAINT `CK_products_QuantityOnHand` CHECK (`QuantityOnHand` >= 0);
ALTER TABLE `products` ADD CONSTRAINT `CK_products_ReorderLevel` CHECK (`ReorderLevel` >= 0);
ALTER TABLE `products` ADD CONSTRAINT `CK_products_UnitPrice` CHECK (`UnitPrice` >= 0);
Done.

Step 4: Prove MySQL Enforces Them​

From the mysql client, try a negative price on insert, and an update that would take more stock than there is:

mysql>
INSERT INTO products (Sku, Name, CategoryId, UnitPrice, QuantityOnHand, ReorderLevel, IsArchived, CreatedAtUtc, UpdatedAtUtc)
VALUES ('CHG-USBC-20W', 'USB-C charger, 20 W', 5, -15.00, 6, 2, 0, UTC_TIMESTAMP(6), UTC_TIMESTAMP(6));
UPDATE products SET QuantityOnHand = QuantityOnHand - 10 WHERE Sku = 'ADP-HDMI-01';
Output
ERROR 3819 (HY000): Check constraint 'CK_products_UnitPrice' is violated.
ERROR 3819 (HY000): Check constraint 'CK_products_QuantityOnHand' is violated.

The mysql client where an INSERT with a negative price fails on CK_products_UnitPrice, an UPDATE that would make stock negative fails on CK_products_QuantityOnHand, and INFORMATION_SCHEMA.CHECK_CONSTRAINTS lists the three constraints.

The update case is the valuable one: product ADP-HDMI-01 had 8 in stock, and no program, however buggy, can now make it negative. From a .NET C# API, the same refusal arrives as a DbUpdateException with an inner MySqlException whose Number is 3819; the fixes post shows how to turn it into a 400 Bad Request.

MySQL also lists what it enforces:

mysql>
SELECT CONSTRAINT_NAME, CHECK_CLAUSE FROM information_schema.CHECK_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = 'inventory';
Output
+----------------------------+-------------------------+
| CONSTRAINT_NAME | CHECK_CLAUSE |
+----------------------------+-------------------------+
| CK_products_QuantityOnHand | (`QuantityOnHand` >= 0) |
| CK_products_ReorderLevel | (`ReorderLevel` >= 0) |
| CK_products_UnitPrice | (`UnitPrice` >= 0) |
+----------------------------+-------------------------+
3 rows in set (0.001 sec)

How to List the CHECK Constraints in MySQL adds the table names and whether each is enforced.

Common Mistakes​

MistakeWhat happensDo this instead
Square brackets from SQL Server examplesMySQL rejects the SQL when the migration runsUse backticks
Reusing a constraint name on another tableMySQL refuses: names are unique per databaseInclude the table name, CK_<table>_<column>
Adding constraints without checking existing rowsThe migration fails on the first offending rowRun the opposite query first
Relying on the constraint instead of validationUsers get a database error instead of a clear messageValidate in the API too; keep the constraint as the last line of defence

Troubleshooting​

SymptomLikely causeFix
Check constraint '…' is violated when applying the migrationExisting rows break the ruleCorrect the rows, then apply again
The constraint exists but nothing is refusedIt was created as NOT ENFORCED, or MySQL is older than 8.0.16Check ENFORCED in TABLE_CONSTRAINTS; upgrade
A syntax error in the migration's SQLThe condition uses another database's syntaxWrite it in MySQL's SQL

FAQ​

Can a constraint compare two columns?​

Yes. The condition is any SQL expression on the row, such as "`DiscountedPrice` <= `Price`".

Does EF Core check the condition before saving?​

No. EF Core only creates the constraint; MySQL enforces it. Validate input in your app so users get a useful message.

Conclusion​

table.HasCheckConstraint("CK_products_UnitPrice", "`UnitPrice` >= 0") inside ToTable adds a CHECK constraint that MySQL enforces on every insert and update, from any program. Write the condition in MySQL's SQL with backticks, give each constraint a name unique across the database, check existing rows before applying the migration, and keep validating input in the API.

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.