Daniel Becroft
Daniel Becroft

Reputation: 716

Entity Framework Core - Earlier data migrations break when models are changed

We are starting a new application with with .NET Core, EF Core and Postgres - we're really liking the migrations :-)

However, I've found a problem that I can't figure out how to work around. In some of our migrations, we use the DbContext to load records, run some business logic, and resave the records. We also run database.Context.Migrate() when the web server starts, so it always runs.

This all works fine (so far).

Unfortunately it works right up until the model is changed at a later point. Consider the following migration order:

  1. Create Customer, Order, and OrderLine models. Add the schema migrations to load the structures

  2. A business rule changes, so we need to set a value on the Order record based on some business logic . We create a data migration where we load the DbContext, run the code, and run SaveChangesAsync()

    At this point, everything is fine.

  3. We need to add a schema migration with a new field to the Customer record.

Here, things break. If the database has not had migration 2 applied, the Migrate() command errors as when applying migration 2, EF is trying to generate a select statement that includes the new field in 3.

I'm not sure where the problem actually is in the process though: should we not be using the DbContext in the migration (and hand-code SQL statements)? Should we be explicitly be doing projections on every record read so that we know exactly what we're doing?

Something else?

Upvotes: 4

Views: 2156

Answers (2)

Devon Parsons
Devon Parsons

Reputation: 1288

I faced a similar issue and landed on this question; I thought I'd share the technique I used to resolve my problem for posterity.

In my case, we had a model Something that initially had a field/database column property, but the field was deprecated because property ended up being stored elsewhere. We wrote a data migration that referenced Something.property and moved it to its new location.

We wanted to update the domain model to remove property from Something as well. However, doing so would break the typechecking in the data migration.

We must have a type that has property on it to allow the data migration to typecheck. However, we must not have property on the domain model.

The solution was to introduce MigrationDbContext, which subclasses the standard DbContext. MigrationDbContext exposes a second projection of the Somethings table, called Somethings_WithProperty. This projection is a public DbSet<SomethingWithProperty>. The SomethingWithProperty class is a record of the historical type of the model with its contemporary properties; and importantly, we only allow the Type to show up in our Migrations folder. SomethingWithProperty is stored in Migrations/HistoricalModels.

Now, my data migration acquires a MigrationDbContext from dependency injection instead of a DbContext. I can replace the reference to dbcontext.Somethings with dbcontext.Somethings_WithProperty, and the data migration can typecheck. My domain model on the other hand will never be procured from the MigrationDbContext, so the only Somethings it can get are from the Somethings DbSet, which correctly omits the property that we dropped.

A few caveats; if your DbContext constructor takes a generic DbContextOptions<MyDbContext> parameter, then to satisfy dependency injection, you will need to introduce a protected constructor on MyDbContext that takes a DbContextOptions<MigrationDbContext> parameter; the child class can invoke this in its constructor. Also, MigrationDbContext will initially be confused that two projections are mapped to the same table; in its SetupSomethingsWithProperty method you should include a foreign key referencing the natural projection to clear up the confusion.

private static void SetupSomethingsWithProperty(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Something_WithProperty>(s =>
    {
        s.ToTable("Somethings");
        s.HasOne<Something>().WithOne().HasForeignKey<Something_WithProperty>(r => r.ID);
    });
}

Upvotes: 0

Caleb
Caleb

Reputation: 2267

In your example, 1 and 3 are Schema Migrations and 2 is a Data Migration.

Since Entity Framework Core will generate queries based off the current models in code, and not the current state of the database, there is two options for handling Data Migrations.

  1. Perform Data Migrations In-Order with Schema Migrations

    Use something like Dapper or ADO.NET to perform data migrations based on the schema at the time the migration is written. You can get the DbConnection object from the EF Context using context.Database.GetDbConnection()

    Pros: Migration code will never need to be changed

    Cons: You won't get any of the benefits of EF Core

  2. Perform Data Migrations After Schema Migrations

    EF performs migrations in the ascending order of the MigrationAttribute.Id string value. EF generates this based off a timestamp when calling dotnet ef migrations add xxx. You can change this attribute to ensure Data Migrations are run after all Schema Migrations. Change the filename as well for readability so that they stay in the order they will be applied.

    Pros: You get all the benefits of EF Core

    Cons: If the schema changes in the future, such as removing a column referenced in the data migration, you will need to modify your migration

    Example MigrationAttribute.Id change:

// change
[Migration("20191002171530_DataMigration")]
// to
[Migration("99999999000001_DataMigration")]

Upvotes: 5

Related Questions