Skip to content

DatabaseFacade.HasPendingModelChanges() false-positives on SQL Server temporal tables (PeriodStart/PeriodEnd) that the CLI correctly reports as clean #38602

Description

@jamesburton

Describe the bug

DatabaseFacade.HasPendingModelChanges() (runtime) reports pending model changes for every DbContext that has one or more SQL Server temporal tables (.ToTable(b => b.IsTemporal())), even immediately after scaffolding a migration that exactly matches the current model. The authoritative CLI equivalent, dotnet ef migrations has-pending-model-changes, correctly reports no changes for the same context/model/migration-set combination.

Dumping the raw IMigrationsModelDiffer.GetDifferences(...) output for the runtime path shows the "differences" are exclusively temporal period-column churn — never a real schema difference:

  • A context with 21 temporal tables produces 63 spurious operations: 21 AlterTableOperation (toggling the table's temporal-ness) + 42 AddColumnOperation for the shadow PeriodStart/PeriodEnd properties (2 per table).
  • A context with 1 temporal table produces 3 spurious operations (1 AlterTableOperation + 2 AddColumnOperation for PeriodStart/PeriodEnd).
  • Contexts with zero temporal tables report clean via both paths.

This makes HasPendingModelChanges() unusable as an in-process CI/test guard against model/migration drift for any context using temporal tables — the only reliable way we've found to detect real drift is running the CLI command out-of-process per context.

Root cause (as far as we've diagnosed)

It looks like an asymmetry in how the snapshot model (deserialized from the last migration's *ModelSnapshot.cs) vs. the current/design-time model are finalized for temporal-table metadata before being diffed:

  • The CLI (dotnet ef migrations has-pending-model-changes) builds its "current model" through the design-time services pipeline and gets a clean diff.
  • The runtime path (DatabaseFacade.HasPendingModelChanges()) finalizes the current model via IModelRuntimeInitializer.Initialize(...), and the resulting runtime model's temporal annotations (or the migrations differ's interpretation of them) disagree with the snapshot model's, even though both ultimately describe the identical schema.

We attempted the one documented lever for this — initializing the snapshot model with IModelRuntimeInitializer.Initialize(model, designTime: true) before diffing — and it did not resolve the discrepancy.

To Reproduce

We don't yet have a minimal standalone repro project to attach, but the shape that reproduces it every time in our codebase is:

  1. A DbContext with at least one entity mapped via .ToTable(b => b.IsTemporal()) (SQL Server provider).
  2. A migration set that is genuinely up to date (dotnet ef migrations has-pending-model-changes reports "No changes have been made to the model since the last migration." for the context).
  3. At runtime, call context.Database.HasPendingModelChanges() — this returns true.
  4. Diffing the resolved models directly via context.GetService<IMigrationsModelDiffer>().GetDifferences(snapshotModel.GetRelationalModel(), currentModel.GetRelationalModel()) shows only AlterTableOperation + AddColumnOperation/DropColumnOperation entries for the temporal table(s) and their PeriodStart/PeriodEnd shadow columns.

We can put together a minimal isolated repro if that would help triage — happy to do so on request.

Further technical details

EF Core version: 10.0.7 (also reproduced with the dotnet-ef CLI tool at 10.0.9)
Database provider: Microsoft.EntityFrameworkCore.SqlServer 10.0.7
Target framework: net10.0
Operating system: Windows (CI agents), reproduces locally on Windows dev machines too

Related issues

This looks adjacent to, but distinct from:

We couldn't find an existing issue describing this specific runtime-vs-CLI divergence for temporal tables, so filing separately. Happy to close as a duplicate if one already exists that we missed.

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