If you have ever profiled an EF Core application and found DetectChanges burning time you did not expect, or wired up a convoluted IServiceCollection override just to swap your database provider in tests, Preview 3 has a few things for you. The April 14 release continues the pragmatic trajectory of EF Core 11 — no headline rewrites, but a stack of changes that remove friction from code you write every day.

The headliners are a new ChangeTracker API that sidesteps DetectChanges entirely, a migration configuration that lets you exclude foreign keys from generated scripts, and a snapshot-based check that catches diverged migration histories before they cause problems. There is also a meaningful SQL generation improvement, SQL Server 2025 JSON support, and a handful of breaking changes that are worth knowing about before you upgrade.

GetEntriesForState: change tracking without DetectChanges

The existing ChangeTracker.Entries() method calls DetectChanges() internally every time you invoke it. For most applications this is fine — the scan is fast enough that you never notice. But in long-lived contexts, batch processing pipelines, or anywhere you are tracking thousands of entities, that implicit scan adds up. If you already know which states you care about, the scan is pure waste.

Preview 3 adds GetEntriesForState, a method that returns tracked entities filtered by state without triggering change detection:

Services/AuditService.cs
public class AuditService(ApplicationDbContext db)
{
    public IReadOnlyList<AuditRecord> BuildAuditLog()
    {
        var modified = db.ChangeTracker.GetEntriesForState(modified: true);
        var deleted = db.ChangeTracker.GetEntriesForState(deleted: true);

        var records = new List<AuditRecord>();

        foreach (var entry in modified)
        {
            records.Add(new AuditRecord
            {
                EntityType = entry.Metadata.ClrType.Name,
                Action = "Modified",
                Timestamp = DateTimeOffset.UtcNow,
                Changes = entry.Properties
                    .Where(p => p.IsModified)
                    .Select(p => $"{p.Metadata.Name}: {p.OriginalValue} -> {p.CurrentValue}")
                    .ToList()
            });
        }

        foreach (var entry in deleted)
        {
            records.Add(new AuditRecord
            {
                EntityType = entry.Metadata.ClrType.Name,
                Action = "Deleted",
                Timestamp = DateTimeOffset.UtcNow
            });
        }

        return records;
    }
}

The API has both generic and non-generic overloads. The generic version filters to a specific entity type:

Example.cs
var pendingOrders = db.ChangeTracker
    .GetEntriesForState<Order>(added: true, modified: true);

All four parameters — added, modified, deleted, unchanged — default to false, so you only opt in to the states you need.

// WARNING

Because GetEntriesForState skips DetectChanges, the entries it returns reflect the tracker's current snapshot. If you have modified entity properties since the last DetectChanges call and have not called it yourself, those modifications will not be reflected. This is the entire point of the API — but it means you need to be deliberate about when you use it versus Entries().

When to use which

Use Entries() when you need the tracker to be fully up to date and performance is not a concern. Use GetEntriesForState when you are in a hot path, already know the tracker is current (perhaps you just called SaveChanges, which triggers DetectChanges internally), or simply need a fast read of what the tracker already knows.

ExcludeForeignKeyFromMigrations

Not every foreign key in your model should exist in the database. Cross-boundary relationships — where two entities live in the same model for navigation convenience but are owned by different services or deployed to different schemas — are a common example. You might model the relationship for EF Core's query generation but not want the migration to create an actual FOREIGN KEY constraint.

Previously, the workaround was to configure the relationship and then manually edit the generated migration to remove the constraint. Preview 3 makes this a first-class configuration:

Data/OrderConfiguration.cs
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
    public void Configure(EntityTypeBuilder<Order> builder)
    {
        builder.HasOne(o => o.Customer)
            .WithMany(c => c.Orders)
            .HasForeignKey(o => o.CustomerId)
            .ExcludeForeignKeyFromMigrations(true);
    }
}

EF Core still understands the relationship for queries and change tracking. It simply will not emit the foreign key constraint in the migration script. This is useful in several scenarios:

// TIP

This does not affect how EF Core generates joins or handles cascade deletes in memory. It only controls whether the constraint appears in the migration output.

Migration tree divergence detection

Anyone who has worked on a team with parallel feature branches has hit this: two developers add migrations at the same time, both branching from the same model snapshot. When both branches merge, the migration history diverges — and EF Core may not notice until you try to apply them to a production database and get an error about a missing column or a conflicting table.

Preview 3 addresses this by recording the latest migration identifier in the model snapshot itself. When you run dotnet ef migrations add, EF Core now checks whether the snapshot's recorded migration matches the latest migration in your project. If it does not — because another migration was added on a different branch — you get an early warning instead of a silent divergence.

This is not a merge-conflict resolver. It is a fence that trips before the damage is done. If you see the warning, the fix is the same as it always was: delete one of the conflicting migrations, rebuild the snapshot from the merged state, and re-add your changes. The difference is that you find out at development time rather than deployment time.

Leaner SQL for to-one joins

EF Core's query pipeline has a habit of generating joins that are technically correct but unnecessary. Preview 3 improves the SQL generation for to-one relationships by eliminating redundant joins when the required data can be satisfied without them.

