EF Core 11 ships alongside .NET 11, and it requires the .NET 11 SDK to build and the .NET 11 runtime to run; there’s no running it on .NET 10 or earlier, and definitely not on .NET Framework.
The verdict up front: for most apps, this is a quiet upgrade. The breaking changes[2] are mostly edge cases: sync I/O that was already deprecated, a SQL Server compatibility level bump, a couple of defaults changing. If you’re on Cosmos DB, on vector columns, or still calling synchronous EF APIs, read the next section carefully before you touch the package reference. Everyone else can skim it and move on to the good stuff.
This post does two things. The first half is a straight upgrade guide: what breaks, what’s new, a checklist. The second half is a deep dive into Complex Types, which I think is the most consequential change in this release even though it’s not technically new in EF Core 11. EF Core 11 is what finally makes Complex Types usable for the kind of modeling I actually want to do. Use the table of contents to jump to whichever one you came for.
Before you upgrade: what will break
Everyone
- Sync I/O APIs now always throw. This was already the case for the Azure Cosmos DB provider since EF Core 9, where calling something like
ToList()orSaveChanges()threw unless you’d opted back into “sync-over-async” blocking. In EF Core 11, that opt-out is gone entirely; sync I/O via Cosmos always throws, full stop. Convert any remaining sync calls (SaveChanges(),ToList(),First(), and so on) to their async equivalents (SaveChangesAsync(),ToListAsync(),FirstAsync()). This specific change is scoped to the Cosmos provider; relational providers (SQL Server, SQLite, etc.) still allow sync I/O. - EF throws by default when
Migrate()finds no migrations. Previously this logged an informational message and did nothing. Now it throws, on the theory that callingMigrate()with zero migrations in the assembly usually means something’s misconfigured. If you genuinely manage schema outside of EF migrations, either drop theMigrate()call or suppress it:options.ConfigureWarnings(w => w.Ignore(RelationalEventId.MigrationsNotFound)).
SQL Server users
UseSqlServernow defaults to compatibility level 160 (SQL Server 2022), up from 150 (SQL Server 2019). This lets EF generate SQL using newer features likeLEAST/GREATESTby default. If your database is still on SQL Server 2019 or older, or just runs at a lower compatibility level, some of the SQL EF generates may not work against it. Pin the old behavior explicitly:optionsBuilder.UseSqlServer(connectionString, o => o.UseCompatibilityLevel(150));Microsoft.Data.SqlClientmoves to 7.0, which strips Azure/Entra ID authentication dependencies (Azure.Core,Azure.Identity,Microsoft.Identity.Client) out of the core package to cut dependency bloat. If your app authenticates with anything likeActiveDirectoryDefault,ActiveDirectoryInteractive,ActiveDirectoryManagedIdentity, orActiveDirectoryServicePrincipal, you now need to addMicrosoft.Data.SqlClient.Extensions.Azureas a separate package reference. No code changes needed beyond that: just the extra package. Also,SqlAuthenticationMethod.ActiveDirectoryPasswordis now obsolete; migrate off it if you’re still using it.
Vector users
SqlVector<T>properties are no longer included inSELECTwhen materializing entities. Vector columns can hold hundreds or thousands of floats, and in most cases you write them once and only ever use them for search, never read them back, so EF stops pulling them back by default. The property comes backnullon the loaded entity unless you project for it explicitly:var embeddings = await context.Blogs .Select(b => new { b.Id, b.Embedding }) .ToListAsync();Vectors still work fine in
WHERE/ORDER BY, including withVectorDistance()andVectorSearch(); they just won’t ride along on the entity projection. The docs note an opt-in to restore automatic loading is coming later in the EF Core 11 release, so this may get easier before GA.
Cosmos DB users (the biggest change)
This is where most of the real risk lives if you’re on Cosmos.
Illegal
idcharacters are no longer escaped. When generating a Cosmosidfrom a composite key, EF used to escape characters that are illegal in Cosmos resource ids (/→^2F,\→^5C,?→^3F,#→^23). That scheme had a real bug: the escape character^was never itself escaped, so a key value containing the literal string^2Fcollided with a key value containing/; two different entities silently mapped to the same document. EF Core 11 fixes this by just not escaping those characters at all; the raw key values go into theid.The catch: if you have existing data, EF generated those ids with the old escaped format (e.g.
Post|1|^2F). After upgrading, EF will look forPost|1|/instead and won’t find the existing document. You have two options: re-key/migrate the existing documents to the new unescaped format, or opt back into the old (buggy) escaping behavior as a stopgap:AppContext.SetSwitch("Microsoft.EntityFrameworkCore.EscapeIllegalCosmosIdCharacters", true);Only do the latter if you can’t migrate right away; the id-collision bug is still there if you do.
Unmapped JSON properties are no longer preserved on round-trip. This follows from the Cosmos provider dropping its
Newtonsoft.Json-based serialization (and the__jObjectshadow property along with it) in favor ofSystem.Text.Json. If your documents have fields outside your EF model that you were relying on EF to read-and-rewrite transparently, those fields will now be dropped on save. Map the fields explicitly in your model, or go around EF withCosmosClientdirectly for documents where you need full control over the JSON shape.Undefined projections now throw instead of silently misbehaving. Previously, projecting through an optional navigation where part of the path was missing in the document gave you inconsistent results: single-property projections silently dropped rows, multi-property projections threw a confusing
InvalidOperationException. Now it’s consistently anInvalidOperationExceptioneither way. UseEF.Functions.IsDefined(...)to filter, orEF.Functions.CoalesceUndefined(...)to supply a default.Smaller Cosmos changes worth knowing about: empty owned collections now materialize as an empty collection instead of
null; the discriminator property is namedDiscriminatorin the model again (while the JSON property name stays$typeby default, so the model name and JSON name are now decoupled); and floating-point values are truncated rather than rounded when materializing into fixed-point types.
Build and tooling
- The
EFOptimizeContextMSBuild property is gone. Code generation (compiled models, precompiled queries) is now controlled entirely viaEFScaffoldModelStageandEFPrecompileQueriesStage, set topublishorbuild. If you havePublishAOTset totrue, code generation at publish time happens automatically without any of this. Microsoft.EntityFrameworkCore.Toolsand.Tasksno longer pull inMicrosoft.EntityFrameworkCore.Design. If your project was relying on that dependency coming along for free via the tools package, add it directly:<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="11.0.0" PrivateAssets="all" />
What you get for the trouble
Prettier SQL for reference navigations
This is the post’s title payoff, so it gets the most space. EF Core 11 trims two kinds of waste out of queries that include to-one (reference) navigations.
First, split queries (AsSplitQuery()) used to join in reference navigations that the split statement didn’t actually need. Given:
var blogs = context.Blogs
.Include(b => b.BlogType)
.Include(b => b.Posts)
.AsSplitQuery()
.ToList();
the Posts split query used to unnecessarily join BlogType:
-- Before EF Core 11
SELECT [p].[Id], [p].[BlogId], [p].[Title], [b].[Id], [b0].[Id]
FROM [Blogs] AS [b]
INNER JOIN [BlogType] AS [b0] ON [b].[BlogTypeId] = [b0].[Id]
INNER JOIN [Post] AS [p] ON [b].[Id] = [p].[BlogId]
ORDER BY [b].[Id], [b0].[Id]
EF Core 11 prunes the join it doesn’t need:
-- EF Core 11
SELECT [p].[Id], [p].[BlogId], [p].[Title], [b].[Id]
FROM [Blogs] AS [b]
INNER JOIN [Post] AS [p] ON [b].[Id] = [p].[BlogId]
ORDER BY [b].[Id]
Second, EF no longer adds a reference navigation’s key to ORDER BY when it’s already functionally determined by the parent’s key via the foreign key:
-- Before EF Core 11
ORDER BY [b].[BlogId], [p].[PersonId]
-- EF Core 11
ORDER BY [b].[BlogId]
Neither change is just cosmetic. Microsoft’s own benchmarks show a 29% improvement for the split-query join pruning, and a 22% improvement for the ORDER BY trim on single queries; bigger wins the more reference navigations you’re including.
Complex types, finally unblocked
EF Core 11 lifts the restriction that kept Complex Types and JSON column mapping off entity types using TPT (table-per-type) or TPC (table-per-concrete-type) inheritance, adds lambda-chained configuration, and adds keys/indexes on complex-type properties. This is the headline change for me personally, so it gets its own section below: Complex Types as value objects.
FullJoin
.NET 11 adds FullJoin as a first-class LINQ operator, and EF Core 11 translates it to a SQL FULL JOIN:
var results = await context.Customers
.FullJoin(
context.Orders,
c => c.Id,
o => o.CustomerId,
(c, o) => new { Customer = c, Order = o })
.ToListAsync();
SELECT [c].[Id], [c].[Name], [o].[Id], [o].[CustomerId], [o].[OrderDate]
FROM [Customers] AS [c]
FULL JOIN [Orders] AS [o] ON [c].[Id] = [o].[CustomerId]
This is the kind of thing you don’t need often, but when you do need it, hand-writing it in raw SQL was the only option before now.
Smaller wins worth knowing about
MaxByAsync/MinByAsynctranslation: find the entity with the max/min value of a key selector, rather than just the max/min value itself.EF.Functions.JsonPathExists(): checks whether a JSON path exists in a document, translating to SQL Server’sJSON_PATH_EXISTS.- Vector index support in migrations, via
HasVectorIndex(), plusVectorSearch()/WithApproximate()for approximate nearest-neighbor search against SQL Server 2025’sVECTOR_SEARCH(). GroupBytranslation improvements, including correct materialization of anonymous-type/DTO projections through left joins and grouping.dotnet ef database update --add: scaffolds and applies a migration in one step, useful for containerized or Aspire-style setups where recompiling isn’t really an option.
There’s more beyond this: full-text search table-valued functions, JSON_CONTAINS translation for SQL Server 2025, temporal period properties mapped to real CLR properties instead of shadow properties, and a pile of Cosmos DB features (transactional batches, bulk execution, session token management). The official What’s New in EF Core 11[1] page covers all of it.
Complex Types as value objects
A decade of circling back to the same problem
I’ve been using Entity Framework since the “full” .NET Framework days, and classic EF had a real ComplexType feature: you could define a reference-type class with no key of its own, map it to columns on the owner’s table, and use it for things like Address without EF pretending it was a separate entity. It had sharp edges (reference types only, no sharing the same instance across two properties without duplicating it), but it worked, and it modeled the right concept.
When EF Core shipped in 2016, complex types didn’t come with it. The replacement was owned entity types, introduced in EF Core 2.0, and owned types are, underneath, actual entity types. EF gives them a hidden key so it can track them, which means they inherit all the baggage that comes with identity: a shadow primary key you never asked for, change-tracking semantics built around that key, and (the part that bit people most often) an inability to point two properties at the same owned instance. For seven years, that was the best EF Core had.
Here’s that bug made concrete, using Address as the example. Take an Order with a billing and a shipping address that happen to be the same place:
[ComplexType]
public record Address(string Line1, string? Line2, string City, string Country, string PostCode);
public class Order
{
public int Id { get; set; }
public required Address BillingAddress { get; set; }
public required Address ShippingAddress { get; set; }
}
var address = new Address("221B Baker Street", null, "London", "UK", "NW1 6XE");
var order = new Order
{
BillingAddress = address,
ShippingAddress = address
};
context.Orders.Add(order);
await context.SaveChangesAsync();
With Address as an owned entity type (the only option before EF Core 8), Orders.Add(order) throws immediately, before SaveChangesAsync() even runs:
InvalidOperationException: The same object instance of type 'Address' is being referenced from the navigation 'BillingAddress' on 'Order' and from the navigation 'ShippingAddress' on 'Order'. Owned entity types cannot have multiple owners pointing to the same instance. Consider creating a copy of the object to use for each navigation.
EF sees two properties pointing at the same owned instance and reads that as the same hidden-keyed entity trying to belong to two owners at once, which it can’t allow. With Address as a complex type, the same code just works: there’s no hidden identity to collide over, so two properties holding equal values are simply two properties holding equal values. That gap between “this is obviously fine” and “EF throws an exception” is exactly the gap complex types close.
EF Core 8 finally brought real complex types back[3], as a genuine third category alongside primitive types and entity types: structured, multi-value, and explicitly not identified by a key. The general reference docs for the feature are worth bookmarking too[4]. EF Core 10 added JSON column mapping and complex collections (JSON only, with no dedicated table). EF Core 11 fills in what was still missing for anyone doing serious modeling with them: TPT/TPC inheritance support, lambda-chained configuration so you don’t have to thread through ComplexProperty() just to set a HasMaxLength, and keys/indexes on properties nested inside a complex type. That’s the combination that makes this, for me, the first EF Core release where modeling a value object doesn’t feel like working around the tool.
Why the entity/value distinction matters, DDD or not
You don’t need to buy into Domain-Driven Design as a methodology to care about this distinction. It answers a concrete question you’re already asking every time you add a property: does this thing have an identity I need to track independently of its data, or is it fully described by its current values?
A Customer has identity: two customers with identical names and addresses are still two different customers, and if one of them changes their address, that doesn’t affect the other. An Address or a PhoneNumber, on the other hand, is usually just a value: two instances with the same data are the same address, there’s no meaningful sense in which they’re “different objects that happen to agree,” and nothing is lost by replacing one with another that has equal data. Model the first kind as an entity and the second kind as a value, and most of your equality and mutation bugs around these supporting types just don’t happen: there’s no separate identity to go stale, no shared row that two different owners are secretly both pointing at.
Model a value as an entity instead (which is exactly what owned types forced you into for years) and you get the opposite: a hidden key implies an identity that doesn’t exist, which is how you end up debugging why updating one customer’s address silently didn’t touch another customer’s identical-looking address, or why EF threw an error because the same Address instance got reused in two places.
Modeling a PhoneNumber
Address already did its job above: it’s the example that makes the owned-type bug concrete. For the rest of this section, where I want to walk through TPT mapping, lambda-chained configuration, and indexing a nested property, I’m reaching for PhoneNumber instead. I’m deliberately not reaching for Money, which drags in currency conversion and exchange rates, a whole separate problem I don’t want this example wading into. PhoneNumber has real international modeling wrinkles of its own: country codes, variable-length national numbers, formatting that differs by locale, without dragging in anything outside what a value-object example needs to show.
[ComplexType]
public record PhoneNumber(int CountryCode, string NationalNumber)
{
public override string ToString() => $"+{CountryCode} {NationalNumber}";
}
An immutable record is the right default shape for a complex type. Complex type instances can be shared across multiple properties on the same entity, and if the shared instance were mutable, changing it through one property would silently change it everywhere else it’s referenced too. Making it immutable (a record, a readonly record struct, or a plain class with an all-args constructor and no setters) sidesteps that entirely: “updating” a phone number means assigning a new instance, never mutating the one you have.
While we’re at it, a person’s name shouldn’t be a bare string either. It is less obviously a value object than a phone number, since names don’t have the same structural rules across cultures, but even a trivial two-field version beats a raw primitive for something with this much latent structure:
[ComplexType]
public record PersonName(string First, string Last)
{
public override string ToString() => $"{First} {Last}";
}
Now a couple of entities that use both value objects, mapped with TPT inheritance (the thing EF Core 11 specifically unblocks):
public abstract class Person
{
public int Id { get; set; }
public required PersonName Name { get; set; }
public required PhoneNumber Phone { get; set; }
}
public class Customer : Person
{
public string Company { get; set; } = string.Empty;
}
public class Employee : Person
{
public DateOnly HireDate { get; set; }
}
Configuring it: lambda chaining, keys, and TPT
Before EF Core 11, configuring a property nested inside a complex type meant calling ComplexProperty() first to get its builder, then Property() on that:
// Before EF Core 11
modelBuilder.Entity<Person>()
.ComplexProperty(p => p.Phone)
.Property(p => p.NationalNumber)
.HasMaxLength(15);
EF Core 11 lets you chain straight through the member access instead:
modelBuilder.Entity<Person>()
.Property(p => p.Phone.NationalNumber)
.HasMaxLength(15);
Indexes work the same way now, which matters if you want to look customers up by phone number (something you couldn’t do against a nested complex type property before EF Core 11):
modelBuilder.Entity<Person>()
.HasIndex(p => p.Phone.NationalNumber);
And then the TPT mapping itself, which is the part that genuinely didn’t work before this release:
modelBuilder.Entity<Person>().UseTptMappingStrategy();
This mirrors the same mechanism EF Core 11’s docs demonstrate with an Animal/AnimalDetails example: EF creates Name_First, Name_Last, Phone_CountryCode, and Phone_NationalNumber columns on the Person table (the TPT base table), with Customer and Employee each getting their own table for just the columns specific to them. Before EF Core 11, adding a complex type to a TPT- or TPC-mapped hierarchy at all would fail: this was a hard blocker, not a rough edge. If you’d rather store the phone number as JSON instead of flattened columns, the same property maps that way too, TPT and all: .ComplexProperty(p => p.Phone, b => b.ToJson()).
Cosmos gets complex types too
Worth calling out on its own, since it’s a genuinely separate code path: complex types were not supported at all on the Azure Cosmos DB provider before EF Core 11. As of this release they are, embedded as nested JSON objects (or arrays, for complex collections) directly inside the document, with no separate configuration dance and no owned-type workaround needed there either. If you’re modeling value objects against Cosmos, this closes the same gap it closes on the relational side.
The gotchas
- Complex types must be explicitly configured. Unlike owned types, EF won’t discover a complex type by convention: you need
[ComplexType]orComplexProperty()/ComplexCollection()inOnModelCreating. - No
DbSet<T>of a complex type. They only ever exist as part of an owning entity; querying one directly means projecting it off its owner (context.People.Select(p => p.Phone)), not querying it on its own. - Collections of complex types only map to JSON.
ComplexCollection()works, but only onto a single JSON column viaToJson(); there’s still no way to put a complex-type collection into its own table the way you could with an owned-type collection. - Shared, mutable instances are still a footgun. Nothing stops you from making a complex type mutable, but if you do and then share one instance across two properties, mutating it through either property changes both. Immutability isn’t enforced: it’s just the pattern that avoids the problem.
Upgrade checklist
- Search for sync calls (
ToList(),SaveChanges(),First(), etc.) against Cosmos DB and convert them to async. - Check your SQL Server version and compatibility level against 160; pin to 150 explicitly if you’re not on SQL Server 2022 or later.
- Confirm your migrations assembly is actually being found, or suppress
RelationalEventId.MigrationsNotFoundif you intentionally run without migrations. - If you use Cosmos DB, plan for the
id-escaping change before upgrading: decide whether you’re migrating existing data or using theAppContextswitch as a stopgap. - If you use vectors, review any code that reads
SqlVector<T>properties off loaded entities; add explicit projections where needed. - If you’re on Cosmos DB and relied on unmapped JSON properties surviving a round-trip, map those properties explicitly now.
- If you have owned JSON collections without an explicit key, plan to migrate them to
ComplexCollection; they’re obsolete as of EF Core 11. - Run your test suite and diff the generated SQL for your hot-path queries; the compatibility level bump and the join/ORDER BY pruning can both change query plans.
Wrap-up
Most of EF Core 11’s breaking changes are narrow (sync I/O, a compatibility level default, a SqlClient major version bump) and will cost most apps little. The real work is concentrated on Cosmos DB, vectors, and sync I/O, so budget your testing time accordingly. This is all based on docs for a release still in development, so some of it could shift before EF Core 11 ships with .NET 11.
The pruned joins and trimmed ORDER BY clauses for reference navigations are worth a closer look on their own, and I may come back to the SQL generation improvements in a follow-up post. But if you only take one thing from this post, take the Complex Types section above: it’s the reason I’m actually excited about this release.
References
- Microsoft. What’s New in EF Core 11. Microsoft Learn, last updated August 2026.
- Microsoft. Breaking changes in EF Core 11 (EF11). Microsoft Learn, last updated July 2026.
- Microsoft. What’s New in EF Core 8: Complex types. Microsoft Learn.
- Microsoft. Complex Types. Microsoft Learn.
- Microsoft. Getting the generated SQL. Microsoft Learn.
