Skip to content

SqlServer: KeyNotFoundException in RewriteOperations when a temporal table is renamed into the model's default schema #38601

Description

@sliekens

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":

  1. the new entry is inserted under ("Widgets", "dbo") (raw NewSchema), then
  2. 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:

  1. With HasDefaultSchema commented out: dotnet ef migrations add Init
  2. Uncomment modelBuilder.HasDefaultSchema("dbo");, then: dotnet ef migrations add MoveToDbo
  3. 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions