<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Efcore on Russell ‛Russ’ Frith</title><link>https://russfrith.com/tags/efcore/</link><description>Recent content in Efcore on Russell ‛Russ’ Frith</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Tue, 20 Oct 2026 14:26:11 -0400</lastBuildDate><atom:link href="https://russfrith.com/tags/efcore/index.xml" rel="self" type="application/rss+xml"/><item><title>Upgrading to EF Core 11: Prettier SQL and a Few Ugly Changes</title><link>https://russfrith.com/posts/upgrading-to-ef-core-11/</link><pubDate>Tue, 20 Oct 2026 14:26:11 -0400</pubDate><guid>https://russfrith.com/posts/upgrading-to-ef-core-11/</guid><description>A practical guide to moving from EF Core 10, plus a deep dive into Complex Types and why value objects matter</description><content:encoded><![CDATA[<aside
  style="
    margin: 20px 0;
    padding: 0 14px;
    border-inline-start: 3px solid var(--primary);
  "
>
  <p>EF Core 11 is still in development as of this writing (based on the Microsoft Learn docs for EF Core 11, last updated August 2026). Some of what&rsquo;s below could change before release. I tested against the <code>mcr.microsoft.com/dotnet/nightly/sdk:11.0-preview</code> image (resolving to SDK <code>11.0.100-rc.1.26411.119</code>) and <code>Microsoft.EntityFrameworkCore.SqlServer</code> <code>11.0.0-preview.6.26359.118</code>; see the <a href="https://github.com/russfrith/russfrith-blog/tree/master/companion-code/upgrading-to-ef-core-11">companion code</a> for the runnable version of every SQL example below.</p>
</aside>

<p>EF Core 11 ships alongside .NET 11, and it requires the .NET 11 SDK to build and the .NET 11 runtime to run; there&rsquo;s no running it on .NET 10 or earlier, and definitely not on .NET Framework.</p>
<p>The verdict up front: for most apps, this is a quiet upgrade. The breaking changes[<a href="#references">2</a>] are mostly edge cases: sync I/O that was already deprecated, a SQL Server compatibility level bump, a couple of defaults changing. If you&rsquo;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.</p>
<p>This post does two things. The first half is a straight upgrade guide: what breaks, what&rsquo;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&rsquo;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.</p>
<h2 id="before-you-upgrade-what-will-break">Before you upgrade: what will break</h2>
<aside
  style="
    margin: 20px 0;
    padding: 0 14px;
    border-inline-start: 3px solid var(--primary);
  "
>
  <p>What I actually hit while building the companion code: <code>ToQueryString()</code>[<a href="#references">5</a>] only returns the SQL for a split query&rsquo;s <em>first</em> statement; it won&rsquo;t show you the pruned join from the <a href="#prettier-sql-for-reference-navigations">Prettier SQL</a> section unless you execute against a real database and capture the command log. Not a breaking change, just a gap in the tooling worth knowing about if you&rsquo;re trying to eyeball query changes the same way. The <a href="#sql-server-users">compatibility-level default change</a> also surprised me a bit: pinning back to 150 doesn&rsquo;t make <code>Math.Min</code> throw, it just silently falls back to client evaluation and pulls both raw columns instead of translating to <code>LEAST</code>. Worth checking your logs for unexpected client-eval warnings after upgrading, not just outright errors.</p>
</aside>