Consider a query that loads orders with their status:

Example.cs
var orders = await db.Orders
    .Where(o => o.CreatedDate > cutoff)
    .Select(o => new
    {
        o.Id,
        o.Total,
        o.Status.Name
    })
    .ToListAsync();

If Status is a required to-one navigation and the StatusId foreign key already exists on Order, previous versions would sometimes generate an unnecessary JOIN to the Statuses table even when the query could be satisfied with just the foreign key column. Preview 3 is better at recognising these cases and dropping the join.

The improvement is automatic — no code changes required. If you have been hand-writing raw SQL for performance-sensitive queries because of unnecessary joins, it is worth re-evaluating whether the LINQ version now generates acceptable SQL.

SQL Server 2025 JSON support

EF Core 11 adds EF.Functions.JsonContains() for SQL Server 2025, which translates to the native JSON_CONTAINS function:

Example.cs
var results = await db.Products
    .Where(p => EF.Functions.JsonContains(p.Tags, "\"electronics\""))
    .ToListAsync();

The method that appeared in earlier previews under the name JsonPathExists has been renamed to better reflect what it does. If you were using the earlier preview API, update your call sites — the old name will not compile.

// NOTE

JsonContains targets SQL Server 2025 specifically. If you are running against an older SQL Server version, the call will fail at runtime with a SQL syntax error.

RemoveDbContext and RemoveExtension

Test projects that need to replace the production database provider with SQLite or an in-memory database have always required a slightly awkward dance:

Example.cs
// Bad — the production provider is still registered
services.AddDbContext<AppDbContext>(options =>
    options.UseSqlite(connectionString));

The problem is that AddDbContext does not remove the existing registration. If the production code already called AddDbContext<AppDbContext> with UseSqlServer, you end up with two provider registrations and the last one wins — most of the time. The behaviour is fragile and has caused subtle test failures for years.

Preview 3 adds RemoveDbContext<TContext>() and RemoveExtension<TExtension>() to clean up existing registrations before adding new ones:

Tests/TestFixture.cs
public class TestFixture
{
    public ServiceProvider BuildTestServices()
    {
        var services = new ServiceCollection();

        // Register production services
        ProductionStartup.ConfigureServices(services);

        // Cleanly replace the database provider
        services.RemoveDbContext<AppDbContext>();
        services.AddDbContext<AppDbContext>(options =>
            options.UseSqlite("Data Source=:memory:"));

        return services.BuildServiceProvider();
    }
}

This also works with AddPooledDbContextFactory, which gains a parameterless overload in Preview 3. If your configuration already lives in a shared ConfigureDbContext<TContext>() method, you no longer need to pass the options action a second time.

Breaking changes worth knowing

Preview 3 ships several breaking changes that are worth flagging before you upgrade:

MigrationsNotFound now throws. The RelationalEventId.MigrationsNotFound event previously logged an informational message when no migrations assembly was found. It now throws an InvalidOperationException by default. If you have a legitimate reason to run without migrations — a read-only context, for instance — you will need to suppress the event explicitly:

Data/ReadOnlyDbContext.cs
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
    options.ConfigureWarnings(w =>
        w.Ignore(RelationalEventId.MigrationsNotFound));
}

EFOptimizeContext is gone. The EFOptimizeContext MSBuild property has been removed. If you were using it to control compiled model or precompiled query generation, replace it with the more granular EFScaffoldModelStage and EFPrecompileQueriesStage properties.

SqlVector requires explicit projection. SqlVector properties are no longer loaded automatically when you query an entity. You must explicitly project them in your Select clause. This avoids loading potentially large vector columns when they are not needed.

SqlClient jumps to 7.0. Microsoft.Data.SqlClient has been updated to version 7.0.0. If you depend on SqlClient directly — for raw ADO.NET queries, custom connection management, or anything that references SqlClient types — check the SqlClient 7.0 release notes for breaking changes in that package.

Tools no longer pull in Design. The Microsoft.EntityFrameworkCore.Tools and Microsoft.EntityFrameworkCore.Tasks packages no longer directly depend on the Design package. If your build relies on that transitive dependency, add an explicit reference to Microsoft.EntityFrameworkCore.Design.

Common pitfalls

Using GetEntriesForState when the tracker is stale. The whole point of this API is that it does not call DetectChanges. If you modify properties on tracked entities and then call GetEntriesForState(modified: true), those entities may not appear because the tracker has not been told about the changes yet. Call DetectChanges() explicitly first if you need fresh state, or use the standard Entries() method.

Forgetting to update MigrationsNotFound handling. The silent-to-throwing change will break any application that runs without a migrations assembly and does not suppress the warning. This is most common in test projects that use EnsureCreated() instead of migrations, or in read-only bounded contexts that intentionally skip migration support.

Assuming ExcludeForeignKeyFromMigrations disables the relationship. The configuration only affects the migration output. EF Core still tracks the relationship, generates joins for it, and enforces referential integrity in the change tracker. If you want to remove the relationship entirely, remove the navigation properties and foreign key configuration from your model.

Summary