Background and information around the 9.0 PendingModelChangesWarning error
Everyone, we know some of you are (understandably) frustrated by the new PendingModelChangesWarning error that gets thrown when upgrading to EF 9. I wanted to share some information on the new error, why we did it and what you should do about it.
The main motivation behind this change was something that happens often: you make some change to your model (e.g. add a property), and then you forget to actually add a migration to apply it to the database. You run MigrateAsync() (or similar) and can't figure out why your database hasn't been updated; the new error clearly tells you that your model contains changes with respect to the last migration snapshot. In other words, the point here is to help you avoid accidental error.
Now, there are some other cases where this error is thrown, even if you haven't done any changes; code that seemed to run just fine in EF 8 suddenly starts throwing in EF 9. A lot of people have assumed that this is a problem in EF, but in reality the error usually still indicates some problem in your code that was there before, but was not flagged. That's a good thing! Even here, the point is to help you clean up the problem and avoid problems. Of course, you always have the option of simply suppressing the warning (see below), so you should never be blocked for upgrading to EF 9; it's fine to do this temporarily, and then maybe come back and deal with it later, after upgrading, once you have more time to focus on it.
Here's an example: some people want to seed their database with some random Guids, so they put code such as the following in their model configuration:
modelBuilder.Entity<Blog>().HasData(new Blog { Id = Guid.NewGuid(), ... });
This looks simple enough, but it's actually quite problematic. For each migration, EF stores a snapshot of your model for that migration. When you create a new migration, EF compares your current model (represented by your code) with the latest snapshot, and the new migration represents the difference between these two. In 9.0, when you call MigrateAsync, EF does the exact same thing, and if any changes are detected, throws the new PendingModelChangesWarning error.
Now, when you generated your last migration, Guid.NewGuid() got evaluated at that point, and an actual random Guid gets integrated into the previous context snapshot; you can open up the XXXContextModelSnapshot.cs in your migrations directory, and see that Guid. Now, when you call MigrateAsync, Guid.NewGuid() gets evaluated again, but because the random Guid it generates will differ from the one stored in the model snapshot, EF detects this as a pending model change (as if you forgot to add a migration!).
The solution to this is very simple: avoid using dynamic (or non-deterministic) APIs such as Guid.NewGuid(), and embed specific GUIDs instead:
modelBuilder.Entity<Blog>().HasData(new Blog { Id = Guid.Parse("9e4f49fe-0786-44c6-9061-53d2aa84fab3"), ... });
Once you do this, generate a new migration once, and this whole thing will be behind you.
Why is this important? Previous versions of EF may have not thrown PendingModelChangesWarning for this, but they also didn't work well around this situation. For example, every time you added a migration, EF would see a model change in the Guid seed data, and generate a useless migration to change that value in the database - that's a bad state of affairs that the new error helps detect and fix. So while the new error is frustrating, it's there to help you get your code in better shape.
There are some other cases where this error will be thrown; we've recently published a breaking change note listing these, and explaining what to do. Also, since the error has been causing some confusion, we recently merged an improvement that will be released in 9.0.1: EF will now recognize some of the issues causing the "pending model changes" error, and throw more specific errors to let you know exactly what the problem is. For example, the case above with Guid.NewGuid() will now be detected, and we'll tell you that you're using dynamic values in HasData.
Hopefully the above clarifies things; we recommend looking into it and trying to fix the root cause - this way you end up with a better, more stable application. But if you choose to, you can suppress the warning, and revert back to the same situation as in previous versions of EF:
options.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning))
I appreciate the clarity and get the desire here to try and help. This error probably will help people new to EF too, but at the very least something here seems off in something like my use case.
As far as i knew, I had no dynamic or non-deterministic APIs. All of my migrations are straight EF generated with zero migration modifications and no HasData usages or other seeds. The closest I have to this is is my BaseEntity (entity, not in a migration) having a default for the id prop of NewGuid():
public abstract class BaseEntity
{
[Key]
public Guid Id { get; private set; } = Guid.NewGuid();
//...
}
From what i saw in the original ticket this is somehow getting picked up, but I know of no no other way to do this in code and not doing so would be bad dx as id's would show as empty guids until save. I don't see this showing up in my snapshot either.
Is there an alternative you guys recommend? Or are we just supposed to live with a bad dx here or add explicit code that overrides this error?
@pdevito3 yeah, initializing your property to a Guid property on your CLR type in this way is effectively the same as including it inside HasData(): every time the model runs, the seeding data that's included in the model has a different Guid (since your HasData has a new MyEntity() in it, which causes a new Guid to get generated).
To fix this, you can:
- Make your Id setter public, and set it explicitly to some value within HasData()
- If you don't want to make the setter public, you can switch to using an anonymous type inside HasData(). For example, instead of having
HasData(new MyEntity { Foo = "..." }), you can haveHasData(new { Id = Guid.Parse("..."), Foo = "..." }). This would restrict to change to your HasData() configuration, without impacting what your CLR type looks like.
@roji I think @pdevito3 is saying they have no calls to HasData:
with zero migration modifications and no HasData usages or other seeds.
I'm glad the breaking change note is there now (there needs to be one for temporal migrations too #35108 as it's impossible to upgrade if you use them).
Agree with @pdevito3 that this is great for new developers to know what's going on and why their code is throwing errors at runtime, but I don't understand why this isn't handled better and with correct terminology.
the new PendingModelChangesWarning error
Is it actually meant to be a warning or an error? If it truly was a warning, that yellow log printing to the console on build would be extremely helpful so that we know we need to do something. It could also throw as an error at the time when it would actually cause the runtime exception rather than being an ambiguous "column not found" (or similar) error. The current behaviour of having it exploding vs not having it at all is worse.
Typically on larger changes to models, for example either early stages where the data structure needs to change to support what is being built out, or later stages where an entire model is being changed, we want to be actively on the system whilst simultaneously updating code for the models until we get the result that we desire. To do this now, we'd have to disable warnings, rebuild, incrementally make our changes, and then migrate and put the warning back in.
#35221 seems to be a step in the right direction where certain types of the same warning won't throw an exception which is awesome! But the use case above as far as I can see is still going to throw an exception by default? I may be being stupid as I've only had a cursory look at the PR.
Surely it would be better to have multiple types of "warnings" so that we can suppress them individually rather than all on and off?
@roji I think @pdevito3 is saying they have no calls to
HasData:with zero migration modifications and no HasData usages or other seeds.
Correct. This is in my base entity model that all my dbsets inherit from. This default makes it so my entities get an actual id value when created in code instead of waiting for save changes to do it for me. Without a default guid like this, the id is always empty until save. I had this in a project once and it was very much bad dx.
@ajcvickers @pdevito3 sorry, I missed that important sentence :) In that case @pdevito3 it would be good to have a repro, to make sure this isn't some case we're not aware of... If you can put that together, can you please open a new issue containing that repro?
@rudiv
Is it actually meant to be a warning or an error?
EF events such as this can be configured by the user to be errors (throw an exception), be a warning (log), or completely ignored. This specific event is handled as an error by default, which is why it's throwing an exception for people (but can be ignored).
Typically on larger changes to models, for example either early stages where the data structure needs to change to support what is being built out, or later stages where an entire model is being changed, we want to be actively on the system whilst simultaneously updating code for the models until we get the result that we desire. To do this now, we'd have to disable warnings, rebuild, incrementally make our changes, and then migrate and put the warning back in.
I don't follow this. All this error does, is throw when you attempt to call MigrateAsync (or similar) and haven't yet added the appropriate migration to represent your later changes. You can make changes to the model and even run your application as long as you don't try to call migrate (your database would then represent a different schema than what your application expects); you can also make changes to the model and add the appropriate migration (that's the normal/expected flow). But I don't ese how it makes sense to change your model and then calling MigrateAsync, without also adding the migration to account for thoes model changes.
It may just be me @roji but after far too many years of working with EF without this exception, and in dev having MigrateAsync() in the startup for dev to keep things always in sync, we could be going through the app to make changes with it running and making changes to the models whilst we're running in dev mode.
The issue now is that when we make 1 (of N) changes and save that model, alongside dotnet watch (a typical dev loop) the application will stop running as an exception is thrown at startup.
If you can put that together, can you please open a new issue containing that repro?
@roji I just took a quick stab at this and from what i can tell so far having a base entity with a default like public Guid Id { get; private set; } = Guid.NewGuid(); does NOT seem to trigger the error after a .NET 9 upgrade like has been assumed previously.... with that said, I stand by the fact that my actual project has no HasData usages, migration mods, or other areas that i can tell are using dynamic or non-deterministic APIs and it does still run into this issue.
I can continue trying to narrow down a repro and open a ticket if I find it, but this does speak to what has been brought up about a specific callout about what change was found so we can target the issue instead of hunting or slapping an ignore on would be great. I know the errors have been enhanced a bit but if they don't get to that level of detail, there's still a gap to fill i think
Having same problem here, I have no dynamic data and no calls to HasData. Adding new migration results in empty migration. I was able to drill into the diff process.
Consider following entity:
public class HandledEvent : BaseEntity
{
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
... other unrelated members
}
StartDate and EndDate are not configured explicitly and have default DateTime configuration.
EF detects following changes in these columns:
Can you spot problems here? Any idea how to fix that?
I'm running Npgsql.EntityFrameworkCore.PostgreSQL 9.0.1
@elementum are you able to put together a minimal repro for this? If so, can you open a new issue with that code? That would be the best way forward on this.
Our team is developing on Windows and deploying to Linux. In our model we have some indexes with SQL filter expressions. Also, we have custom constraints. In many cases these index filters and constraints are defined as multiline verbatim or raw string literals.
On Windows everything runs fine, but once I'm deploying to Linux and try to run, I'm getting 'Microsoft.EntityFrameworkCore.Migrations.PendingModelChangesWarning' exception.
This forces me to checkout the project on Linux and generate new migration on that platform.
Adding new migration on Windows produces empty migration. Adding new migration on Linux produces migration with all line endings in custom SQL definitions replaced by '\n'.
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropCheckConstraint(
name: "ddc9f54a-7afa-471c-8b73-017b5916df9e",
schema: "mesh",
table: "ent_ent_perm");
migrationBuilder.AddCheckConstraint(
name: "ddc9f54a-7afa-471c-8b73-017b5916df9e",
schema: "mesh",
table: "ent_ent_perm",
sql: "\nleft_type = 6 AND right_type = 5\nOR left_type = 3 AND right_type IN (5, 1)\nOR left_type = -1 AND right_type = 1");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropCheckConstraint(
name: "ddc9f54a-7afa-471c-8b73-017b5916df9e",
schema: "mesh",
table: "ent_ent_perm");
migrationBuilder.AddCheckConstraint(
name: "ddc9f54a-7afa-471c-8b73-017b5916df9e",
schema: "mesh",
table: "ent_ent_perm",
sql: "\r\nleft_type = 6 AND right_type = 5\r\nOR left_type = 3 AND right_type IN (5, 1)\r\nOR left_type = -1 AND right_type = 1");
}
I see what the problem is and how to resolve it. Just sharing my case and hoping it could help someone else to spot the problems when exception is thrown despite empty migration.
@dcby thanks for reporting and analyzing this. I've opened #35382 to look into fixing this on the EF side.
Fix: Get over the issue by suppressing the warning which is treated as an error:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning));
}
Also getting the pending model changes warning. I read this explanation, went through the code to everywhere replacing Guid.NewGuid() and DateTime.Now with deterministic values (first step: for the seeding in the integration tests), but the tests are still failing with the warning. When creating a migration, it is empty. https://github.com/orso-co/Orso.Arpa.Api/pull/1063/files Looking forward to v9.0.1 which hopefully will bring more detailed error messages.
The new PendingModelChangesWarning is a good choice and beneficial in most scenarios and I don't want to ignore it entirely.
Are there any plans to make it configurable so that only certain models or specific properties are ignored? For example, Ignore the Password property on the User model so we can use a password generator like this example. Or is it a bad practice?
protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data.json"); string jsonData = File.ReadAllText(path);
DataModel? data = JsonSerializer.Deserialize<DataModel>(jsonData);
if (data != null)
{
foreach (User user in data.Users)
{
if (user.Password != null)
{
user.Password = PasswordManager.HashPassword(user.Password);
}
}
modelBuilder.Entity<User>().HasData(data.Users);
}
}
Have you tried [NotMapped] Data annotation on the password field? You never save the user password as a standard.
[NotMapped] will prevent the property from being mapped to the database. That's not what I want.
For development purposes, I am using the data.json file that contains all initial values like lookups and admin credentials. data.json will not be committed and will be replaced more securely later So I am not saving the user password.
Maybe my example is not the best but my idea is what if we can explicitly make the PendingModelChangesWarning configurable to ignore specific properties or models instead of having only two options - ( either enable it for all or suppress the warning entirely )
Thank you for your reply.
I see what you are trying to do. Can you use two different classes (standard practice), UserEntity and UserDTO.
List<UserDTO> users = Load("data.json"); foreach user in users{ UserEntity entity = _mapper.Map<UserEntity>(user); SeedUser(entity); }
Or use AddRange after you update entity.Password = PasswordHash(user.Password);
Good luck!
I've encountered the next issue with EF 9 update.
Solution
|- Tests
|- App
|- API (startup project)
|- Data (DbContext)
When running initial migration with --startup-project Data DbContext created at design time and migration is successful but I'm getting
[Fatal] API.Program A migration failed during startup
System.InvalidOperationException: An error was generated for warning 'Microsoft.EntityFrameworkCore.Migrations.PendingModelChangesWarning':
when running next code
using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<AppDbContext>();
var pendingMigrations = await context.Database.GetPendingMigrationsAsync();
if (pendingMigrations.Any())
await context.Database.MigrateAsync();
}
...
Regardless of that I can see existing pending migration during debug.
When running initial migration with --startup-project API I'm getting
[Information] Program Starting up
[Fatal] Program Application terminated unexpectedly
Microsoft.Extensions.Hosting.HostAbortedException: The host was aborted.
Because of runtime creation of DbContext via DI as I understand.
Am I missing something? Is there a way to fix this without suppressing warning?
@AlexMelnykov Have you tried 9.0.1?
i have the same problem, when i try run, my code
using keycontrol.Domain.Entities; using keycontrol.Infrastructure.EntityConfigurations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics;
namespace keycontrol.Infrastructure.Context;
public class AppDbContext : DbContext { public AppDbContext(DbContextOptions options) : base(options) { } public DbSet<User> users { get; set; } public DbSet<KeyRoom> keys { get; set; } public DbSet<Report> reports { get; set; } public DbSet<Token> tokens { get; set; } public DbSet<Room> rooms { get; set; } public DbSet<Role> roles { get; set; } public DbSet<RolePermission> rolePermissions { get; set; } public DbSet<Permission> permissions { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new UserConfiguration());
modelBuilder.ApplyConfiguration(new TokenConfiguration());
modelBuilder.ApplyConfiguration(new RoomConfiguration());
modelBuilder.ApplyConfiguration(new KeyConfiguration());
modelBuilder.ApplyConfiguration(new ReportConfiguration());
modelBuilder.ApplyConfiguration(new PermissionConfiguration());
modelBuilder.ApplyConfiguration(new RoleConfiguration());
modelBuilder.ApplyConfiguration(new RolePermissionConfiguration());
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning));
}
}
namespace keycontrol.Infrastructure.EntityConfigurations;
public class RoleConfiguration : IEntityTypeConfiguration<Role> { public void Configure(EntityTypeBuilder<Role> builder) { builder.HasKey(b => b.Id); builder.HasMany(b => b.Permissions) .WithMany() .UsingEntity<RolePermission>(); builder.HasMany(b => b.Users) .WithMany(); builder.HasData(Role.GetValues()); } } using keycontrol.Domain.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace keycontrol.Infrastructure.EntityConfigurations;
public class PermissionConfiguration : IEntityTypeConfiguration<Permission> { public void Configure(EntityTypeBuilder<Permission> builder) { builder.HasKey(p => p.Id);
var permissions = Enum.GetValues<Domain.Enums.Permission>()
.Where(p => p != Domain.Enums.Permission.None)
.Select(p => new Permission { Id = (int)p, Name = p.ToString() });
builder.HasData(permissions);
}
}
using keycontrol.Domain.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Permission = keycontrol.Domain.Enums.Permission;
namespace keycontrol.Infrastructure.EntityConfigurations;
public class RolePermissionConfiguration : IEntityTypeConfiguration<RolePermission> { public void Configure(EntityTypeBuilder<RolePermission> builder) { builder.HasKey(rp => new {rp.RoleId, rp.PermissionId}); builder.HasData( Create(Role.Doorman, Permission.Administrator), Create(Role.CommonUser, Permission.ReadMember)); }
private static RolePermission Create(Role role, Permission permission)
{
return new RolePermission() { RoleId = role.Id, PermissionId = (int)permission };
}
}
@Erick-Bueno
public void Configure(EntityTypeBuilder builder)
{
builder.HasKey(p => p.Id);
var permissions = Enum.GetValues<Domain.Enums.Permission>()
.Where(p => p != Domain.Enums.Permission.None)
.Select(p => new Permission { Id = (int)p, Name = p.ToString() });
builder.HasData(permissions);
}
This is very problematic code - it configures model seed data based on data that's already elsewhere in the database. That means that every time the permissions change, your EF model would change (since it contains the model seed data), and you'd need to generate a new migration to update the seed data in the database.
It's highly recommended to not do this - consider switching to the new UseAsyncSeeding (docs), which doesn't store the data in the model. Basically it allows you to simply perform whatever seeding operation (query, savechanges) on app startup, without involving the model.
@roji
something like that?
`
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var permissionsEnum = Enum.GetValues<Domain.Enums.Permission>()
.Where(p => p != Domain.Enums.Permission.None)
.Select(p => new Permission { Id = (int)p, Name = p.ToString() });
optionsBuilder.UseNpgsql(_configuration.GetConnectionString("Default"))
.UseSeeding((context, _) =>
{
var permissions = context.Set<Permission>().ToList();
if (permissions == null)
{
context.Set<Permission>().AddRange(permissionsEnum);
context.SaveChanges();
}
})
.UseAsyncSeeding(async (context, _, cancellationToken) =>
{
var permissions = await context.Set<Permission>().ToListAsync(cancellationToken);
if (permissions == null)
{
context.Set<Permission>().AddRange(permissionsEnum, cancellationToken);
await context.SaveChangesAsync(cancellationToken);
}
});
}
`
@Erick-Bueno please look at your code snippets above - they're unreadable because you're not using markdown properly.
Otherwise, UsAsyncSeeding gets run every time your application starts (please read the docs), so you'll repeatedly add the same permissions over and over again. You need to either clear the permissions before adding them, or check which exist and only add the missing ones. Finally, don't use UseSeeding unless you really need to do sync I/O, which is strongly discouraged.
@roji what do you think
optionsBuilder.UseNpgsql(_configuration.GetConnectionString("Default"))
.UseAsyncSeeding(async (context, _, cancellationToken) =>
{
var existingPermissions = await context.Set<Permission>().ToListAsync(cancellationToken);
if (existingPermissions.Any())
{
context.Set<Permission>().RemoveRange(existingPermissions);
await context.SaveChangesAsync(cancellationToken);
}
context.Set<Permission>().AddRange(permissionsEnum, cancellationToken);
await context.SaveChangesAsync(cancellationToken);
});
@Erick-Bueno yeah, looks reasonable. Though clearing the permissions is much simpler (and faster) by simply using ExecuteDeleteAsync.
@roji thanks bro
Hi, I wanted to share a problem we encountered related to this change.
In our codebase, we’ve been using Npgsql.EnableLegacyTimestampBehavior = true. since it's introduction (I think Npgsql 6).
After updating to the latest EFCore version, one unittest started failing - but only when run as part of the full suite. Standalone, it worked fine.
The issue was that some tests weren’t setting Npgsql.EnableLegacyTimestampBehavior = true during initialization. Since the state of this AppContext flag cannot be changed later during a test run (which I feel maybe should be logged as a warning), the inconsistency caused a mismatch between the database and model - probably due to the timestamp column differences. This went unnoticed until the introduction of the PendingModelChangesWarning, which helped us catch it - thanks! :)
So - if you've suddenly encountered this error - and are using Npgsql.EnableLegacyTimestampBehavior = true. Check it is always configured during initialization!
(And yes, moving away from LegacyTimestampBehavior is on our backlog!)