# Migrating Ef6 Code First To Ef Core

> Migrates Entity Framework 6 Code-First projects to EF Core. Use when upgrading projects that already use DbContext with Code-First models (no EDMX files) and need to move to EF Core. Triggers for "migrate EF6 to EF Core", "upgrade Entity Framework to EF Core", "convert EF6 Code-First to EF Core", "replace EntityFramework package with EF Core", or "modernize Entity Framework". Also relevant when encountering EF6-specific APIs like EntityTypeConfiguration, DbModelBuilder, Database.SetInitializer, or HasDatabaseGeneratedOption during .NET modernization.

- Skill: `microsoft/migrating-ef6-code-first-to-ef-core` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add microsoft/migrating-ef6-code-first-to-ef-core`
- Raw SKILL.md: https://api.skillmd.com/api/skills/microsoft/migrating-ef6-code-first-to-ef-core/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: Microsoft (https://skillmd.com/u/microsoft)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/microsoft/migrating-ef6-code-first-to-ef-core

---


# EF6 Code-First to EF Core Migration

## Overview

Migrates Entity Framework 6 Code-First projects to EF Core. This skill covers projects that already use `DbContext` with fluent or data-annotation-based models — no EDMX files involved.

> **STOP — is the old app still running?** If a .NET Framework host and a new .NET host share
> this database while both are live (a side-by-side migration), **load the
> `managing-shared-database-schema` skill first** and follow it instead of Step 7 here:
> `get_instructions(kind='skill', query='managing-shared-database-schema')`
>
> **This skill never deletes `Migrations/` or `Migrations/Configuration.cs`.** Those assets are
> the EF6 schema ledger. While any EF6 host is still live, they are the only way to deploy
> further schema changes, and removing them is not recoverable. Teardown belongs to full
> cutover only — see `ref/cutover-teardown.md`.
>
> **This gate applies to the EDMX path below as well.** If the scope note routes you to
> `migrating-edmx-to-code-first`, the same prohibition travels with you: during a side-by-side
> window, do not delete the migration assets and do not enable startup migration, whichever
> skill you end up in.
>
> **If you cannot tell, assume the database is shared.** Whether the Framework host stays live
> against this database frequently is not determinable from the repo alone. Unless you can
> positively confirm it is retired or runs against its own database, take the shared branch and
> load the skill first — the destructive steps here are correct only for a full cutover.

> **Scope:** This skill targets EF6 Code-First projects (no `.edmx` files). For EDMX-based projects (Database-First/Model-First), use the `migrating-edmx-to-code-first` skill instead. For DbContext registration and DI setup during ASP.NET Core migration, also apply the `migrating-ef-dbcontext` skill — it is complementary to this one.

## Workflow

```
Migration Progress:
- [ ] Step 1: Assess EF6 usage
- [ ] Step 2: Swap NuGet packages
- [ ] Step 3: Update namespaces
- [ ] Step 4: Migrate DbContext and configuration
- [ ] Step 5: Migrate entity configurations
- [ ] Step 6: Handle breaking API changes
- [ ] Step 7: Establish the EF Core migrations baseline
- [ ] Step 8: Validate
- [ ] Step 9: Full-cutover teardown — only if the old host is retired; skipped during a side-by-side window
```

### Step 1: Assess EF6 Usage

1. Confirm the project uses EF6 Code-First (has a `DbContext` subclass, no `.edmx` files)
2. List all `DbContext` subclasses and their `DbSet<T>` properties
3. Find `EntityTypeConfiguration<T>` classes and `OnModelCreating` overrides
4. Search for EF6-specific APIs: `Database.SetInitializer`, `DbModelBuilder`, `HasDatabaseGeneratedOption`, `Map()`, `MapToStoredProcedures()`
5. Check for custom conventions (`IConvention`, `Convention`)
6. Note any raw SQL usage: `Database.SqlQuery<T>()`, `Database.ExecuteSqlCommand()`
7. Check for the EF6 `Configuration` class generated by `Enable-Migrations` (typically in `Migrations/Configuration.cs`) — note its presence; it is retained unless the user confirms a full cutover
8. Confirm with the user before proceeding: if the project targets .NET Framework, advise upgrading to .NET 8 or later and updating EntityFramework to 6.5.1 first

### Step 2: Swap NuGet Packages

Remove the EF6 package and add EF Core packages matching the target framework.

```xml
<!-- Remove -->
<PackageReference Include="EntityFramework" />

