Short story first, then the details and the fix.
Symptom:
After upgrading a Virto Commerce instance from .NET 8 → .NET 10 (and EF Core 8 → EF Core 10), the platform started logging PendingModelChangesWarning / warnings about pending model changes at startup.
An error was generated for warning ‘Microsoft.EntityFrameworkCore.Migrations.PendingModelChangesWarning’: The model for context ‘SecurityDbContext’ has pending changes. Add a new migration before updating the database. See Breaking changes in EF Core 9 (EF9) - EF Core | Microsoft Learn. This exception can be suppressed or logged by passing event ID ‘RelationalEventId.PendingModelChangesWarning’ to the ‘ConfigureWarnings’ method in ‘DbContext.OnConfiguring’ or ‘AddDbContext’.
No new code changes to domain entities were intended, migrations looked “up-to-date”, but EF reported differences and startup showed warnings (and some environments failed CI policy checks that treat these as errors).
As a workaround, we added Ignore for PendingModelChangesWarning
options.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning));
Common/expected cause and solution (short)
EF Core 10 is stricter about detecting relational model differences between the compiled model and the last migration snapshot. The usual solutions are:
- Create a new migration that captures the model differences (recommended when model actually changed).
- Suppress the warning or ignore it (only if you’re absolutely sure differences are benign and deterministic).
- Fix the root cause in database/schema (e.g., mismatched column types/lengths) so the model and snapshot match.
In our case at Virto Commerce the situation was more complicated — When I created a new migration, it was empty. The differences were not due to missing entity classes, but due to existing database columns having different sizes than our EF model expected (specifically, Name and LoginProvider columns were 450 instead of 128).
Because EF compares relational metadata (column sizes, types, keys, etc.) the mismatch triggered the warning even though domain code didn’t change.
How we diagnosed it?
To reliably identify what EF thinks is different, we created a small runtime PendingModelChangesChecker utility that walks the snapshot versus the runtime model and lists the differences. You can find the exact utility we used here:
PendingModelChangesChecker.cs.
using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.Extensions.DependencyInjection;
namespace VirtoCommerce.Platform.Data.Utilities;
/// <summary>
/// Utility class to check for pending model changes in DbContext.
/// This is similar to Migrator.HasPendingModelChanges() from Microsoft.EntityFrameworkCore.Migrations.Internal.
///
/// Usage:
/// var context = serviceProvider.GetRequiredService<SecurityDbContext>();
/// if (PendingModelChangesChecker.HasPendingModelChanges(context))
/// {
/// var details = PendingModelChangesChecker.GetPendingModelChanges(context);
/// Console.WriteLine(details);
/// }
/// </summary>
public static class PendingModelChangesChecker
{
/// <summary>
/// Checks if there are pending model changes that require a new migration.
/// This method compares the current model with the snapshot model from the last migration.
/// </summary>
/// <param name="context">The SecurityDbContext instance</param>
/// <returns>True if there are pending model changes, false otherwise</returns>
public static bool HasPendingModelChanges(DbContext context)
{
ArgumentNullException.ThrowIfNull(context);
try
{
var serviceProvider = context.GetInfrastructure();
var migrationsAssembly = serviceProvider.GetRequiredService<IMigrationsAssembly>();
var migrationsModelDiffer = serviceProvider.GetRequiredService<IMigrationsModelDiffer>();
var modelRuntimeInitializer = serviceProvider.GetRequiredService<IModelRuntimeInitializer>();
// Get the snapshot model (from the last migration's ModelSnapshot)
var snapshotModel = migrationsAssembly.ModelSnapshot?.Model;
if (snapshotModel == null)
{
// No migrations exist yet - check if the current model has any entities
// Use runtime model (design-time model not available at runtime)
return context.Model.GetEntityTypes().Any();
}
// Initialize the snapshot model with runtime dependencies before getting its relational model
// The snapshot model is already finalized, but needs runtime initialization
var initializedSnapshotModel = modelRuntimeInitializer.Initialize(snapshotModel, validationLogger: null);
// Get the current model from the design-time model if available (for full configuration),
// otherwise use runtime model. Note: IDesignTimeModel is only available at design-time.
// At runtime, we use context.Model which may not have all configuration details.
IModel currentModel;
try
{
// Try to get design-time model (only available at design-time)
var designTimeModelService = context.GetService<IDesignTimeModel>();
currentModel = designTimeModelService?.Model ?? context.Model;
}
catch
{
// Fallback to runtime model if design-time model is not available
currentModel = context.Model;
}
// Compare the snapshot model with the current model
// The HasDifferences method compares the relational models
var snapshotRelationalModel = initializedSnapshotModel.GetRelationalModel();
var currentRelationalModel = currentModel.GetRelationalModel();
return migrationsModelDiffer.HasDifferences(snapshotRelationalModel, currentRelationalModel);
}
catch (Exception ex)
{
// If we can't determine, throw to indicate the issue
throw new InvalidOperationException(
"Unable to check for pending model changes. Ensure migrations are properly configured.", ex);
}
}
/// <summary>
/// Gets detailed information about pending model changes.
/// This method returns a description of what differences exist between the snapshot and current model.
/// </summary>
/// <param name="context">The SecurityDbContext instance</param>
/// <returns>A string describing the differences, or null if no differences</returns>
public static string GetPendingModelChanges(DbContext context)
{
ArgumentNullException.ThrowIfNull(context);
try
{
var serviceProvider = context.GetInfrastructure();
var migrationsAssembly = serviceProvider.GetRequiredService<IMigrationsAssembly>();
var migrationsModelDiffer = serviceProvider.GetRequiredService<IMigrationsModelDiffer>();
var modelRuntimeInitializer = serviceProvider.GetRequiredService<IModelRuntimeInitializer>();
var snapshotModel = migrationsAssembly.ModelSnapshot?.Model;
if (snapshotModel == null)
{
// Use runtime model (design-time model not available at runtime)
var entityCount = context.Model.GetEntityTypes().Count();
return entityCount > 0
? $"No migrations exist. The current model has {entityCount} entity type(s) that need to be migrated."
: "No migrations exist and the model is empty.";
}
// Initialize the snapshot model with runtime dependencies before getting its relational model
// The snapshot model is already finalized, but needs runtime initialization
var initializedSnapshotModel = modelRuntimeInitializer.Initialize(snapshotModel, validationLogger: null);
var snapshotRelationalModel = initializedSnapshotModel.GetRelationalModel();
// Get the current model from the design-time model if available (for full configuration),
// otherwise use runtime model. Note: IDesignTimeModel is only available at design-time.
// At runtime, we use context.Model which may not have all configuration details.
IModel currentModel;
try
{
// Try to get design-time model (only available at design-time)
var designTimeModelService = context.GetService<IDesignTimeModel>();
currentModel = designTimeModelService?.Model ?? context.Model;
}
catch
{
// Fallback to runtime model if design-time model is not available
currentModel = context.Model;
}
var currentRelationalModel = currentModel.GetRelationalModel();
var differences = migrationsModelDiffer.GetDifferences(snapshotRelationalModel, currentRelationalModel);
if (differences.Count == 0)
{
return null;
}
var result = $"Found {differences.Count} pending model change(s) in SecurityDbContext:\n";
foreach (var difference in differences)
{
result += $" - {difference}\n";
}
return result;
}
catch (Exception ex)
{
return $"Error checking for pending model changes: {ex.Message}\nStack trace: {ex.StackTrace}";
}
}
}
PendingModelChangesChecker - How to use it (example)
- Add the file to your solution.
- Resolve the
DbContext(e.g.,SecurityDbContext) from your DI container and call:
using VirtoCommerce.Platform.Data.Utilities;
var context = serviceProvider.GetRequiredService<SecurityDbContext>();
if (PendingModelChangesChecker.HasPendingModelChanges(context))
{
var details = PendingModelChangesChecker.GetPendingModelChanges(context);
Console.WriteLine(details);
}
The tool prints the concrete relational differences EF detects (column length mismatches, missing columns, different nullability, etc.). This is much clearer than the generic PendingModelChangesWarning text.
What we found in our DB
The checker reported column size differences. Concretely:
Namecolumn: actual DB length = 450, expected by EF model = 128LoginProvidercolumn: actual DB length = 450, expected by EF model = 128
(These kinds of columns are typically in AspNetUserTokens / AspNetUserLogins or similar identity-related tables — adjust table names for your schema.)
Those wider nvarchar(450) columns are a legacy/previous migration artifact. EF10’s relational diff sees 450 != 128 and signals pending model changes.
Summary
- Do not suppress RelationalEventId.PendingModelChangesWarning warning permanently unless you understand the exact difference and why it’s benign. EF is warning you for a reason — it’s detected a real mismatch.
- The utility I linked is very helpful for pinpointing the difference. Use it during upgrades — it saved us hours of guesswork.