Lessons Learned from Migrating from EF6 to EF Core

An EF6 application can compile after moving to EF Core and still behave differently. A query might fail only when executed, an entity relationship might map differently, or a save operation might insert something you expected to update.

I would approach this migration as a change to application behavior. Package installation and namespace changes are only the starting point. The useful work is proving that queries, mappings, and writes still mean what the application expects.

Start with the database contract

Before changing code, choose the target EF Core version and database provider. Check the features your application depends on, especially custom mappings, provider-specific functions, and existing model configuration.

Consider an order system with established table names, decimal precision, and delete rules. Those details are part of its database contract. Make them explicit where necessary instead of assuming EF Core conventions will reproduce them.

Even table naming deserves attention. EF6 uses pluralization by convention, while EF Core generally uses the DbSet property name, falling back to the entity class name. A harmless-looking property rename can therefore affect the mapping. Microsoft documents this among the behavior changes between EF6 and EF Core.

My preference is to preserve the existing schema during the initial port. Combining an ORM migration with database redesign makes failures harder to explain.

Treat important queries as integration points

LINQ expressions are translated by a different query pipeline. Familiar syntax does not guarantee identical SQL or support for every expression.

For example, suppose an order search calls a custom C# method inside Where. In EF Core 3.0 and later, an expression that cannot be translated outside the final projection generally causes a runtime exception. Limited client evaluation remains supported in the final Select. This is an EF Core version distinction, not a claim that EF6 translated arbitrary C# methods.

Moving ToList before the filter may make the exception disappear, but it also moves that filtering into application memory. For a large orders table, that can mean transferring far more data than intended.

Review important queries using representative data. Compare results, generated SQL, database round trips, and execution time. Rewrite filters into translatable expressions where possible, and make any client processing deliberate.

Review writes before trusting them

Disconnected updates need particular care. Suppose an API receives an existing order with a new order line. EF Core’s Attach and Update behavior depends partly on whether entities have generated keys and whether those keys are set. Related entities can end up in different states. The documented graph behavior deserves a focused review.

Test adding children, changing relationships, deleting records, and handling concurrency conflicts. Check the resulting database state, not just whether SaveChanges succeeds.

For these tests, use an isolated database running the same engine as production. SQLite and the EF Core InMemory provider can behave differently from your actual provider, including query translation and string comparison. Microsoft explains these limits in its testing strategy guidance.

Plan the migration history separately

Existing EF6 migrations do not transfer directly into EF Core. Microsoft describes starting a new history after applying the EF6 migrations, using an initial EF Core migration with empty Up and Down methods while retaining its model snapshot. See the code-based model porting guide.

That baseline assumes the mapped model already matches the database. Verify that first, and plan separately how new environments will receive their initial schema.

The migration is ready when important application workflows produce the expected results and database changes. A successful build is useful evidence, but query and write behavior should decide when you ship.

Sources

Assi Arai
Assi Arai
Articles: 61