<!-- Add (choose the appropriate database provider) -->
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" />
```

> **If EF6 migration assets are being retained** (Step 7 — another host still writes this
> database), removing the `EntityFramework` package breaks them: EF6 `DbMigration` and
> `DbMigrationsConfiguration` classes are compiled code that depends on it. Two options:
>
> - **Preferred — exclude the retained folder from compilation** with
>   `<Compile Remove="Migrations\**" />`, then remove the `EntityFramework` package as normal.
>   The EF6 ledger stays on disk as the historical record without being built.
> - **Alternative — keep the `EntityFramework` reference** alongside the EF Core ones, leaving
>   the EF6 migrations compiled. Use this only if something in this project still needs to run
>   them.
>
> Decide this now — Step 7 depends on it. Note that the exclusion glob is recursive, so EF Core
> migrations must be scaffolded **outside** `Migrations/` (Step 7 does this).

Use the EF Core major version that matches the project's target framework (e.g., EF Core 8.x for .NET 8, EF Core 10.x for .NET 10). Use the **managing-package-references** skill to add these packages — it handles version determination, CPM detection, and NuGet feed lookup.

### Step 3: Update Namespaces

Replace `System.Data.Entity` usings with `Microsoft.EntityFrameworkCore` **in application code**. Note that `System.Data.Entity.Validation` and `System.Data.Entity.Core.Objects` are removed entirely in EF Core — replace with custom validation and DbContext APIs respectively.

> **Exclude a retained EF6 `Migrations/` folder from this replacement.** Those files are EF6
> migration classes (`DbMigration`, `DbMigrationsConfiguration`) built on
> `System.Data.Entity.Migrations`. Rewriting their usings destroys the EF6 ledger exactly as
> deleting it would, only silently — the folder survives while ceasing to be valid EF6
> migrations. See Step 7.

### Step 4: Migrate DbContext and Configuration

- Change constructor to accept `DbContextOptions<T>` instead of a connection string name
- Change `OnModelCreating` parameter from `DbModelBuilder` to `ModelBuilder`

**Database initializers:** EF Core does not support `Database.SetInitializer` or the `MigrateDatabaseToLatestVersion` initializer. Remove all initializer calls and convert seed logic to `HasData()` in `OnModelCreating` or a separate seed method called at startup.

> The EF6 `Configuration` class (typically `Migrations/Configuration.cs`) has no EF Core
> equivalent, but **do not delete it here**. It is retained until a confirmed full cutover —
> see `ref/cutover-teardown.md`.

### Step 5: Migrate Entity Configurations

- Convert `EntityTypeConfiguration<T>` to `IEntityTypeConfiguration<T>` (add `Configure(EntityTypeBuilder<T>)` method, prefix all calls with `builder.`)
- Replace `HasRequired`/`HasOptional` with `HasOne` + `.IsRequired()`, and `WithRequired`/`WithOptional` with `WithOne` + `.IsRequired()`
- Replace `WillCascadeOnDelete(false)` with `OnDelete(DeleteBehavior.Restrict)`
- Replace `HasDatabaseGeneratedOption`:
  - `DatabaseGeneratedOption.Identity` → `ValueGeneratedOnAdd()` (database-generated IDs)
  - `DatabaseGeneratedOption.Computed` → `ValueGeneratedOnAddOrUpdate()` (computed columns)
  - `DatabaseGeneratedOption.None` → Check if you manually assign IDs before `Add()`. If yes, use `ValueGeneratedNever()`. If no (or using custom HiLo generator), use `ValueGeneratedOnAdd()` or EF Core's built-in `UseHiLo()` to avoid tracking conflicts from duplicate default IDs
- Replace `Map(m => m.ToTable("Name"))` with `ToTable("Name")` directly
- `HasMany().WithMany()` requires EF Core 5.0+. For join tables with payload columns, explicit join entity configuration is still required
- `MapToStoredProcedures()` is removed — use `FromSql()` or `ExecuteSql()`
- `modelBuilder.Conventions.Remove<T>()` is removed — override `ConfigureConventions(ModelConfigurationBuilder)` on the `DbContext` to add, remove, or replace conventions

**Decimal property precision:** EF6 automatically mapped `decimal` properties to `decimal(18,2)`. EF Core requires explicit precision configuration to avoid silent data truncation:

```csharp
// EF6 - automatic precision
public decimal Price { get; set; }  // Became decimal(18,2) in database

