Bug description
Generating a migration script (or applying migrations at runtime) throws KeyNotFoundException on SQL Server when a migration renames a temporal table from an unspecified schema (null) into the schema that is also the model's default schema (e.g. modelBuilder.HasDefaultSchema("dbo")).
This is the exact operation set EF produces when you take an existing model that has a temporal table with no explicit schema and later add HasDefaultSchema("dbo"). EF emits EnsureSchema("dbo") + RenameTable(null → dbo) + AlterTable(dbo) for the temporal table, and script generation fails with:
System.Collections.Generic.KeyNotFoundException: The given key '(<Table>, dbo)' was not present in the dictionary.
The failure is in the stock SqlServerMigrationsSqlGenerator — no custom code required. It reproduces with a plain dotnet ef migrations script and does not need a database connection.
The essential trigger is: RenameTable of a temporal table from schema null → the default schema, followed by any further table operation on that (now dbo) table.
Root cause
A raw-vs-defaulted schema key inconsistency in SqlServerMigrationsSqlGenerator.RewriteOperations.
The method builds a lookup keyed by (TableName, Schema) and adds / looks up using the raw schema exactly as written on the operation (which may be null):
var rawSchema = tableMigrationOperation.Schema; // e.g. null
var schema = rawSchema ?? model?.GetDefaultSchema(); // e.g. "dbo"
...
temporalInformation = temporalTableInformationMap[(tableName, rawSchema)]; // keyed by RAW schema
But the two Remove calls use the defaulted schema. On the RenameTableOperation branch (release/10.0, lines 2850–2851):
// add new entry under the RAW new schema:
temporalTableInformationMap[(renameTableOperation.NewName!, renameTableOperation.NewSchema)] = temporalInformation;
// remove old entry under the DEFAULTED old schema:
temporalTableInformationMap.Remove((tableName, schema)); // schema = rawSchema ?? GetDefaultSchema()
When a table is renamed from null → the default schema "dbo":
- the new entry is inserted under
("Widgets", "dbo") (raw NewSchema), then
- the removal key is
("Widgets", rawSchema ?? GetDefaultSchema()) = ("Widgets", "dbo") — the same key that was just inserted, so it is immediately deleted.
The subsequent AlterTable / AlterColumn on dbo.Widgets then does temporalTableInformationMap[("Widgets", "dbo")] and throws. (The DropTableOperation branch has the same defaulted-key Remove and is likely affected by the equivalent scenario.)
When it was introduced
The temporalTableInformationMap mechanism does not exist in release/8.0 — EF 8 handled temporal rewriting differently and never hits this path, which is why this model change scripted fine on EF Core 8. The dictionary-based rewrite (with this raw/defaulted key mismatch) was introduced in EF Core 9.0 and is still present in 10.0 and main (11.0 previews).
Suggested fix
Make the Remove keys consistent with insert/lookup — remove by the raw schema (Remove((tableName, rawSchema))), or normalize all keys (insert, lookup, and remove) to the defaulted schema. The insert/lookup/remove keying must agree.
Workaround
Replace the offending migration's Up() with equivalent raw SQL via migrationBuilder.Sql(...). Raw SqlOperations are not ITableMigrationOperation, so they bypass the temporal-rewrite path entirely and avoid the crash.
Your code
Minimal, self-contained repro (no database required).
Steps:
- With
HasDefaultSchema commented out: dotnet ef migrations add Init
- Uncomment
modelBuilder.HasDefaultSchema("dbo");, then: dotnet ef migrations add MoveToDbo
dotnet ef migrations script → crash
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace Ef10Repro;
public class Widget
{
public int Id { get; set; }
public string? Name { get; set; }
}
public class AppDbContext : DbContext
{
public DbSet<Widget> Widgets => Set<Widget>();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseSqlServer("Server=(localdb)\\MSSQLLocalDB;Database=Ef10Repro");
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// STEP 2 (the trigger): add this line, then add a second migration.
// modelBuilder.HasDefaultSchema("dbo");
modelBuilder.Entity<Widget>().ToTable("Widgets", t => t.IsTemporal());
}
}
public class Factory : IDesignTimeDbContextFactory<AppDbContext>
{
public AppDbContext CreateDbContext(string[] args) => new();
}
// The generated MoveToDbo migration that triggers the crash:
//
// migrationBuilder.EnsureSchema(name: "dbo");
//
// migrationBuilder.RenameTable(
// name: "Widgets", newName: "Widgets", newSchema: "dbo")
// .Annotation("SqlServer:IsTemporal", true)
// .Annotation("SqlServer:TemporalHistoryTableName", "WidgetsHistory")
// .Annotation("SqlServer:TemporalHistoryTableSchema", null)
// .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
// .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");
//
// migrationBuilder.AlterTable(name: "Widgets", schema: "dbo")
// .Annotation("SqlServer:IsTemporal", true)
// .Annotation("SqlServer:TemporalHistoryTableSchema", "dbo")
// // ...temporal annotations...
// .OldAnnotation("SqlServer:IsTemporal", true)
// .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null);
Stack traces
System.Collections.Generic.KeyNotFoundException: The given key '(Widgets, dbo)' was not present in the dictionary.
at System.Collections.Generic.Dictionary`2.get_Item(TKey key)
at Microsoft.EntityFrameworkCore.Migrations.SqlServerMigrationsSqlGenerator.RewriteOperations(IReadOnlyList`1 migrationOperations, IModel model, MigrationsSqlGenerationOptions options)
at Microsoft.EntityFrameworkCore.Migrations.SqlServerMigrationsSqlGenerator.Generate(IReadOnlyList`1 operations, IModel model, MigrationsSqlGenerationOptions options)
at Microsoft.EntityFrameworkCore.Migrations.Internal.Migrator.GenerateUpSql(Migration migration, MigrationsSqlGenerationOptions options)
at Microsoft.EntityFrameworkCore.Migrations.Internal.Migrator.GenerateScript(String fromMigration, String toMigration, MigrationsSqlGenerationOptions options)
at Microsoft.EntityFrameworkCore.Design.Internal.MigrationsOperations.ScriptMigration(String fromMigration, String toMigration, MigrationsSqlGenerationOptions options, String contextType)
at Microsoft.EntityFrameworkCore.Design.OperationExecutor.ScriptMigrationImpl(String fromMigration, String toMigration, Boolean idempotent, Boolean noTransactions, String contextType)
at Microsoft.EntityFrameworkCore.Design.OperationExecutor.ScriptMigration.<>c__DisplayClass0_0.<.ctor>b__0()
at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.<>c__DisplayClass3_0`1.<Execute>b__0()
at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.Execute(Action action)
The same exception occurs at runtime via Migrator.Migrate(), which also goes through RewriteOperations.
Verbose output
dotnet ef migrations script --verbose (trimmed to the relevant portion):
Using DbContext factory 'Factory'.
Using context 'AppDbContext'.
Finding design-time services for provider 'Microsoft.EntityFrameworkCore.SqlServer'...
Using design-time services from provider 'Microsoft.EntityFrameworkCore.SqlServer'.
Generating up script for migration '20260709085127_Init'.
Generating up script for migration '20260709085146_MoveToDbo'.
'AppDbContext' disposed.
System.Collections.Generic.KeyNotFoundException: The given key '(Widgets, dbo)' was not present in the dictionary.
at System.Collections.Generic.Dictionary`2.get_Item(TKey key)
at Microsoft.EntityFrameworkCore.Migrations.SqlServerMigrationsSqlGenerator.RewriteOperations(...)
...
The given key '(Widgets, dbo)' was not present in the dictionary.
The Init migration scripts fine; the crash happens while generating the up-script for MoveToDbo.
EF Core version
10.0.9 (also reproduces on 9.0.x and on main / 11.0 previews; not present in 8.0)
Database provider
Microsoft.EntityFrameworkCore.SqlServer
Target framework
.NET 10.0
Operating system
Windows
IDE
N/A — reproduced with the dotnet ef CLI (dotnet-ef 10.0.9)
Bug description
Generating a migration script (or applying migrations at runtime) throws
KeyNotFoundExceptionon SQL Server when a migration renames a temporal table from an unspecified schema (null) into the schema that is also the model's default schema (e.g.modelBuilder.HasDefaultSchema("dbo")).This is the exact operation set EF produces when you take an existing model that has a temporal table with no explicit schema and later add
HasDefaultSchema("dbo"). EF emitsEnsureSchema("dbo")+RenameTable(null → dbo)+AlterTable(dbo)for the temporal table, and script generation fails with:The failure is in the stock
SqlServerMigrationsSqlGenerator— no custom code required. It reproduces with a plaindotnet ef migrations scriptand does not need a database connection.The essential trigger is:
RenameTableof a temporal table from schemanull→ the default schema, followed by any further table operation on that (nowdbo) table.Root cause
A raw-vs-defaulted schema key inconsistency in
SqlServerMigrationsSqlGenerator.RewriteOperations.The method builds a lookup keyed by
(TableName, Schema)and adds / looks up using the raw schema exactly as written on the operation (which may benull):But the two
Removecalls use the defaulted schema. On theRenameTableOperationbranch (release/10.0, lines 2850–2851):When a table is renamed from
null→ the default schema"dbo":("Widgets", "dbo")(rawNewSchema), then("Widgets", rawSchema ?? GetDefaultSchema())=("Widgets", "dbo")— the same key that was just inserted, so it is immediately deleted.The subsequent
AlterTable/AlterColumnondbo.Widgetsthen doestemporalTableInformationMap[("Widgets", "dbo")]and throws. (TheDropTableOperationbranch has the same defaulted-keyRemoveand is likely affected by the equivalent scenario.)When it was introduced
The
temporalTableInformationMapmechanism does not exist inrelease/8.0— EF 8 handled temporal rewriting differently and never hits this path, which is why this model change scripted fine on EF Core 8. The dictionary-based rewrite (with this raw/defaulted key mismatch) was introduced in EF Core 9.0 and is still present in 10.0 andmain(11.0 previews).Suggested fix
Make the
Removekeys consistent with insert/lookup — remove by the raw schema (Remove((tableName, rawSchema))), or normalize all keys (insert, lookup, and remove) to the defaulted schema. The insert/lookup/remove keying must agree.Workaround
Replace the offending migration's
Up()with equivalent raw SQL viamigrationBuilder.Sql(...). RawSqlOperations are notITableMigrationOperation, so they bypass the temporal-rewrite path entirely and avoid the crash.Your code
Minimal, self-contained repro (no database required).
Steps:
HasDefaultSchemacommented out:dotnet ef migrations add InitmodelBuilder.HasDefaultSchema("dbo");, then:dotnet ef migrations add MoveToDbodotnet ef migrations script→ crashStack traces
The same exception occurs at runtime via
Migrator.Migrate(), which also goes throughRewriteOperations.Verbose output
dotnet ef migrations script --verbose(trimmed to the relevant portion):The
Initmigration scripts fine; the crash happens while generating the up-script forMoveToDbo.EF Core version
10.0.9 (also reproduces on 9.0.x and on
main/ 11.0 previews; not present in 8.0)Database provider
Microsoft.EntityFrameworkCore.SqlServer
Target framework
.NET 10.0
Operating system
Windows
IDE
N/A — reproduced with the
dotnet efCLI (dotnet-ef 10.0.9)