<h3 id="everyone">Everyone</h3>
<ul>
<li><strong>Sync I/O APIs now always throw.</strong> This was already the case for the Azure Cosmos DB provider since EF Core 9, where calling something like <code>ToList()</code> or <code>SaveChanges()</code> threw unless you&rsquo;d opted back into &ldquo;sync-over-async&rdquo; 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 (<code>SaveChanges()</code>, <code>ToList()</code>, <code>First()</code>, and so on) to their async equivalents (<code>SaveChangesAsync()</code>, <code>ToListAsync()</code>, <code>FirstAsync()</code>). This specific change is scoped to the Cosmos provider; relational providers (SQL Server, SQLite, etc.) still allow sync I/O.</li>
<li><strong>EF throws by default when <code>Migrate()</code> finds no migrations.</strong> Previously this logged an informational message and did nothing. Now it throws, on the theory that calling <code>Migrate()</code> with zero migrations in the assembly usually means something&rsquo;s misconfigured. If you genuinely manage schema outside of EF migrations, either drop the <code>Migrate()</code> call or suppress it: <code>options.ConfigureWarnings(w =&gt; w.Ignore(RelationalEventId.MigrationsNotFound))</code>.</li>
</ul>
<h3 id="sql-server-users">SQL Server users</h3>
<ul>
<li><strong><code>UseSqlServer</code> now defaults to compatibility level 160</strong> (SQL Server 2022), up from 150 (SQL Server 2019). This lets EF generate SQL using newer features like <code>LEAST</code>/<code>GREATEST</code> by 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: <code>optionsBuilder.UseSqlServer(connectionString, o =&gt; o.UseCompatibilityLevel(150));</code></li>
<li><strong><code>Microsoft.Data.SqlClient</code> moves to 7.0</strong>, which strips Azure/Entra ID authentication dependencies (<code>Azure.Core</code>, <code>Azure.Identity</code>, <code>Microsoft.Identity.Client</code>) out of the core package to cut dependency bloat. If your app authenticates with anything like <code>ActiveDirectoryDefault</code>, <code>ActiveDirectoryInteractive</code>, <code>ActiveDirectoryManagedIdentity</code>, or <code>ActiveDirectoryServicePrincipal</code>, you now need to add <code>Microsoft.Data.SqlClient.Extensions.Azure</code> as a separate package reference. No code changes needed beyond that: just the extra package. Also, <code>SqlAuthenticationMethod.ActiveDirectoryPassword</code> is now obsolete; migrate off it if you&rsquo;re still using it.</li>
</ul>
<h3 id="vector-users">Vector users</h3>
<ul>
<li>
<p><strong><code>SqlVector&lt;T&gt;</code> properties are no longer included in <code>SELECT</code> when materializing entities.</strong> 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 back <code>null</code> on the loaded entity unless you project for it explicitly:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> embeddings = <span style="color:#66d9ef">await</span> context.Blogs
</span></span><span style="display:flex;"><span>    .Select(b =&gt; <span style="color:#66d9ef">new</span> { b.Id, b.Embedding })
</span></span><span style="display:flex;"><span>    .ToListAsync();
</span></span></code></pre></div><p>Vectors still work fine in <code>WHERE</code>/<code>ORDER BY</code>, including with <code>VectorDistance()</code> and <code>VectorSearch()</code>; they just won&rsquo;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.</p>
</li>
</ul>
<h3 id="cosmos-db-users-the-biggest-change">Cosmos DB users (the biggest change)</h3>
<p>This is where most of the real risk lives if you&rsquo;re on Cosmos.</p>
<ul>
<li>
<p><strong>Illegal <code>id</code> characters are no longer escaped.</strong> When generating a Cosmos <code>id</code> from a composite key, EF used to escape characters that are illegal in Cosmos resource ids (<code>/</code> → <code>^2F</code>, <code>\</code> → <code>^5C</code>, <code>?</code> → <code>^3F</code>, <code>#</code> → <code>^23</code>). That scheme had a real bug: the escape character <code>^</code> was never itself escaped, so a key value containing the literal string <code>^2F</code> collided with a key value containing <code>/</code>; 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 the <code>id</code>.</p>
<p>The catch: if you have existing data, EF generated those ids with the old escaped format (e.g. <code>Post|1|^2F</code>). After upgrading, EF will look for <code>Post|1|/</code> instead and won&rsquo;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:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>AppContext.SetSwitch(<span style="color:#e6db74">&#34;Microsoft.EntityFrameworkCore.EscapeIllegalCosmosIdCharacters&#34;</span>, <span style="color:#66d9ef">true</span>);
</span></span></code></pre></div><p>Only do the latter if you can&rsquo;t migrate right away; the id-collision bug is still there if you do.</p>
</li>
<li>
<p><strong>Unmapped JSON properties are no longer preserved on round-trip.</strong> This follows from the Cosmos provider dropping its <code>Newtonsoft.Json</code>-based serialization (and the <code>__jObject</code> shadow property along with it) in favor of <code>System.Text.Json</code>. 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 with <code>CosmosClient</code> directly for documents where you need full control over the JSON shape.</p>
</li>
<li>
<p><strong>Undefined projections now throw instead of silently misbehaving.</strong> 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 <code>InvalidOperationException</code>. Now it&rsquo;s consistently an <code>InvalidOperationException</code> either way. Use <code>EF.Functions.IsDefined(...)</code> to filter, or <code>EF.Functions.CoalesceUndefined(...)</code> to supply a default.</p>
</li>
<li>
<p>Smaller Cosmos changes worth knowing about: empty owned collections now materialize as an empty collection instead of <code>null</code>; the discriminator property is named <code>Discriminator</code> in the model again (while the JSON property name stays <code>$type</code> by 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.</p>
</li>
</ul>
<h3 id="build-and-tooling">Build and tooling</h3>
<ul>
<li><strong>The <code>EFOptimizeContext</code> MSBuild property is gone.</strong> Code generation (compiled models, precompiled queries) is now controlled entirely via <code>EFScaffoldModelStage</code> and <code>EFPrecompileQueriesStage</code>, set to <code>publish</code> or <code>build</code>. If you have <code>PublishAOT</code> set to <code>true</code>, code generation at publish time happens automatically without any of this.</li>
<li><strong><code>Microsoft.EntityFrameworkCore.Tools</code> and <code>.Tasks</code> no longer pull in <code>Microsoft.EntityFrameworkCore.Design</code>.</strong> If your project was relying on that dependency coming along for free via the tools package, add it directly: <code>&lt;PackageReference Include=&quot;Microsoft.EntityFrameworkCore.Design&quot; Version=&quot;11.0.0&quot; PrivateAssets=&quot;all&quot; /&gt;</code></li>
</ul>
<h2 id="what-you-get-for-the-trouble">What you get for the trouble</h2>
<h3 id="prettier-sql-for-reference-navigations">Prettier SQL for reference navigations</h3>
<p>This is the post&rsquo;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.</p>
<p>First, split queries (<code>AsSplitQuery()</code>) used to join in reference navigations that the split statement didn&rsquo;t actually need. Given:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> blogs = context.Blogs
</span></span><span style="display:flex;"><span>    .Include(b =&gt; b.BlogType)
</span></span><span style="display:flex;"><span>    .Include(b =&gt; b.Posts)
</span></span><span style="display:flex;"><span>    .AsSplitQuery()
</span></span><span style="display:flex;"><span>    .ToList();
</span></span></code></pre></div><p>the <code>Posts</code> split query used to unnecessarily join <code>BlogType</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#75715e">-- Before EF Core 11
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span> [p].[Id], [p].[BlogId], [p].[Title], [b].[Id], [b0].[Id]
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> [Blogs] <span style="color:#66d9ef">AS</span> [b]
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">INNER</span> <span style="color:#66d9ef">JOIN</span> [BlogType] <span style="color:#66d9ef">AS</span> [b0] <span style="color:#66d9ef">ON</span> [b].[BlogTypeId] <span style="color:#f92672">=</span> [b0].[Id]
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">INNER</span> <span style="color:#66d9ef">JOIN</span> [Post] <span style="color:#66d9ef">AS</span> [p] <span style="color:#66d9ef">ON</span> [b].[Id] <span style="color:#f92672">=</span> [p].[BlogId]
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span> [b].[Id], [b0].[Id]
</span></span></code></pre></div><p>EF Core 11 prunes the join it doesn&rsquo;t need:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#75715e">-- EF Core 11
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span> [p].[Id], [p].[BlogId], [p].[Title], [b].[Id]
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> [Blogs] <span style="color:#66d9ef">AS</span> [b]
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">INNER</span> <span style="color:#66d9ef">JOIN</span> [Post] <span style="color:#66d9ef">AS</span> [p] <span style="color:#66d9ef">ON</span> [b].[Id] <span style="color:#f92672">=</span> [p].[BlogId]
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span> [b].[Id]
</span></span></code></pre></div><p>Second, EF no longer adds a reference navigation&rsquo;s key to <code>ORDER BY</code> when it&rsquo;s already functionally determined by the parent&rsquo;s key via the foreign key:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#75715e">-- Before EF Core 11
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span> [b].[BlogId], [p].[PersonId]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- EF Core 11
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span> [b].[BlogId]
</span></span></code></pre></div><p>Neither change is just cosmetic. Microsoft&rsquo;s own benchmarks show a 29% improvement for the split-query join pruning, and a 22% improvement for the <code>ORDER BY</code> trim on single queries; bigger wins the more reference navigations you&rsquo;re including.</p>
<h3 id="complex-types-finally-unblocked">Complex types, finally unblocked</h3>
<p>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: <a href="#complex-types-as-value-objects">Complex Types as value objects</a>.</p>
<h3 id="fulljoin">FullJoin</h3>
<p>.NET 11 adds <code>FullJoin</code> as a first-class LINQ operator, and EF Core 11 translates it to a SQL <code>FULL JOIN</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> results = <span style="color:#66d9ef">await</span> context.Customers
</span></span><span style="display:flex;"><span>    .FullJoin(
</span></span><span style="display:flex;"><span>        context.Orders,
</span></span><span style="display:flex;"><span>        c =&gt; c.Id,
</span></span><span style="display:flex;"><span>        o =&gt; o.CustomerId,
</span></span><span style="display:flex;"><span>        (c, o) =&gt; <span style="color:#66d9ef">new</span> { Customer = c, Order = o })
</span></span><span style="display:flex;"><span>    .ToListAsync();
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span> [<span style="color:#66d9ef">c</span>].[Id], [<span style="color:#66d9ef">c</span>].[Name], [o].[Id], [o].[CustomerId], [o].[OrderDate]
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> [Customers] <span style="color:#66d9ef">AS</span> [<span style="color:#66d9ef">c</span>]
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FULL</span> <span style="color:#66d9ef">JOIN</span> [Orders] <span style="color:#66d9ef">AS</span> [o] <span style="color:#66d9ef">ON</span> [<span style="color:#66d9ef">c</span>].[Id] <span style="color:#f92672">=</span> [o].[CustomerId]
</span></span></code></pre></div><p>This is the kind of thing you don&rsquo;t need often, but when you do need it, hand-writing it in raw SQL was the only option before now.</p>
<h3 id="smaller-wins-worth-knowing-about">Smaller wins worth knowing about</h3>
<ul>
<li><code>MaxByAsync</code>/<code>MinByAsync</code> translation: find the entity with the max/min value of a key selector, rather than just the max/min value itself.</li>
<li><code>EF.Functions.JsonPathExists()</code>: checks whether a JSON path exists in a document, translating to SQL Server&rsquo;s <code>JSON_PATH_EXISTS</code>.</li>
<li>Vector index support in migrations, via <code>HasVectorIndex()</code>, plus <code>VectorSearch()</code>/<code>WithApproximate()</code> for approximate nearest-neighbor search against SQL Server 2025&rsquo;s <code>VECTOR_SEARCH()</code>.</li>
<li><code>GroupBy</code> translation improvements, including correct materialization of anonymous-type/DTO projections through left joins and grouping.</li>
<li><code>dotnet ef database update --add</code>: scaffolds and applies a migration in one step, useful for containerized or Aspire-style setups where recompiling isn&rsquo;t really an option.</li>
</ul>
<p>There&rsquo;s more beyond this: full-text search table-valued functions, <code>JSON_CONTAINS</code> 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 <a href="https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-11.0/whatsnew">What&rsquo;s New in EF Core 11</a>[<a href="#references">1</a>] page covers all of it.</p>
<h2 id="complex-types-as-value-objects">Complex Types as value objects</h2>
<aside
  style="
    margin: 20px 0;
    padding: 0 14px;
    border-inline-start: 3px solid var(--primary);
  "
>
  <p>Every value object below (<code>Address</code>, <code>PersonName</code>, <code>PhoneNumber</code>) is deliberately minimal. A real <code>Address</code> would need more fields: apartment or suite lines, a state or province, postal code formats that vary by country. A real <code>PersonName</code> would need to handle cultures where a simple first/last split doesn&rsquo;t fit. A real <code>PhoneNumber</code> would need actual validation and formatting rules per country, not just a country code and a string. None of that is the point here. The point is the EF Core mapping, not a production-ready value object library.</p>
</aside>

<h3 id="a-decade-of-circling-back-to-the-same-problem">A decade of circling back to the same problem</h3>
<p>I&rsquo;ve been using Entity Framework since the &ldquo;full&rdquo; .NET Framework days, and classic EF had a real <code>ComplexType</code> feature: you could define a reference-type class with no key of its own, map it to columns on the owner&rsquo;s table, and use it for things like <code>Address</code> 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.</p>
<p>When EF Core shipped in 2016, complex types didn&rsquo;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.</p>
<p>Here&rsquo;s that bug made concrete, using <code>Address</code> as the example. Take an <code>Order</code> with a billing and a shipping address that happen to be the same place:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#a6e22e">[ComplexType]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">record</span> <span style="color:#a6e22e">Address</span>(<span style="color:#66d9ef">string</span> Line1, <span style="color:#66d9ef">string?</span> Line2, <span style="color:#66d9ef">string</span> City, <span style="color:#66d9ef">string</span> Country, <span style="color:#66d9ef">string</span> PostCode);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Order</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">int</span> Id { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">set</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> required Address BillingAddress { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">set</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> required Address ShippingAddress { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">set</span>; }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> address = <span style="color:#66d9ef">new</span> Address(<span style="color:#e6db74">&#34;221B Baker Street&#34;</span>, <span style="color:#66d9ef">null</span>, <span style="color:#e6db74">&#34;London&#34;</span>, <span style="color:#e6db74">&#34;UK&#34;</span>, <span style="color:#e6db74">&#34;NW1 6XE&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> order = <span style="color:#66d9ef">new</span> Order
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    BillingAddress = address,
</span></span><span style="display:flex;"><span>    ShippingAddress = address
</span></span><span style="display:flex;"><span>};
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>context.Orders.Add(order);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">await</span> context.SaveChangesAsync();
</span></span></code></pre></div><p>With <code>Address</code> as an owned entity type (the only option before EF Core 8), <code>Orders.Add(order)</code> throws immediately, before <code>SaveChangesAsync()</code> even runs:</p>
<pre tabindex="0"><code>InvalidOperationException: The same object instance of type &#39;Address&#39; is being referenced from the navigation &#39;BillingAddress&#39; on &#39;Order&#39; and from the navigation &#39;ShippingAddress&#39; on &#39;Order&#39;. Owned entity types cannot have multiple owners pointing to the same instance. Consider creating a copy of the object to use for each navigation.
</code></pre><p>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&rsquo;t allow. With <code>Address</code> as a complex type, the same code just works: there&rsquo;s no hidden identity to collide over, so two properties holding equal values are simply two properties holding equal values. That gap between &ldquo;this is obviously fine&rdquo; and &ldquo;EF throws an exception&rdquo; is exactly the gap complex types close.</p>
<p>EF Core 8 finally brought real complex types back[<a href="#references">3</a>], as a genuine third category alongside primitive types and entity types: structured, multi-value, and explicitly <em>not</em> identified by a key. The general reference docs for the feature are worth bookmarking too[<a href="#references">4</a>]. 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&rsquo;t have to thread through <code>ComplexProperty()</code> just to set a <code>HasMaxLength</code>, and keys/indexes on properties nested inside a complex type. That&rsquo;s the combination that makes this, for me, the first EF Core release where modeling a value object doesn&rsquo;t feel like working around the tool.</p>
<h3 id="why-the-entityvalue-distinction-matters-ddd-or-not">Why the entity/value distinction matters, DDD or not</h3>
<p>You don&rsquo;t need to buy into Domain-Driven Design as a methodology to care about this distinction. It answers a concrete question you&rsquo;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?</p>
<p>A <code>Customer</code> has identity: two customers with identical names and addresses are still two different customers, and if one of them changes their address, that doesn&rsquo;t affect the other. An <code>Address</code> or a <code>PhoneNumber</code>, on the other hand, is usually just a value: two instances with the same data <em>are</em> the same address, there&rsquo;s no meaningful sense in which they&rsquo;re &ldquo;different objects that happen to agree,&rdquo; 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&rsquo;t happen: there&rsquo;s no separate identity to go stale, no shared row that two different owners are secretly both pointing at.</p>
<p>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&rsquo;t exist, which is how you end up debugging why updating one customer&rsquo;s address silently didn&rsquo;t touch another customer&rsquo;s identical-looking address, or why EF threw an error because the same <code>Address</code> instance got reused in two places.</p>
<h3 id="modeling-a-phonenumber">Modeling a PhoneNumber</h3>
<p><code>Address</code> already did its job above: it&rsquo;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&rsquo;m reaching for <code>PhoneNumber</code> instead. I&rsquo;m deliberately not reaching for <code>Money</code>, which drags in currency conversion and exchange rates, a whole separate problem I don&rsquo;t want this example wading into. <code>PhoneNumber</code> 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.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#a6e22e">[ComplexType]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">record</span> <span style="color:#a6e22e">PhoneNumber</span>(<span style="color:#66d9ef">int</span> CountryCode, <span style="color:#66d9ef">string</span> NationalNumber)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">override</span> <span style="color:#66d9ef">string</span> ToString() =&gt; <span style="color:#e6db74">$&#34;+{CountryCode} {NationalNumber}&#34;</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>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&rsquo;s referenced too. Making it immutable (a <code>record</code>, a <code>readonly record struct</code>, or a plain class with an all-args constructor and no setters) sidesteps that entirely: &ldquo;updating&rdquo; a phone number means assigning a new instance, never mutating the one you have.</p>
<p>While we&rsquo;re at it, a person&rsquo;s name shouldn&rsquo;t be a bare <code>string</code> either. It is less obviously a value object than a phone number, since names don&rsquo;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:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#a6e22e">[ComplexType]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">record</span> <span style="color:#a6e22e">PersonName</span>(<span style="color:#66d9ef">string</span> First, <span style="color:#66d9ef">string</span> Last)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">override</span> <span style="color:#66d9ef">string</span> ToString() =&gt; <span style="color:#e6db74">$&#34;{First} {Last}&#34;</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Now a couple of entities that use both value objects, mapped with TPT inheritance (the thing EF Core 11 specifically unblocks):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">abstract</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Person</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">int</span> Id { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">set</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> required PersonName Name { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">set</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> required PhoneNumber Phone { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">set</span>; }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Customer</span> : Person
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">string</span> Company { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">set</span>; } = <span style="color:#66d9ef">string</span>.Empty;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Employee</span> : Person
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> DateOnly HireDate { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">set</span>; }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="configuring-it-lambda-chaining-keys-and-tpt">Configuring it: lambda chaining, keys, and TPT</h3>
<p>Before EF Core 11, configuring a property nested inside a complex type meant calling <code>ComplexProperty()</code> first to get its builder, then <code>Property()</code> on that:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#75715e">// Before EF Core 11</span>
</span></span><span style="display:flex;"><span>modelBuilder.Entity&lt;Person&gt;()
</span></span><span style="display:flex;"><span>    .ComplexProperty(p =&gt; p.Phone)
</span></span><span style="display:flex;"><span>    .Property(p =&gt; p.NationalNumber)
</span></span><span style="display:flex;"><span>    .HasMaxLength(<span style="color:#ae81ff">15</span>);
</span></span></code></pre></div><p>EF Core 11 lets you chain straight through the member access instead:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>modelBuilder.Entity&lt;Person&gt;()
</span></span><span style="display:flex;"><span>    .Property(p =&gt; p.Phone.NationalNumber)
</span></span><span style="display:flex;"><span>    .HasMaxLength(<span style="color:#ae81ff">15</span>);
</span></span></code></pre></div><p>Indexes work the same way now, which matters if you want to look customers up by phone number (something you couldn&rsquo;t do against a nested complex type property before EF Core 11):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>modelBuilder.Entity&lt;Person&gt;()
</span></span><span style="display:flex;"><span>    .HasIndex(p =&gt; p.Phone.NationalNumber);
</span></span></code></pre></div><p>And then the TPT mapping itself, which is the part that genuinely didn&rsquo;t work before this release:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>modelBuilder.Entity&lt;Person&gt;().UseTptMappingStrategy();
</span></span></code></pre></div><p>This mirrors the same mechanism EF Core 11&rsquo;s docs demonstrate with an <code>Animal</code>/<code>AnimalDetails</code> example: EF creates <code>Name_First</code>, <code>Name_Last</code>, <code>Phone_CountryCode</code>, and <code>Phone_NationalNumber</code> columns on the <code>Person</code> table (the TPT base table), with <code>Customer</code> and <code>Employee</code> 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&rsquo;d rather store the phone number as JSON instead of flattened columns, the same property maps that way too, TPT and all: <code>.ComplexProperty(p =&gt; p.Phone, b =&gt; b.ToJson())</code>.</p>
<h3 id="cosmos-gets-complex-types-too">Cosmos gets complex types too</h3>
<p>Worth calling out on its own, since it&rsquo;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&rsquo;re modeling value objects against Cosmos, this closes the same gap it closes on the relational side.</p>
<h3 id="the-gotchas">The gotchas</h3>
<ul>
<li><strong>Complex types must be explicitly configured.</strong> Unlike owned types, EF won&rsquo;t discover a complex type by convention: you need <code>[ComplexType]</code> or <code>ComplexProperty()</code>/<code>ComplexCollection()</code> in <code>OnModelCreating</code>.</li>
<li><strong>No <code>DbSet&lt;T&gt;</code> of a complex type.</strong> They only ever exist as part of an owning entity; querying one directly means projecting it off its owner (<code>context.People.Select(p =&gt; p.Phone)</code>), not querying it on its own.</li>
<li><strong>Collections of complex types only map to JSON.</strong> <code>ComplexCollection()</code> works, but only onto a single JSON column via <code>ToJson()</code>; there&rsquo;s still no way to put a complex-type collection into its own table the way you could with an owned-type collection.</li>
<li><strong>Shared, mutable instances are still a footgun.</strong> 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&rsquo;t enforced: it&rsquo;s just the pattern that avoids the problem.</li>
</ul>
<h2 id="upgrade-checklist">Upgrade checklist</h2>
<ul>
<li>Search for sync calls (<code>ToList()</code>, <code>SaveChanges()</code>, <code>First()</code>, etc.) against Cosmos DB and convert them to async.</li>
<li>Check your SQL Server version and compatibility level against 160; pin to 150 explicitly if you&rsquo;re not on SQL Server 2022 or later.</li>
<li>Confirm your migrations assembly is actually being found, or suppress <code>RelationalEventId.MigrationsNotFound</code> if you intentionally run without migrations.</li>
<li>If you use Cosmos DB, plan for the <code>id</code>-escaping change before upgrading: decide whether you&rsquo;re migrating existing data or using the <code>AppContext</code> switch as a stopgap.</li>
<li>If you use vectors, review any code that reads <code>SqlVector&lt;T&gt;</code> properties off loaded entities; add explicit projections where needed.</li>
<li>If you&rsquo;re on Cosmos DB and relied on unmapped JSON properties surviving a round-trip, map those properties explicitly now.</li>
<li>If you have owned JSON collections without an explicit key, plan to migrate them to <code>ComplexCollection</code>; they&rsquo;re obsolete as of EF Core 11.</li>
<li>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.</li>
</ul>
<h2 id="wrap-up">Wrap-up</h2>
<p>Most of EF Core 11&rsquo;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.</p>
<p>The pruned joins and trimmed <code>ORDER BY</code> 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 <a href="#complex-types-as-value-objects">Complex Types section above</a>: it&rsquo;s the reason I&rsquo;m actually excited about this release.</p>
<h2 id="references">References</h2>
<ol>
<li>Microsoft. <a href="https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-11.0/whatsnew">What&rsquo;s New in EF Core 11</a>. <em>Microsoft Learn</em>, last updated August 2026.</li>
<li>Microsoft. <a href="https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-11.0/breaking-changes">Breaking changes in EF Core 11 (EF11)</a>. <em>Microsoft Learn</em>, last updated July 2026.</li>
<li>Microsoft. <a href="https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-8.0/whatsnew#complex-types">What&rsquo;s New in EF Core 8: Complex types</a>. <em>Microsoft Learn</em>.</li>
<li>Microsoft. <a href="https://learn.microsoft.com/en-us/ef/core/modeling/complex-types">Complex Types</a>. <em>Microsoft Learn</em>.</li>
<li>Microsoft. <a href="https://learn.microsoft.com/en-us/ef/core/querying/sql-queries#getting-the-generated-sql">Getting the generated SQL</a>. <em>Microsoft Learn</em>.</li>
</ol>
]]></content:encoded></item></channel></rss>