// EF Core - must specify precision
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Product>()
        .Property(p => p.Price)
        .HasPrecision(18, 2);  // Or use [Column(TypeName = "decimal(18,2)")]
}
```

**Register configurations:** Replace `modelBuilder.Configurations.Add(...)` with `modelBuilder.ApplyConfiguration(...)` or use `modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly)` to apply all at once.

### Step 6: Handle Breaking API Changes

**Raw SQL:** Replace `Database.SqlQuery<T>()` with `Set<T>().FromSql()` and `Database.ExecuteSqlCommand()` with `Database.ExecuteSql()`. Use interpolated strings (`$"..."`) for automatic parameterization — do not use string concatenation, which bypasses EF Core's parameterization and creates SQL injection vulnerabilities.

**Lazy loading:** EF Core disables lazy loading by default.
- Option 1: Install `Microsoft.EntityFrameworkCore.Proxies`, call `UseLazyLoadingProxies()`
- Option 2 (preferred): Use explicit `.Include()` for eager loading

**Complex types:** In EF Core 7 and earlier, `[ComplexType]` → `OwnsOne()`. EF Core 8+ reintroduces native support — use `ComplexProperty()` instead.

**Validation:** EF Core does not call `IValidatableObject.Validate()` on `SaveChanges()`. Add validation in the application layer or override `SaveChanges()` to call it explicitly.

### Step 7: Establish the EF Core Migrations Baseline

**First — check whether anything else still writes this database.** If any other application,
job, or deployment pipeline still writes this schema — a .NET Framework host, another service, a
SQL Agent job, a DACPAC or DbUp pipeline — stop and follow `managing-shared-database-schema`.
The steps below assume EF Core is taking over as the sole schema owner.

Advise the user to apply all pending EF6 migrations to their database before starting the EF Core migration — this is a manual prerequisite. Once confirmed:

1. **Keep the existing `Migrations/` folder in place.** It is the EF6 schema ledger and remains
   the record of what was applied. It is removed only at full cutover, under
   `ref/cutover-teardown.md`. If EF6 and EF Core migration types collide in the same project,
   exclude the EF6 folder from compilation — `<Compile Remove="Migrations\**" />` in an
   SDK-style project — rather than deleting it. **Renaming the folder does not exclude it:**
   SDK-style projects glob `**/*.cs`, so `Migrations.EF6/` still compiles, and in a non-SDK
   project renaming breaks the explicit `<Compile Include>` items instead. If you keep the
   folder compiled, keep the `EntityFramework` package reference too (Step 2).
2. Create a baseline EF Core migration **in its own folder, outside `Migrations/`**. Present the
   command and offer to run it with user confirmation:

```powershell
dotnet ef migrations add InitialCreate --output-dir EFCoreMigrations
```

> **Scaffold outside `Migrations/`, not into a subfolder of it.** EF Core defaults to
> `Migrations/` — the same folder holding the EF6 `DbMigration` classes and `Configuration`.
> Two reasons a sibling folder is required rather than, say, `Migrations/EFCore`:
>
> - The exclusion glob in Step 2 (`Migrations\**`) is **recursive**. A nested
>   `Migrations/EFCore/` would be excluded from compilation along with the EF6 files, silently
>   leaving the project with no EF Core migrations at all.
> - Any later "delete the `Migrations/` folder" teardown would take the EF Core snapshot with it.
>
> Keeping the two ledgers in sibling directories means neither instruction has to know about
> the other.

3. **Before marking the migration as applied, the user must verify that the existing database schema matches the EF Core model.** The user should review the generated migration file (`InitialCreate`) and confirm it reflects their current schema — not new changes. If the migration contains unexpected schema alterations, applying it could cause data loss. Only proceed once the user confirms the migration is a clean baseline.

4. Once verified, register the migration as applied **without executing it**. Generate a script and apply only the history insert:

```powershell
dotnet ef migrations script --idempotent --output mark-migration.sql
```

Advise the user to review the generated script and apply **only** its history-table portions: the conditional `CREATE TABLE ... __EFMigrationsHistory` block at the top of the script, followed by the `INSERT INTO __EFMigrationsHistory` statement. This registers the migration as applied without modifying the application schema.

> **Do not skip the `CREATE TABLE` block.** A database that has only ever been managed by EF6
> has no `__EFMigrationsHistory` table — that is EF Core's ledger, and EF6 never created it.
> Applying the `INSERT` on its own therefore fails with an invalid-object error. The
> `--idempotent` script guards the create with an existence check, so running both statements
> is safe whether or not the table is already there.

> **Do not use `dotnet ef database update` for this.** That command *executes* the migration —
> EF Core has no `--fake` / "mark as applied" option. Against a live database whose schema
> already exists, `InitialCreate` attempts to create every table and fails, or worse, partially
> applies. The script approach above is the only safe way to baseline an existing database.

**Always pause and confirm with the user before executing any database-modifying command**,
even in auto-execute mode — these connect to a live database.

EF6's `dbo.__MigrationHistory` and EF Core's `dbo.__EFMigrationsHistory` are separate tables
and do not interfere. Leave the EF6 table in place; never copy rows between them.

### Step 8: Validate

Compilation alone is not sufficient. Many EF6→EF Core differences only surface at runtime.

**LLM responsibilities (automated):**
1. Build the project and fix compilation errors
2. Confirm no EF6 code artifacts remain in the migrated code paths (`System.Data.Entity` namespaces, `EntityFramework` package reference). **The `Migrations/` folder and `Configuration.cs` are expected to still be present** — they are removed only at full cutover.
3. Run existing unit tests

**User responsibilities (require database access and manual verification):**
4. Test CRUD operations against the database
5. Verify navigation property loading works as expected (eager vs lazy)
6. Validate change tracking behavior matches expectations (Added, Modified, Deleted states)
7. If stored procedures were migrated, test `FromSql`/`ExecuteSql` results
8. Compare query results with the EF6 version for any complex LINQ queries

Present items 4–8 as a checklist for the user — the LLM cannot validate runtime database behavior.

### Step 9: Full-Cutover Teardown

**Conditional — do not run this during a side-by-side window.** Removing the EF6 assets is
correct only once the old host is retired and nothing else writes this database.

- If any other host, job, or pipeline still writes this schema: **stop here.** Steps 1–8 are the
  whole job. The EF6 `Migrations/` folder, the `Configuration` class, and `dbo.__MigrationHistory`
  all stay, and governance for the window belongs to `managing-shared-database-schema`.
- Once the old host is confirmed retired, follow `ref/cutover-teardown.md` — it carries the
  confirmation gate and the removal inventory.

## Success Criteria

### Code migration (always required)

- `EntityFramework` NuGet package removed from application code paths, EF Core packages added
  (the reference is retained if EF6 migration assets are still compiled — see Step 2)
- All `System.Data.Entity` namespaces in **application code** replaced with
  `Microsoft.EntityFrameworkCore`; a retained EF6 `Migrations/` folder is exempt
- `DbContext` constructor accepts `DbContextOptions`
- All `EntityTypeConfiguration<T>` classes converted to `IEntityTypeConfiguration<T>`
- Fluent API calls updated (`HasRequired` → `HasOne`, etc.)
- Raw SQL calls migrated to `FromSql`/`ExecuteSql`
- Lazy loading strategy decided and configured
- Complex types converted to owned entities or complex properties
- Database initializer calls removed and seed data migrated to `HasData()` or a seed method
- Project builds, tests pass, queries return correct results
- No secrets are stored in plain text

### Schema-deployment handover (EF Core takes ownership)

Applies only once EF Core owns schema deployment — by default at cutover, and during a
side-by-side window only under the ownership override in `managing-shared-database-schema`
(Step 2). While EF6 still owns the pen, EF Core is the non-owner and **never scaffolds a
migration**, so neither criterion below applies and neither is a gap:

- EF Core migrations baseline created and registered via the history-insert script
- Adding a new EF Core migration produces one without any operations (model matches database)

### Side-by-side freeze (old host still live)

- EF6 `Migrations/` folder and `Configuration` class **retained**
- EF6 `dbo.__MigrationHistory` retained and untouched
- Schema ownership decided per `managing-shared-database-schema`
- No startup migration enabled in either host

### Full cutover only (old host retired)

Applies only after the confirmation gate in `ref/cutover-teardown.md`:

- EF6 `Migrations/` folder and `Configuration` class removed
- No EF6 artifacts remain anywhere in the solution

