EF Core 10 Named Default Constraints: Preview the Migration SQL Before Deploying
Overview
EF Core 10 solves an annoying SQL Server maintenance problem: database-generated names such as DF__Jobs__Status__... are hard to predict in scripts and incident playbooks. The new convention gives every default a stable name. The catch is easy to miss: enabling it on an existing model makes the next migration change every default constraint in that model. That is exactly the kind of small configuration edit that deserves a migration gate.
I want to know how many columns will change, what SQL will run, and whether the preview can be checked without pointing a tool at a real database. Why EF Core 10 named default constraints can surprise CI builds?
Feature Overview
EF Core 10 adds two naming choices for SQL Server defaults. I can pass a name directly to HasDefaultValue or HasDefaultValueSql, or enable the global convention:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.UseNamedDefaultConstraints();
modelBuilder.Entity(entity =>
{
entity.Property(job => job.Status).HasDefaultValue("queued");
entity.Property(job => job.CreatedUtc).HasDefaultValueSql("SYSUTCDATETIME());
entity.Property(job => job.RetryCount).HasDefaultValue(0);
});
}
The generated names are predictable: DF_Jobs_Status, DF_Jobs_CreatedUtc, and DF_Jobs_RetryCount. Predictability helps when a deployment script, DBA, or rollback procedure must refer to a constraint.
The global call is not metadata-only for an existing schema. Microsoft's EF Core 10 release notes warn that the next migration renames every default constraint in the model. A build that only checks whether migrations compile will not show the scope clearly.
Migration Impact
Preview the migration SQL without SQL Server. I built the runnable sample around two committed migrations. InitialSchema represents the old model with three unnamed defaults. NameDefaultConstraints is scaffolded after adding the global convention. The second migration contains three AlterColumn operations. Each operation keeps the same CLR type and default value, but adds the relational annotation:
migrationBuilder.AlterColumn(
name: "Status",
table: "Jobs",
type: "nvarchar(32)",
maxLength: 32,
nullable: false,
defaultValue: "queued",
oldClrType: typeof(string),
oldType: "nvarchar(32)",
oldMaxLength: 32,
oldDefaultValue: "queued"
);
Annotation("Relational:DefaultConstraintName", "DF_Jobs_Status");
Generating a migration script does not require a live connection. The SQL Server provider can translate the committed operations locally:
dotnet tool restore dotnet restore dotnet build -c Release --no-restore
dotnet tool run dotnet-ef migrations script `InitialSchema NameDefaultConstraints` --no-build --configuration Release
The preview reveals more than the C# migration name suggests. For every affected column, EF queries sys.default_constraints to discover the opaque old name, drops that constraint, alters the column, and adds the predictable constraint. In this sample that sequence happens three times. The script does not drop a table or column, but it is still schema work worth reviewing for locks and deployment duration.
Building the Migration Script
Turn the migration preview into a deterministic gate. Reading a script once is useful; making the expectation executable is better. The sample resolves EF's migrations services without opening the placeholder connection. It inspects the second migration and then generates the upgrade SQL twice from fresh contexts:
var alteredDefaults = namingMigration.UpOperations.OfType<AlterColumnOperation>().ToArray();
Check(alteredDefaults.Length == 3, "enabling the convention changes all three defaults");
Check(CountOccurrences(sql, "DROP CONSTRAINT") == 3, "SQL drops all three existing default constraints");
The verifier also checks the three exact DF_... names, confirms three lookups in sys.default_constraints, rejects DROP TABLE and DROP COLUMN, and confirms the placeholder connection remains closed. Five repeated runs produce identical output. The complete commands and expected nine checks are in the sample README, and the merged pull request preserves the reviewed diff.
For CI, I prefer these semantic assertions over a snapshot of the entire SQL file. Provider patches may adjust whitespace, batch separators, or local variable names without changing the operation. Counting the affected defaults and checking the exact new constraint names keeps the gate focused. If a fourth default is added later, the test fails deliberately and asks the reviewer to update the expected scope instead of accepting a wider migration by accident. This is intentionally a contract test, not an integration test. It answers "what does this migration plan to do?" without requiring SQL Server, credentials, or a disposable database.
Recommendations
I would still apply the migration to a representative database before production because an offline script cannot predict lock duration, workload contention, or provider permissions. Microsoft's migration management guidance makes the same broader point: generated migrations should be reviewed and customized when needed.
When I would avoid the global convention UseNamedDefaultConstraints() is convenient for a new schema because the names exist from the first migration. It is also reasonable for a small existing schema when the generated changes have been reviewed and scheduled. For a large or busy database, I would consider a staged rollout.
EF Core 10's UseNamedDefaultConstraints API is SQL Server-specific, and the property-level overloads let me name selected defaults first. That keeps an unrelated model edit from producing a broad migration. I would also avoid treating the operation count as a performance estimate. Three safe-looking changes in a demo say nothing about hundreds of defaults on hot production tables. The gate protects scope and intent; database rehearsal protects the rollout.
Would you enable named defaults globally, or introduce explicit names one property at a time? Happy coding!
Comments
No comments yet. Start the discussion.