<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
<channel>
<title><![CDATA[ elmah.io Blog - .NET Technical tutorials/guides and new features ]]></title>
<description><![CDATA[ On the elmah.io blog, the team behind elmah.io blog about new features and technologies either used to build elmah.io or of general interest. ]]></description>
<link>https://blog.elmah.io</link>
<image>
    <url>https://blog.elmah.io/favicon.png</url>
    <title>elmah.io Blog - .NET Technical tutorials/guides and new features</title>
    <link>https://blog.elmah.io</link>
</image>
<lastBuildDate>Tue, 14 Apr 2026 11:47:50 +0200</lastBuildDate>
<atom:link href="https://blog.elmah.io" rel="self" type="application/rss+xml"/>
<ttl>60</ttl>

    <item>
        <title><![CDATA[ EF Core query translation: Why does some LINQ never become SQL? ]]></title>
        <description><![CDATA[ We know that every Entity Framework Core (EF Core) LINQ query has a corresponding SQL query. That equivalent SQL is actually executed under the hood. Some LINQ expressions involve. NET-specific code, such as calling a method, using a reflection filter, or accessing files. You may have found several code blocks ]]></description>
        <link>https://blog.elmah.io/ef-core-query-translation-why-does-some-linq-never-become-sql/</link>
        <guid isPermaLink="false">69a7d1bf88698d00019ce93d</guid>
        <category><![CDATA[  ]]></category>
        <dc:creator><![CDATA[ Ali Hamza Ansari ]]></dc:creator>
        <pubDate>Tue, 14 Apr 2026 09:52:54 +0200</pubDate>
        <media:content url="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/ef-core-query-translation-why-does-some-linq-never-become-sql-o.png" medium="image"/>
        <content:encoded><![CDATA[ <p>This blog post is originally published on <a href="https://blog.elmah.io/ef-core-query-translation-why-does-some-linq-never-become-sql/">https://blog.elmah.io/ef-core-query-translation-why-does-some-linq-never-become-sql/</a></p> 
<!--kg-card-begin: html-->
<div class="toc"></div>
<!--kg-card-end: html-->
<p>We know that every Entity Framework Core (EF Core) LINQ query has a corresponding SQL query. That equivalent SQL is actually executed under the hood. Some LINQ expressions involve. NET-specific code, such as calling a method, using a reflection filter, or accessing files. You may have found several code blocks of your query that could not be translated. Why does it happen? In today's post, I will cover the reasons along with on-ground examples.</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/ef-core-query-translation-why-does-some-linq-never-become-sql-o-1.png" class="kg-image" alt="EF Core query translation: Why does some LINQ never become SQL?" loading="lazy" width="1500" height="750" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/03/ef-core-query-translation-why-does-some-linq-never-become-sql-o-1.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/03/ef-core-query-translation-why-does-some-linq-never-become-sql-o-1.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/ef-core-query-translation-why-does-some-linq-never-become-sql-o-1.png 1500w" sizes="(min-width: 1200px) 1200px"></figure><h2 id="linq-queries-translation">LINQ Queries Translation</h2><p>We will write different LINQ queries in a real project and examine what results they yield. I will use a Console application with a PostgreSQL database as an example.</p><p><strong>Step 1: Create a project</strong></p><pre><code class="language-console">dotnet new console -n EfTranslationDemo
cd EfTranslationDemo</code></pre><p><strong>Step 2: Install NuGet packages</strong></p><pre><code class="language-console">dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL</code></pre><p><strong>Step 3: Model for the table</strong></p><p>This will be a single-table application consisting of the <code>Perfume</code> model.</p><pre><code class="language-csharp">public class Perfume
{
    public int Id { get; set; }

    public string Name { get; set; } = string.Empty;

    public string Brand { get; set; } = string.Empty;

    public decimal Price { get; set; }

    public int Rating { get; set; }

    public DateTime ReleaseDate { get; set; }
}</code></pre><p><strong>Step 4: Create a database</strong></p><p>To speed up the process, I have already created the database and the <code>Perfumes</code> table.</p><pre><code class="language-SQL">CREATE DATABASE perfumedb;

CREATE TABLE public."Perfumes" (
    "Id" serial PRIMARY KEY,
    "Name" varchar(255) NOT NULL DEFAULT '',
    "Brand" varchar(255) NOT NULL DEFAULT '',
    "Price" decimal(10,2) NOT NULL,
    "Rating" integer NOT NULL,
    "ReleaseDate" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC')
);</code></pre><p><strong>Step 5: Set up the DB context</strong></p><pre><code class="language-csharp">using EfTranslationDemo.Models;
using Microsoft.EntityFrameworkCore;

namespace EfTranslationDemo.Data;

public class ApplicationDbContext: DbContext
{
    public DbSet&lt;Perfume&gt; Perfumes =&gt; Set&lt;Perfume&gt;();

    protected override void OnConfiguring(DbContextOptionsBuilder options)
    {
        options
            .UseNpgsql("Your Connection string with db name perfumedb")
            .LogTo(Console.WriteLine);
    }
}</code></pre><p><strong>Step 6: Seed data</strong></p><p>To begin with, I will seed some data programmatically.</p><pre><code class="language-csharp">using EfTranslationDemo.Data;
using EfTranslationDemo.Models;
using Microsoft.EntityFrameworkCore;

using var context = new ApplicationDbContext();

context.Database.EnsureCreated();

SeedData(context);

static void SeedData(ApplicationDbContext context)
{
    if (context.Perfumes.Any())
        return;

    context.Perfumes.AddRange(
        new Perfume { Name="Sauvage", Brand="Dior", Price=120, Rating=9, ReleaseDate=new DateTime(2018,1,1,0,0,0, DateTimeKind.Utc)},
        new Perfume { Name="Bleu De Chanel", Brand="Chanel", Price=150, Rating=10, ReleaseDate=new DateTime(2017,1,1,0,0,0, DateTimeKind.Utc)},
        new Perfume { Name="Aventus", Brand="Creed", Price=300, Rating=10, ReleaseDate=new DateTime(2015,1,1,0,0,0, DateTimeKind.Utc)},
        new Perfume { Name="F Black", Brand="Ferragamo", Price=60, Rating=7, ReleaseDate=new DateTime(2019,1,1,0,0,0, DateTimeKind.Utc)}
    );

    context.SaveChanges();
}</code></pre><p>Fast forward, the data is seeded to the database</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-2.png" class="kg-image" alt="Perfumes table" loading="lazy" width="837" height="162" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/03/image-2.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-2.png 837w" sizes="(min-width: 720px) 720px"></figure><p><strong>Step 7: LINQ queries testing</strong> </p><p><strong>1. Query with price filter</strong></p><p>A simple query to fetch perfumes with a price of more than 100. </p><pre><code class="language-csharp">Console.WriteLine("Full translable");
var query1 = context
    .Perfumes
    .Where(p =&gt; p.Price &gt; 100)
    .Select(p =&gt; new { p.Name, p.Price });

Console.WriteLine(query1.ToQueryString());</code></pre><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-3.png" class="kg-image" alt="Query with price filter" loading="lazy" width="362" height="108"></figure><p><strong>2. Filter with Name</strong></p><p>We will filter the data with the name starting with 'B'.</p><pre><code class="language-csharp">var query2 = context
    .Perfumes
    .Where(p =&gt; p.Name.StartsWith("B"));

Console.WriteLine(query2.ToQueryString());</code></pre><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-4.png" class="kg-image" alt="Filter with Name" loading="lazy" width="1055" height="112" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/03/image-4.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/03/image-4.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-4.png 1055w" sizes="(min-width: 720px) 720px"></figure><p><code>StartsWith()</code> translates to SQL's <code>LIKE</code> . Similarly, <code>EndsWith</code> and <code>Contains</code> are also translated.</p><p><strong>3. Client Side Execution</strong></p><p>Another translatable but performance-heavy solution is </p><pre><code class="language-csharp">var perfumes = context
    .Perfumes
    .ToList()
    .Where(p =&gt; p.Price &gt; 100);

Console.WriteLine(perfumes.Count());</code></pre><p>It loads all the data in memory first, then applies a filter on the stored data. Such a solution can cause significant problems, especially when the data is large.</p><p><strong>4. External list filter</strong></p><p>We can write a translatable filter with an external list</p><pre><code class="language-csharp">var brands = new List&lt;string&gt; { "Dior", "Creed" };

var query = context
    .Perfumes
    .Where(p =&gt; brands.Contains(p.Brand));

Console.WriteLine(query.ToQueryString());</code></pre><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-5.png" class="kg-image" alt="External list filter" loading="lazy" width="1013" height="157" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/03/image-5.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/03/image-5.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-5.png 1013w" sizes="(min-width: 720px) 720px"></figure><p>EF Core converts the C# in-memory list into SQL constants. </p><p><strong>5. DateTime filter</strong></p><p>Any filter with parsing datetime will work the same as any other.</p><pre><code class="language-csharp">var query = context
    .Perfumes
    .Where(p =&gt; p.ReleaseDate.Year &gt; 2016);

Console.WriteLine(query.ToQueryString());</code></pre><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-6.png" class="kg-image" alt="DateTime filter" loading="lazy" width="981" height="123" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/03/image-6.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-6.png 981w" sizes="(min-width: 720px) 720px"></figure><p>Common <code>DateTime</code> properties are supported.</p><p><strong>6. Custom method filter</strong></p><p>What you cannot do in a LINQ query is to filter by a method. LINQ can't find an SQL equivalent of it.</p><pre><code class="language-csharp">static bool IsLuxury(Perfume perfume)
{
    return perfume.Price &gt; 200;
}

var query = context
    .Perfumes
    .Where(p =&gt; IsLuxury(p));

Console.WriteLine(query.ToQueryString());
</code></pre><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-7-1.png" class="kg-image" alt="Custom method filter" loading="lazy" width="942" height="203" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/03/image-7-1.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-7-1.png 942w" sizes="(min-width: 720px) 720px"></figure><p>EF Core cannot inspect the body of arbitrary C# methods.</p><p><strong>7. Regex matching</strong></p><p>We can simply put a regex pattern in the filter as well.</p><pre><code class="language-csharp">var query = context
    .Perfumes
    .Where(p =&gt; Regex.IsMatch(p.Name, "^A"));

Console.WriteLine(query.ToQueryString());

var result = query.ToList();
Console.WriteLine(result.Count());</code></pre><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-8.png" class="kg-image" alt="Regex matching" loading="lazy" width="971" height="159" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/03/image-8.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-8.png 971w" sizes="(min-width: 720px) 720px"></figure><p><strong>8. Non-Translatable Projection</strong></p><pre><code class="language-csharp">string GetCategory(decimal price)
{
    return price &gt; 150 ? "Luxury" : "Regular";
}

var query = context
    .Perfumes
    .Select(p =&gt; new
    {
        p.Name,
        Category = GetCategory(p.Price)
    });

Console.WriteLine(query.ToQueryString());</code></pre><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-10-1.png" class="kg-image" alt="Non-Translatable Projection" loading="lazy" width="827" height="241" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/03/image-10-1.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-10-1.png 827w" sizes="(min-width: 720px) 720px"></figure><p>The right way</p><pre><code class="language-csharp">var query = context
    .Perfumes
    .Select(p =&gt; new
    {
        p.Name,
        Category = p.Price &gt; 150 ? "Luxury" : "Regular"
    });

Console.WriteLine(query.ToQueryString());</code></pre><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-11.png" class="kg-image" alt="Non-Translatable Projection" loading="lazy" width="588" height="197"></figure><p><strong>9. Reflection</strong></p><p>.NET does not have an equivalent in SQL.</p><pre><code class="language-csharp">var query = context
    .Perfumes
    .Where(p =&gt; p.GetType().GetProperty("Price") != null);

Console.WriteLine(query.ToQueryString());</code></pre><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-13.png" class="kg-image" alt="Reflection" loading="lazy" width="843" height="424" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/03/image-13.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-13.png 843w" sizes="(min-width: 720px) 720px"></figure><p><strong>10. File System Access</strong></p><pre><code class="language-csharp">var query = context
    .Perfumes
    .Where(p =&gt; File.Exists($"brands/{p.Brand}.txt"));

Console.WriteLine(query.ToQueryString());</code></pre><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-12.png" class="kg-image" alt="File System Access" loading="lazy" width="838" height="277" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/03/image-12.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-12.png 838w" sizes="(min-width: 720px) 720px"></figure><p><strong>11. Random number generation</strong></p><pre><code class="language-csharp">var query = context
    .Perfumes
    .Where(p =&gt; Random.Shared.Next(0, 10) &gt; 5);

Console.WriteLine(query.ToQueryString());</code></pre><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-14.png" class="kg-image" alt="Random number generation" loading="lazy" width="855" height="413" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/03/image-14.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-14.png 855w" sizes="(min-width: 720px) 720px"></figure><p>A filter using random number generation did not translate.</p><p><strong>12. string.Compare method</strong></p><p>One of the cool methods that we often use for string comparison is <code>string.Comparison</code>. Unfortunately, you cannot use it in a LINQ query.</p><pre><code class="language-csharp">var query = context
    .Perfumes
    .Where(p =&gt; 
        string.Compare(p.Name, "Sauvage", 
            StringComparison.OrdinalIgnoreCase) == 0
        );
var result  = await query.ToListAsync();
Console.WriteLine(query.ToQueryString());
</code></pre><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-16.png" class="kg-image" alt="string.Compare method" loading="lazy" width="773" height="420" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/03/image-16.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-16.png 773w" sizes="(min-width: 720px) 720px"></figure><h2 id="why-does-ef-core-have-limitations-in-translating-linq-queries">Why does EF Core have limitations in translating LINQ queries?</h2><p>One question arises that even after years of improvement and enhancement, EF Core does not translate some common C# expressions in LINQ. Actually, LINQ does not merely read and execute any query. It first creates <a href="https://blog.elmah.io/expression-trees-in-c-building-dynamic-linq-queries-at-runtime/" rel="noreferrer">an expression tree of the query</a>. Next, these trees are converted to SQL. SQL queries are a declarative language that describes what data you want, not how to compute it step by step. So, methods' bodies are not parsed, as in our case with the <code>IsLuxury</code> method, where expressions do not analyze arbitrary method bodies for translation. Also, C# is a full programming language, whereas SQL lacks many of the features of a programming language. Neither can it read its logic, as SQL servers cannot execute .NET runtime code.</p><p>EF Core is responsible for translating an expression tree to database-specific SQL. We know that every database has its own SQL dialect. For example, <code>DATE_PART('year', column)</code> in a PostgreSQL expression, while the SQL Server equivalent is <code>YEAR(column)</code>. So EF must maintain different translators for each database. It does not rely on a single translator, so there are limitations in translating each and every query across diverse database providers. Finally, some .NET runtime features are specific to the program, such as random number generation, ordinal string comparison, and <a href="https://blog.elmah.io/4-real-life-examples-of-using-reflection-in-c/" rel="noreferrer">reflection,</a> which are impossible to translate.</p><h2 id="conclusion">Conclusion</h2><p>EF Core is a popular ORM and the default choice for many developers for database operations. EF Core uses LINQ for querying and enables you to use databases as if they were in C#. However, the underlying database cannot fully replicate a complete programming language like C#. Many programming expressions and filtering logic cannot be translated into a database query. Calling a method in a LINQ query, using reflection, and ordinal string comparison are examples of limitations in EF Core. SQL is specifically designed to fetch data and is not a programming language. It does not have the .NET runtime, so it cannot execute .NET features. In this post, I have gone through many examples that can be translated into SQL and some common queries that cannot.</p><p>Code: <a href="https://github.com/elmahio-blog/EfTranslationDemo.git">https://github.com/elmahio-blog/EfTranslationDemo.git</a></p> ]]></content:encoded>
    </item>
    <item>
        <title><![CDATA[ Mapping database views in EF Core without breaking migrations ]]></title>
        <description><![CDATA[ Entity Framework Core (EF Core) is working fine in your project. But the moment you use views, the migration gets messy. As a developer, I know any problem in the migration is haunting. You have to update and take care of other migrations so they don&#39;t get disturbed. ]]></description>
        <link>https://blog.elmah.io/mapping-database-views-in-ef-core-without-breaking-migrations/</link>
        <guid isPermaLink="false">69c3cb472828cd0001f15a4b</guid>
        <category><![CDATA[  ]]></category>
        <dc:creator><![CDATA[ Ali Hamza Ansari ]]></dc:creator>
        <pubDate>Tue, 07 Apr 2026 09:20:36 +0200</pubDate>
        <media:content url="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/mapping-database-views-in-ef-core-without-breaking-migrations-o.png" medium="image"/>
        <content:encoded><![CDATA[ <p>This blog post is originally published on <a href="https://blog.elmah.io/mapping-database-views-in-ef-core-without-breaking-migrations/">https://blog.elmah.io/mapping-database-views-in-ef-core-without-breaking-migrations/</a></p> 
<!--kg-card-begin: html-->
<div class="toc"></div>
<!--kg-card-end: html-->
<p>Entity Framework Core (EF Core) is working fine in your project. But the moment you use views, the migration gets messy. As a developer, I know any problem in the migration is haunting. You have to update and take care of other migrations so they don't get disturbed. If not done well, database views can throw you into a pitfall. In today's post, I will walk through how to map views in EF Core without breaking migrations.</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/mapping-database-views-in-ef-core-without-breaking-migrations-o-1.png" class="kg-image" alt="Mapping database views in EF core without breaking migrations" loading="lazy" width="1500" height="750" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/03/mapping-database-views-in-ef-core-without-breaking-migrations-o-1.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/03/mapping-database-views-in-ef-core-without-breaking-migrations-o-1.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/mapping-database-views-in-ef-core-without-breaking-migrations-o-1.png 1500w" sizes="(min-width: 1200px) 1200px"></figure><h2 id="what-is-a-database-view">What is a database view?</h2><p>A view is a named query that acts as a virtual table. It is defined using base tables or previously defined views and provides an abstraction of a complex query. Views do not store any data, but they execute the enclosed query. Views reduce client-side code by encapsulating logic and adding reusability. Although it supports data modifications like create and update in some cases, it is primarily used for read-only operations.</p><h2 id="mapping-database-views-with-ef-core">Mapping database views with EF Core</h2><p>Let's see, with the glasses from the actual project, how we can utilize views. I will use a console project with a PostgreSQL database.</p><p><strong>Step 1: Create the project</strong></p><pre><code class="language-console">dotnet new console -n EfCoreViewDemo
cd EfCoreViewDemo</code></pre><p><strong>Step 2: Install necessary NuGet packages</strong></p><pre><code class="language-console">dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.Json</code></pre><p>Configuration packages will be used to load appsettings.</p><p><strong>Step 3: Create&nbsp;<code>appsettings.json</code></strong></p><p>Adding&nbsp;<code>appsettings.json</code>&nbsp;with a database connection string.</p><pre><code class="language-json">{
  "ConnectionStrings": {
    "PostgresConnection": "Host=localhost;Port=5432;Database=productVDb;Username=postgres;Password=1234"
  }
}
</code></pre><p><strong>Step 4: Define Models</strong></p><p>Add the <code>Product</code> class:</p><pre><code class="language-csharp">namespace EfCoreViewDemo.Models;
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
}</code></pre><p>The <code>ProductSummary</code> class will catch the results of the view:</p><pre><code class="language-csharp">namespace EfCoreViewDemo.Models;

public class ProductSummary
{
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
}</code></pre><p> <strong>Step 5: Configure DbContext</strong></p><pre><code class="language-csharp">using EfCoreViewDemo.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;

namespace EfCoreViewDemo.Data;

public class ApplicationDbContext: DbContext
{
    public DbSet&lt;Product&gt; Products =&gt; Set&lt;Product&gt;();
    public DbSet&lt;ProductSummary&gt; ProductSummaries =&gt; Set&lt;ProductSummary&gt;();

    private readonly string _connectionString;

    public ApplicationDbContext()
    {
        var config = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("appsettings.json")
            .Build();

        _connectionString = config.GetConnectionString("PostgresConnection");
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseNpgsql(_connectionString);
    }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity&lt;ProductSummary&gt;()
            .ToView("vw_product_summary")   
            .HasNoKey();                  
    }
}</code></pre><p>One important configuration is <code>ToView(...)</code> that specifies the <code>DbSet</code> map as a database view. Calling <code>ProductSummaries</code> will fetch data from the view named <code>vw_product_summary</code>. <code>HasNoKey()</code> specifies that views are read-only structures in the database and do not contain any primary key. If not specified, EF Core expects a primary key on the entity and throws an exception.</p><p><strong>Step 6: &nbsp;Set up&nbsp;<code>appsettings.json</code>&nbsp;in the project</strong></p><p>By default, a console app will expect the file in the bin directory. To read newly added appsettings from the root directory, add the following inside the&nbsp;<code>&lt;Project&gt;</code>&nbsp;tag of the application's project file:</p><pre><code class="language-xml">&lt;ItemGroup&gt;
  &lt;None Update="appsettings.json"&gt;
    &lt;CopyToOutputDirectory&gt;PreserveNewest&lt;/CopyToOutputDirectory&gt;
  &lt;/None&gt;
&lt;/ItemGroup&gt;</code></pre><p><strong>Step 7: Run migrations</strong></p><pre><code class="language-console">dotnet ef migrations add InitialCreate</code></pre><p>So the migration will look like</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-44.png" class="kg-image" alt="Migrations" loading="lazy" width="324" height="69"></figure><pre><code class="language-csharp">using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;

#nullable disable

namespace EfCoreViewDemo.Migrations
{
    /// &lt;inheritdoc /&gt;
    public partial class InitialCreate : Migration
    {
        /// &lt;inheritdoc /&gt;
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "Products",
                columns: table =&gt; new
                {
                    Id = table.Column&lt;int&gt;(type: "integer", nullable: false)
                        .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
                    Name = table.Column&lt;string&gt;(type: "text", nullable: false),
                    Price = table.Column&lt;decimal&gt;(type: "numeric", nullable: false)
                },
                constraints: table =&gt;
                {
                    table.PrimaryKey("PK_Products", x =&gt; x.Id);
                });            
        }

        /// &lt;inheritdoc /&gt;
        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "Products");
        }
    }
}
</code></pre><p>But we skipped views from EF Core's tracking, so we need to add it manually in the <code>Up</code> method.</p><pre><code class="language-csharp">migrationBuilder.Sql(@"
  CREATE VIEW vw_product_summary AS
  SELECT ""Name"", ""Price""
  FROM ""Products"";
");</code></pre><p>While in the <code>Down</code> method:</p><pre><code class="language-csharp">migrationBuilder
    .Sql(@"DROP VIEW IF EXISTS vw_product_summary;");</code></pre><p>To reflect the migration</p><pre><code class="language-console">dotnet ef database update</code></pre><p>Now, the database looks like</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-45.png" class="kg-image" alt="Database" loading="lazy" width="216" height="250"></figure><p><strong>Step 8: Define program</strong></p><pre><code class="language-csharp">using EfCoreViewDemo.Data;
using EfCoreViewDemo.Models;

using var context = new ApplicationDbContext();

context.Products.Add(new Product { Name = "Laptop", Price = 1000 });
context.Products.Add(new Product { Name = "Keyboard", Price = 100 });
context.Products.Add(new Product { Name = "Headphone", Price = 150 });
context.Products.Add(new Product { Name = "Web cam", Price = 200 });
context.Products.Add(new Product { Name = "Mouse", Price = 50 });

context.SaveChanges();

var summaries = context.ProductSummaries.ToList();

foreach (var item in summaries)
{
    Console.WriteLine($"{item.Name} - {item.Price}");
}</code></pre><p>To summarize, I added 5 records manually. Later, I am calling <code>ProductSummaries</code>, which is a view.</p><p><strong>Step 9: Run the project</strong></p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-43.png" class="kg-image" alt="Result" loading="lazy" width="210" height="174"></figure><p>Manual migration allows you to automate view definition without breaking migration functionality. A naive approach is to write views directly in the database. </p><p><strong>Step 10: Update the view</strong></p><p>Let's say a requirement came, and we need to update the view.</p><p>Create an empty migration:</p><pre><code class="language-csharp">dotnet ef migrations add UpdateProductSummaryView</code></pre><p>Manual query in migration will look like this:</p><pre><code class="language-csharp">using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace EfCoreViewDemo.Migrations
{
    /// &lt;inheritdoc /&gt;
    public partial class UpdateProductSummaryView : Migration
    {
        /// &lt;inheritdoc /&gt;
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.Sql(@"
                DROP VIEW IF EXISTS vw_product_summary;

                CREATE VIEW vw_product_summary AS
                SELECT ""Name"", ""Price"", ""Price"" * 0.9 AS ""DiscountedPrice""
                FROM ""Products"";
            ");
        }

        /// &lt;inheritdoc /&gt;
        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.Sql(@"
                DROP VIEW IF EXISTS vw_product_summary;

                CREATE VIEW vw_product_summary AS
                SELECT ""Name"", ""Price""
                FROM ""Products"";
            ");
        }
    }
}
</code></pre><p>Updating the model accordingly:</p><pre><code class="language-csharp">namespace EfCoreViewDemo.Models;

public class ProductSummary
{
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public decimal DiscountedPrice { get; set; }
}</code></pre><p>In <code>Program.cs</code>, the view call will become:</p><pre><code class="language-csharp">var summaries = context.ProductSummaries.ToList();

foreach (var item in summaries)
{
    Console.WriteLine($"{item.Name} - {item.Price} - {item.DiscountedPrice}");
}</code></pre><p>Hence, the result</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-46.png" class="kg-image" alt="Result" loading="lazy" width="321" height="174"></figure><h2 id="conclusion">Conclusion</h2><p>Database Views are great for enclosing complex logic as virtual tables. Developers often use it extensively in the application. However, views can make EF Core migration tracking tedious if not managed properly. I provided a solution in today's blog on how you can design views without harming migration.</p><p>Code: <a href="https://github.com/elmahio-blog/EfCoreViewDemo.git">https://github.com/elmahio-blog/EfCoreViewDemo.git</a></p> ]]></content:encoded>
    </item>
    <item>
        <title><![CDATA[ How .NET handles exceptions internally (and why they&#x27;re expensive) ]]></title>
        <description><![CDATA[ What really happens when you write throw new Exception() in .NET? Microsoft guidelines state that

When a member throws an exception, its performance can be orders of magnitude slower. 

It&#39;s not just a simple jump to a catch block, but a lot goes in CLR (Common Language Runtime) ]]></description>
        <link>https://blog.elmah.io/how-net-handles-exceptions-internally-and-why-theyre-expensive/</link>
        <guid isPermaLink="false">69a53c145070010001209946</guid>
        <category><![CDATA[  ]]></category>
        <dc:creator><![CDATA[ Ali Hamza Ansari ]]></dc:creator>
        <pubDate>Tue, 31 Mar 2026 08:42:26 +0200</pubDate>
        <media:content url="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/how-dotnet-handles-exceptions-internally-and-why-theyre-expensive-o-2.png" medium="image"/>
        <content:encoded><![CDATA[ <p>This blog post is originally published on <a href="https://blog.elmah.io/how-net-handles-exceptions-internally-and-why-theyre-expensive/">https://blog.elmah.io/how-net-handles-exceptions-internally-and-why-theyre-expensive/</a></p> 
<!--kg-card-begin: html-->
<div class="toc"></div>
<!--kg-card-end: html-->
<p>What really happens when you write <code>throw new Exception()</code> in .NET? <a href="https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/exceptions-and-performance" rel="noreferrer">Microsoft guidelines</a> state that </p><blockquote>When a member throws an exception, its performance can be orders of magnitude slower.&nbsp;</blockquote><p>It's not just a simple jump to a <code>catch</code> block, but a lot goes in CLR (Common Language Runtime). Expensive operations such as stack trace capture, heap allocations, and method unwinding occur each time. You will not want to use them in any hot paths. Today, In today's post, I will help you decide when exceptions are appropriate and when a simple alternative type might be better.</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/how-dotnet-handles-exceptions-internally-and-why-theyre-expensive-o-3.png" class="kg-image" alt="How .NET handles exceptions internally (and why they're expensive)" loading="lazy" width="1500" height="750" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/03/how-dotnet-handles-exceptions-internally-and-why-theyre-expensive-o-3.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/03/how-dotnet-handles-exceptions-internally-and-why-theyre-expensive-o-3.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/how-dotnet-handles-exceptions-internally-and-why-theyre-expensive-o-3.png 1500w" sizes="(min-width: 1200px) 1200px"></figure><h2 id="what-is-an-exception">What is an Exception?</h2><p>An exception is an error condition or unexpected behaviour during the execution of a program. Exceptions can occur at runtime for various reasons, such as accessing a null object, dividing by zero, or requesting a file that is not found. A C# exception contains several properties, including a <strong>Message </strong>describing the cause of the exception. <a href="https://blog.elmah.io/understanding-net-stack-traces-a-guide-for-developers/" rel="noreferrer"><strong>StackTrace</strong></a><strong> </strong>contains the sequence of method calls that led to the exception in reverse call order to trace the exception source.</p><h2 id="how-does-an-exception-work">How does an exception work?</h2><p>The try block encloses the code prone to exceptions. try/catch protects the application from blowing up. Use the throw keyword to signal the error and throw an Exception object containing detailed information, such as a message and a <a href="https://blog.elmah.io/understanding-net-stack-traces-a-guide-for-developers/" rel="noreferrer">stack trace</a>. The caught exception allows the program to continue gracefully and notify the user where and what error occurred. When an error occurs, the CLR searches for a compatible&nbsp;<code>catch</code>&nbsp;block in the current method. If not found, it moves up the call stack to the calling method, and so on. Once a matching catch is found based on the exception type, control jumps to that block. In an unhandled exception situation where no compatible catch block is found, the application can terminate. Exception handling uses a heap to store the message. To look for a catch body, the CLR unwinds the stack by removing intermediate stack frames. The JIT must generate EH tables and add hidden control-flow metadata.</p><h2 id="what-is-oneoft-in-net">What is OneOf&lt;T&gt; in .NET?</h2><p><code>OneOf&lt;T&gt;</code>&nbsp;or&nbsp;<code>OneOf&lt;T1, T2, T...&gt;</code>&nbsp;represents a discriminated union containing all possible returns of an operation or a method. It contains an array of types, allowing a method to return one of several defined possibilities. The <code>OneOf</code> pattern provides you with fine-grained control and type safety.</p><h2 id="examine-exceptions-with-the-benchmark">Examine Exceptions with the benchmark.</h2><p>To truly understand it, let's create an application. I will use a console application.  </p><p><strong>Step 1: Create the project </strong></p><pre><code class="language-console">dotnet new console -n ExceptionBenchmark
cd ExceptionBenchmark</code></pre><p><strong>Step 2: Add necessary packages</strong></p><p>I am adding the <a href="https://blog.elmah.io/how-to-monitor-your-apps-performance-with-net-benchmarking/" rel="noreferrer">Benchmark</a> library along with <code>OneOf</code>, which is used for the <code>OneOf</code> return type.</p><pre><code class="language-console">dotnet add package BenchmarkDotNet
dotnet add package OneOf</code></pre><p><strong>Step 3: Set up the program.cs</strong></p><p>All the code is in the <code>Program.cs</code></p><pre><code class="language-csharp">using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using OneOf;

BenchmarkRunner.Run&lt;ExceptionBenchmarks&gt;();

[MemoryDiagnoser] 
public class ExceptionBenchmarks
{
    private const int Iterations = 100_000;
    private const int FailureEvery = 10;

    [Benchmark]
    public int NoException()
    {
        int failures = 0;

        for (int i = 1; i &lt;= Iterations; i++)
        {
            if (!DoWork_NoException(i))
                failures++;
        }

        return failures;
    }

    private bool DoWork_NoException(int i)
    {
        return i % FailureEvery != 0;
    }

    [Benchmark]
    public int WithException()
    {
        int failures = 0;

        for (int i = 1; i &lt;= Iterations; i++)
        {
            try
            {
                DoWork_WithException(i);
            }
            catch
            {
                failures++;
            }
        }

        return failures;
    }

    private void DoWork_WithException(int i)
    {
        if (i % FailureEvery == 0)
            throw new InvalidOperationException();
    }

    [Benchmark]
    public int WithOneOf()
    {
        int failures = 0;

        for (int i = 1; i &lt;= Iterations; i++)
        {
            var result = DoWork_WithOneOf(i);

            if (result.IsT1)
                failures++;
        }

        return failures;
    }

    private OneOf&lt;Success, Error&gt; DoWork_WithOneOf(int i)
    {
        if (i % FailureEvery == 0)
            return new Error("Error");

        return new Success("Passed");
    }

    private readonly struct Success
    {
        public string Message { get; }

        public Success(string message)
        {
            Message = message;
        }
    }

    private readonly struct Error
    {
        public string Message { get; }

        public Error(string message)
        {
            Message = message;
        }
    }
}</code></pre><p>The first method is simple with no exception. Then it throws an exception, and in subsequent methods, it finally returns an error object from <a href="https://blog.elmah.io/using-result-t-or-oneof-t-for-better-error-handling-in-net/" rel="noreferrer">OneOf</a>. To make it realistic, each method will observe with 10% error and 90% success rate, as <code>FailureEvery</code> is set to 10. <code>Success</code> and <code>Error</code> are value types to avoid allocations, since they only return the value from the method. </p><p><strong>Step 4: Run and test</strong></p><pre><code class="language-console">dotnet run -c Release</code></pre><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-1.png" class="kg-image" alt="Benchmark results" loading="lazy" width="1040" height="199" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/03/image-1.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/03/image-1.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-1.png 1040w" sizes="(min-width: 720px) 720px"></figure><p>The best performer is the <code>NoException</code>. But that is not practical, you have to identify unexpected behaviour and report it in the code flow. Firstly, a naive approach is to use an exception. Using it adds a time cost and increases the <a href="https://blog.elmah.io/how-net-garbage-collector-works-and-when-you-should-care/" rel="noreferrer">Garbage collector's Gen 0</a> pressure. So, our alternative to exception is <code>OneOf</code>, which significantly saved time and memory. We can further add Objects to the <code>OneOf</code>, considering the possible return values of the method. </p><p>In exceptions, Stack tracing is very expensive, as it propagates stacks, captures method names, stores IL offsets, and inspects frames. Also, the JIT inlining is limited during exceptions. With <code>OneOf</code>I used a struct value type, so Gen 0 utilization is minimized. Neither does it fall for stack trace nor unwind it. Hence, the execution remains linear. </p><h2 id="when-can-i-use-an-exception-alternative">When can I use an exception alternative?</h2><p>In the following cases, exceptions can be replaced with <a href="https://blog.elmah.io/using-result-t-or-oneof-t-for-better-error-handling-in-net/" rel="noreferrer">Result or OneOf</a> in normal application flows.</p><ul><li>Business rule rejection, such as the customers cannot order out-of-stock items. You can return an error in response.</li><li>API validation, where you can simply return 400 with a custom message after figuring out all possible error cases.</li><li>High-throughput paths where you cannot afford an exception mechanism. </li><li>Validation failure, such as invalid email or mobile number input.</li><li>Data not found scenarios where you know either the request data will be available or will not be found. Simply, you can deal with both cases.  </li></ul><h2 id="when-is-an-exception-the-optimal-choice">When is an exception the optimal choice?</h2><p>You don't remove fire alarms from a building because they're loud. You just don't pull them every time someone burns toast. We have some situations where exceptions stand out even if they are expensive. </p><ul><li>Exceptions occur when the program falls into an impossible state, such as when a null database connection is used. You cannot proceed anywhere because the connection is not even initialized for some reason.</li><li>For environmental failures, you will opt for exceptions such as timeout failures, disk I/O failures, or database connection losses. </li><li>Programming bugs where your code falls into a dead end, and it cannot handle further. Conditions where your input case exhaust, such as you have order statuses of pending, cancelled, and confirmed, are enumerated with 1,2 and 3, respectively. There is no case apart from that, so you can simply throw an <code>ArgumentOutOfRangeException</code> or a custom exception in the default case.</li><li>If developing a library, use an exception to signal to the user what went wrong and halt normal execution. Here, you cannot force consumers to handle result types. </li></ul><h2 id="conclusion">Conclusion</h2><p>In high-performance systems, every allocation matters. Exceptions aim to provide a safeguard against anomalous conditions, but they can sometimes be a burden on memory and CPU. I put light on the exception of how much resource they can use for simple operations compared to their counterparts. We explored where it is suitable and where it can be replaced. In short, use exceptions for Unexpected, impossible, and environmental failures. While you can simply use <code>Result/OneOf</code> As an alternative, when conditions are expected, it is useful for business validation, user-driven errors, and high-frequency failures.</p><p>Code: <a href="https://github.com/elmahio-blog/ExceptionBenchmarks.git">https://github.com/elmahio-blog/ExceptionBenchmarks.git</a></p> ]]></content:encoded>
    </item>
    <item>
        <title><![CDATA[ Designing business rules that don&#x27;t leak into controllers ]]></title>
        <description><![CDATA[ APIs are the engine of modern applications. Your product belongs to any domain, either medical, banking, or IoT, APIs are most probable bricks in it. Good, maintainable, and reusable code promises a functional system. While a bad one makes maintenance and testing tedious. There is so much to care about ]]></description>
        <link>https://blog.elmah.io/designing-business-rules-that-dont-leak-into-controllers/</link>
        <guid isPermaLink="false">69a45c305070010001209863</guid>
        <category><![CDATA[  ]]></category>
        <dc:creator><![CDATA[ Ali Hamza Ansari ]]></dc:creator>
        <pubDate>Tue, 24 Mar 2026 09:59:38 +0100</pubDate>
        <media:content url="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/designing-business-rules-that-dont-leak-into-controllers-o-2.png" medium="image"/>
        <content:encoded><![CDATA[ <p>This blog post is originally published on <a href="https://blog.elmah.io/designing-business-rules-that-dont-leak-into-controllers/">https://blog.elmah.io/designing-business-rules-that-dont-leak-into-controllers/</a></p> 
<!--kg-card-begin: html-->
<div class="toc"></div>
<!--kg-card-end: html-->
<p>APIs are the engine of modern applications. Your product belongs to any domain, either medical, banking, or IoT, APIs are most probable bricks in it. Good, maintainable, and reusable code promises a functional system. While a bad one makes maintenance and testing tedious. <a href="https://blog.elmah.io/16-common-mistakes-c-net-developers-make-and-how-to-avoid-them/" rel="noreferrer">There is so much to care about in your application</a>. In today's post, I will dig into writing a clean controller that fulfils the Single Responsibility Principle. We will see why exposing any business logic can harm the application and what exactly a controller should include. </p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/designing-business-rules-that-dont-leak-into-controllers-o-3.png" class="kg-image" alt="Designing business rules that don't leak into controllers" loading="lazy" width="1500" height="750" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/03/designing-business-rules-that-dont-leak-into-controllers-o-3.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/03/designing-business-rules-that-dont-leak-into-controllers-o-3.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/designing-business-rules-that-dont-leak-into-controllers-o-3.png 1500w" sizes="(min-width: 1200px) 1200px"></figure><h2 id="example-with-fat-controller-bad-design">Example with Fat Controller (bad design)</h2><p>Consider the following ASP.NET Core controller implementation:</p><pre><code class="language-csharp">using System;
using Microsoft.AspNetCore.Mvc;

namespace EcCommerce.Controllers;

[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    private readonly ApplicationDbContext _context;

    public OrdersController(ApplicationDbContext context)
    {
        _context = context;
    }
        
    [HttpPost]
    public async Task&lt;IActionResult&gt; CreateOrder(CreateOrderRequest request)
    {
        var user = await _context.Users
            .Include(u =&gt; u.Orders)
            .FirstOrDefaultAsync(u =&gt; u.Id == request.UserId);
    
        if (user == null)
            return NotFound("User not found");
    
        if (!user.IsActive)
            return BadRequest("User is not active");
    
        if (request.TotalAmount &lt;= 0)
            return BadRequest("Invalid order amount");
    
        var todayOrdersCount = user.Orders
            .Count(o =&gt; o.CreatedAt.Date == DateTime.UtcNow.Date);
    
        if (todayOrdersCount &gt;= 5)
            return BadRequest("Daily order limit exceeded");
    
        var order = new Order
        {
            UserId = user.Id,
            TotalAmount = request.TotalAmount,
            CreatedAt = DateTime.UtcNow
        };
    
        _context.Orders.Add(order);
        await _context.SaveChangesAsync();
    
        return Ok(order);
    }
}</code></pre><p>The first thing in the code is that it is piercing to the eyes. Also, the controller knows too much of business logic and is really fat. If any business logic changes are required, we need to update the controller. Let's say we have to increase the daily order limit from 5 to 50, then we will need to update it here. Testing is difficult too, any test, even for business logic, will be done on the controller. All of that violates the DRY (Don't Repeat Yourself) and Single Responsibility Principles, as the controller is not inherently dedicated to these tasks. The example may look like I have added too much logic here, which people usually avoid, but you still need to be clear about exactly what the controllers should have. Many developers get confused and write a few lines of business logic where they shouldn't. The controllers should handle HTTP requests, validate the mapping, and return responses.</p><h2 id="business-rules-dont-leak-into-controllers">Business Rules Don't Leak Into Controllers</h2><p>So, correcting the faulty controller here.</p><p><strong>Step 1: Create a Domain Service</strong></p><pre><code class="language-csharp ">public interface IOrderService
{
    Task&lt;Order&gt; CreateOrderAsync(int userId, decimal totalAmount);
}</code></pre><p>A very good way to encapsulate business logic is to introduce a service layer with an interface and its implementation, and inject them into the controller.</p><p><strong>Step 2: Implementation of the domain service</strong></p><pre><code class="language-csharp">public class OrderService : IOrderService
{
    private readonly ApplicationDbContext _context;

    public OrderService(ApplicationDbContext context)
    {
        _context = context;
    }

    public async Task&lt;Order&gt; CreateOrderAsync(int userId, decimal totalAmount)
    {
        var user = await _context.Users
            .Include(u =&gt; u.Orders)
            .FirstOrDefaultAsync(u =&gt; u.Id == userId);

        if (user == null)
            throw new Exception("User not found");

        if (!user.IsActive)
            throw new Exception("User is not active");

        if (totalAmount &lt;= 0)
            throw new Exception("Invalid order amount");

        var todayOrdersCount = user.Orders
            .Count(o =&gt; o.CreatedAt.Date == DateTime.UtcNow.Date);

        if (todayOrdersCount &gt;= 5)
            throw new Exception("Daily order limit exceeded");

        var order = new Order
        {
            UserId = user.Id,
            TotalAmount = totalAmount,
            CreatedAt = DateTime.UtcNow
        };

        _context.Orders.Add(order);
        await _context.SaveChangesAsync();

        return order;
    }
}</code></pre><p>Sometimes, you can add a <a href="https://blog.elmah.io/the-repository-pattern-is-simple-yet-misunderstood/" rel="noreferrer">repository layer</a> below the services and inject it instead of using  <code>ApplicationDbContext</code> directly.</p><p><strong>Step 3: Thin Controller</strong></p><pre><code class="language-csharp">using System;
using Microsoft.AspNetCore.Mvc;

namespace EcCommerce.Controllers;

[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    private readonly IOrderService _orderService;

    public OrdersController(IOrderService orderService)
    {
        _orderService = orderService;
    }
        
    [HttpPost]
    public async Task&lt;IActionResult&gt; CreateOrder(CreateOrderRequest request)
    {
      
        var order = await _orderService
            .CreateOrderAsync(request.UserId, request.TotalAmount);
    
        return Ok(order);
    }
}</code></pre><p>Now, we have a soothing code. Not to forget dependency injection in your <code>Program.cs</code>:</p><pre><code class="language-csharp">var builder = WebApplication.CreateBuilder(args);

// Other injections

builder.Services.AddScoped&lt;IOrderService, OrderService&gt;();
</code></pre><h2 id="clean-architecture-style-domain-driven">Clean Architecture Style (Domain-Driven)</h2><p>One more way you can deal is to define the logic in the domain itself.</p><p><strong>User Model </strong></p><pre><code class="language-csharp">public class User
{
    public bool IsActive { get; private set; }
    public List&lt;Order&gt; Orders { get; private set; } = new();

    public void CanPlaceOrder(decimal totalAmount)
    {
        if (!IsActive)
            throw new Exception("User is not active");

        if (totalAmount &lt;= 0)
            throw new Exception("Invalid order amount");

        var todayOrders = Orders
            .Count(o =&gt; o.CreatedAt.Date == DateTime.UtcNow.Date);

        if (todayOrders &gt;= 5)
            throw new Exception("Daily limit exceeded");
    }
}</code></pre><p><strong>Service </strong></p><pre><code class="language-csharp">public class OrderService : IOrderService
{
    private readonly ApplicationDbContext _context;

    public OrderService(ApplicationDbContext context)
    {
        _context = context;
    }
    
    public async Task&lt;Order&gt; CreateOrderAsync(int userId, decimal totalAmount)
    {
        var user = await _context.Users
            .Include(u =&gt; u.Orders)
            .FirstOrDefaultAsync(u =&gt; u.Id == userId);
    
        if (user == null)
            throw new Exception("User not found");
    
        user.CanPlaceOrder(totalAmount);
    
        var order = new Order(userId, totalAmount);
    
        _context.Orders.Add(order);
        await _context.SaveChangesAsync();
    
        return order;
    }
}</code></pre><p><strong>Dependency injection</strong></p><pre><code class="language-csharp">var builder = WebApplication.CreateBuilder(args);

// Other injections

builder.Services.AddScoped&lt;IOrderService, OrderService&gt;();
</code></pre><p>Now the service becomes cleaner. In our corrected versions, we addressed all issues seen in the first code. If a new requirement asks to change the daily limit, we will go to the service or the domain model in a later example, and the controller will remain unaffected in both cases. Testing units are also made easy with the exposed method in the service. If any endpoint requires similar <code>CreateOrder</code> operations, then we can simply use it from <code>OrderService</code>, complying with the DRY principle. Our controller is now clean, and it adheres to the Single Responsibility Principle. The controller can only focus on validating requests and generating responses, while other tasks are handled in the underlying layers.</p><h2 id="conclusion">Conclusion</h2><p>API controllers are responsible for exposing endpoints that your clients extensively rely on. They should validate the request, call the underlying layer like the service layer, and return the data. Keeping things in their place can help a lot with testing and maintenance, especially if your code is considerably large. One overlooked aspect is understanding exactly what a controller should contain. People often mistakenly add logic inside the controller method and fail to identify where to draw the line. I made it easy with this article and showed what potential problems can arise if business logic leaks inside the controller.</p> ]]></content:encoded>
    </item>
    <item>
        <title><![CDATA[ When NOT to use the repository pattern in EF Core ]]></title>
        <description><![CDATA[ If you design an application with a data source, the repository pattern often comes to mind as a prominent choice. In fact, many developers see it as the default choice. However, the pattern is not helping every time. In this post, I will pinpoint some cases where the repository pattern ]]></description>
        <link>https://blog.elmah.io/when-not-to-use-the-repository-pattern-in-ef-core/</link>
        <guid isPermaLink="false">6993547fa80af5000150cc37</guid>
        <category><![CDATA[  ]]></category>
        <dc:creator><![CDATA[ Ali Hamza Ansari ]]></dc:creator>
        <pubDate>Wed, 18 Mar 2026 06:24:07 +0100</pubDate>
        <media:content url="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/when-not-to-use-the-repository-pattern-in-ef-core-o-2.png" medium="image"/>
        <content:encoded><![CDATA[ <p>This blog post is originally published on <a href="https://blog.elmah.io/when-not-to-use-the-repository-pattern-in-ef-core/">https://blog.elmah.io/when-not-to-use-the-repository-pattern-in-ef-core/</a></p> 
<!--kg-card-begin: html-->
<div class="toc"></div>
<!--kg-card-end: html-->
<p>If you design an application with a data source, the repository pattern often comes to mind as a prominent choice. In fact, many developers see it as the default choice. However, the pattern is not helping every time. In this post, I will pinpoint some cases where the repository pattern is not the best choice.</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/when-not-to-use-the-repository-pattern-in-ef-core-o-3.png" class="kg-image" alt="When NOT to use the repository pattern in EF Core" loading="lazy" width="1500" height="750" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/03/when-not-to-use-the-repository-pattern-in-ef-core-o-3.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/03/when-not-to-use-the-repository-pattern-in-ef-core-o-3.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/when-not-to-use-the-repository-pattern-in-ef-core-o-3.png 1500w" sizes="(min-width: 1200px) 1200px"></figure><h2 id="what-is-a-repository-pattern">What is a repository pattern?</h2><p>The <a href="https://blog.elmah.io/the-repository-pattern-is-simple-yet-misunderstood/" rel="noreferrer">repository pattern</a> is a design pattern that acts as an intermediate layer between data access and business logic. It abstracts the data source and implements the details, providing a clean representation of data manipulation as objects and lists.</p><p>Let us start by looking at how a repository pattern can be implemented with EF Core.</p><p>Start by adding a new model named <code>Movie</code>:</p><pre><code class="language-csharp">public class Movie
{
    public Guid Id { get; set; }
    public string Title { get; set; } = string.Empty;
    public string Director { get; set; } = string.Empty;
    public int ReleaseYear { get; set; }
    public double ImdbRating { get; set; }
    public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow;
}</code></pre><p>Next, add a <code>IMovieRepository</code> interface with the basic methods for adding, getting, and saving movies:</p><pre><code class="language-csharp">public interface IMovieRepository
{
    Task AddAsync(Movie movie);
    Task&lt;Movie?&gt; GetByIdAsync(Guid id);
    Task&lt;List&lt;Movie&gt;&gt; GetTopRatedAsync(double minRating);
    Task SaveChangesAsync();
}</code></pre><p>Add an implementation of that interface using EF Core:</p><pre><code class="language-csharp">public class MovieRepository : IMovieRepository
{
    private readonly AppDbContext _context;

    public MovieRepository(AppDbContext context)
    {
        _context = context;
    }

    public async Task AddAsync(Movie movie)
    {
        await _context.Movies.AddAsync(movie);
    }

    public async Task&lt;Movie?&gt; GetByIdAsync(Guid id)
    {
        return await _context.Movies.FindAsync(id);
    }

    public async Task&lt;List&lt;Movie&gt;&gt; GetTopRatedAsync(double minRating)
    {
        return await _context.Movies
            .Where(m =&gt; m.ImdbRating &gt;= minRating)
            .OrderByDescending(m =&gt; m.ImdbRating)
            .ToListAsync();
    }

    public async Task SaveChangesAsync()
    {
        await _context.SaveChangesAsync();
    }
}</code></pre><p>Finally, I'll add a service class that shows how to use the movie repository:</p><pre><code class="language-csharp">public class MovieService
{
    private readonly IMovieRepository _repository;

    public MovieService(IMovieRepository repository)
    {
        _repository = repository;
    }

    public async Task&lt;Guid&gt; CreateMovieAsync(
        string title,
        string director,
        int releaseYear,
        double rating)
    {
        var movie = new Movie
        {
            Id = Guid.NewGuid(),
            Title = title,
            Director = director,
            ReleaseYear = releaseYear,
            ImdbRating = rating
        };

        await _repository.AddAsync(movie);
        await _repository.SaveChangesAsync();

        return movie.Id;
    }

    public async Task&lt;List&lt;Movie&gt;&gt; GetHighlyRatedMoviesAsync()
    {
        return await _repository.GetTopRatedAsync(8.0);
    }
}</code></pre><p>If you are writing CRUD applications, implementing a data layer like this probably looks very familiar.</p><h2 id="what-are-the-advantages-of-the-repository-pattern">What are the advantages of the Repository pattern?</h2><p>The repository pattern promises several key advantages.</p><ul><li>A clean separation of concerns where data access logic is centralized.</li><li>Reusability, where the same repo methods can be used without copying the same logic again.</li></ul><h2 id="when-to-use-the-repository-pattern">When to use the Repository pattern</h2><p>Like any tool, it offers leverage only when in the right place. If you smell any scent in your code, go for the repository pattern.</p><ul><li>When your application does not rely on simple data storage or fetching but requires enquiring logic such as validation, projection, object preparation, or calculations. Domains such as insurance, banking, healthcare, and IoT require calculations, so the repository pattern can be helpful.</li><li>The repository pattern can win for you if you are aggregating multiple data sources but presenting them as a single source to the upper layers. Usage of different data sources, such as MSSQL, Postgres, and external APIs, is kept hidden from the business logic layer. </li><li>The repository layer can be handy if an application demands sophisticated caching strategies and you don't want to pollute the business layers. Hence, the service layer can be unaware of how the cache is configured, or even of whether the data comes from the cache or another source.</li><li>For unit testing, you can employ the repository pattern, especially in error-critical systems such as financial systems, medical devices, and safety systems. Repositories enable you to test business logic in&nbsp;isolation&nbsp;by swapping real data access with test doubles. You can verify complex business rules, edge cases, and error handling without the overhead, unpredictability, and slowness of database tests.  </li></ul><h2 id="when-to-avoid-the-repository-pattern">When to avoid the Repository pattern</h2><p>Well, we have seen the usefulness of the repository pattern. Now, rejoining our original question, "In what conditions can you avoid the repository pattern?"</p><ul><li>If your app is just basic Create, Read, Update, Delete operations without complex business logic, you simply go without it. A simple creation or fetch will not require verbose code, and adding a new layer will overengineer it.</li></ul><p>For example, in the code, a repository pattern has simple operations:</p><pre><code class="language-csharp">public class UserRepository : IUserRepository 
{
    public User GetById(int id) =&gt; _context.Users.Find(id);
    public void Add(User user) =&gt; _context.Users.Add(user);
}</code></pre><ul><li>With an ORM, you can avoid the abstraction layer. Most ORMs, such as Entity&nbsp;Framework Core, NHibernate, and Doctrine, already implement the repository pattern using <code>DbSet</code> and <code>AppDbContext</code>. You can simply deal with entities like collections and objects. If you don't have to add conditions, validation, and projections in the operations, you can choose simplicity. When wrapping an ORM in repositories, you are often hiding powerful features (like <code>IQueryable</code> for deferred execution or <code>Include</code> for eager loading) behind a more restrictive interface.</li><li>Smaller projects also don't need to be tedious. If your project requires simple queries and consists of 10-15 tables, you are good to go without bombarding a small project with more code. </li><li>Any abstraction comes with overhead. In a performance-critical system, a repository may not be the best choice for the same reason. Repository layers can require memory allocation, additional method calls, or complex query translation, which may slow down the software. Repositories often lead to the N+1 query problem or over-fetching data because the repository method returns a generic object rather than a specific projection (<code>Select</code>) tailored to the view.</li><li>One more scenario where you can skip the repository pattern is in a microservice architecture. If a service is simple enough to have a small database and minimal operations, you don't need to trade off the repository pattern for maintenance and performance.</li><li>While preparing reporting and analytics data, the repository pattern can be unnecessary. Mostly, the stored procedures, raw SQL queries, and database-specific optimizations do the whole job for us. The code only calls those underlying queries and returns. To keep things maintainable and, of course, speedy, you can avoid one layer. </li></ul><h2 id="conclusion">Conclusion</h2><p>The repository pattern is something you have probably used on your development journey. Why not? It is one of the popular choices for abstracting data access. However, abstractions have a hidden cost that I highlighted in the blog. I identified a few scenarios where you can escape it and barely lose anything. If you still want to use the repository pattern without losing its limitation, the <a href="https://blog.elmah.io/repository-pattern-vs-specification-pattern-which-is-more-maintainable/" rel="noreferrer">specification pattern is another player that can work</a>. It allows for reusable query logic without the bloat of a traditional repository.</p> ]]></content:encoded>
    </item>
    <item>
        <title><![CDATA[ Why IEnumerable Can Kill Performance in Hot Paths ]]></title>
        <description><![CDATA[ For F1 racing, choosing the right car is as important as your expertise. No matter how skilled you are, if you race in an ordinary car, you can&#39;t stand out. You need to understand the race and use the F1 racing car. The same goes for programming. Going ]]></description>
        <link>https://blog.elmah.io/why-ienumerable-can-kill-performance-in-hot-paths/</link>
        <guid isPermaLink="false">69916569d370370001951f37</guid>
        <category><![CDATA[  ]]></category>
        <dc:creator><![CDATA[ Ali Hamza Ansari ]]></dc:creator>
        <pubDate>Tue, 03 Mar 2026 07:04:34 +0100</pubDate>
        <media:content url="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/02/why-ienumerable-can-kill-performance-in-hot-paths-o.png" medium="image"/>
        <content:encoded><![CDATA[ <p>This blog post is originally published on <a href="https://blog.elmah.io/why-ienumerable-can-kill-performance-in-hot-paths/">https://blog.elmah.io/why-ienumerable-can-kill-performance-in-hot-paths/</a></p> 
<!--kg-card-begin: html-->
<div class="toc"></div>
<!--kg-card-end: html-->
<p>For F1 racing, choosing the right car is as important as your expertise. No matter how skilled you are, if you race in an ordinary car, you can't stand out. You need to understand the race and use the F1 racing car. The same goes for programming. Going with the wrong option can hurt your application. If your application contains high-throughput junctions prone to bottlenecks, you are left with choices for different collections. Today, I will explore popular data collection options, including <code>IEnumerable</code> and <code>List</code>, and examine how <code>IEnumerable</code> can hinder hot paths.</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/02/why-ienumerable-can-kill-performance-in-hot-paths-o-1.png" class="kg-image" alt="Why IEnumerable Can Kill Performance in Hot Paths" loading="lazy" width="1500" height="750" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/02/why-ienumerable-can-kill-performance-in-hot-paths-o-1.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/02/why-ienumerable-can-kill-performance-in-hot-paths-o-1.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/02/why-ienumerable-can-kill-performance-in-hot-paths-o-1.png 1500w" sizes="(min-width: 1200px) 1200px"></figure><h2 id="ienumerable-in-c">IEnumerable in C#</h2><p><code>IEnumerable</code> is an interface that represents a forward-only sequence of elements. It represents a behaviour that can be deferred or materialized based on implementation. It exposes a single method, <code>GetEnumerator()</code> that returns an <code>IEnumerator&lt;T&gt;</code> object you can iterate sequentially. However, it does not support indexing.</p><h2 id="list-in-c">List in C#</h2><p>A list is a dynamic collection that resizes automatically and allows access to elements by index. The <code>List</code> class provides methods such as <code>Add</code>, <code>Remove</code>, and <code>AddRange</code>. A <code>List</code> is materialized, loading all data into memory. In fact, List implements <code>IEnumerable</code>.</p><h2 id="ienumerable-analysis-with-benchmark">IEnumerable analysis with benchmark</h2><p>To observe actual ground effects, let's create a console application where we can benchmark <code>IEnumerable</code> with a concrete <code>List</code>. </p><p><strong>Step 1: Create the project</strong></p><pre><code class="language-console">dotnet new console -n IEnumerableBenchmark
cd IEnumerableBenchmark</code></pre><p><strong>Step 2: Install the BenchmarkDotNet library</strong></p><pre><code class="language-console">dotnet add package BenchmarkDotNet
</code></pre><p><strong>Step 3: Prepare the benchmarking code</strong></p><p>Here, we will define everything in the <code>Program.cs</code> file:</p><pre><code class="language-csharp">using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;

BenchmarkRunner.Run&lt;EnumerationBenchmarks&gt;();

[MemoryDiagnoser]
public class EnumerationBenchmarks
{
    private List&lt;int&gt; _list = null!;
    private IEnumerable&lt;int&gt; _interfaceEnumerable = null!;
    private IEnumerable&lt;int&gt; _linqEnumerable = null!;
    private IEnumerable&lt;int&gt; _yieldEnumerable = null!;
    private int[] _array = null!;

    [Params(1000, 100_000)]
    public int N;

    [GlobalSetup]
    public void Setup()
    {
        // Independent data sources
        _list = Enumerable.Range(1, N).ToList();

        _array = Enumerable.Range(1, N).ToArray();

        _interfaceEnumerable = Enumerable.Range(1, N);

        _linqEnumerable = Enumerable.Range(1, N)
                                    .Where(x =&gt; x % 2 == 0)
                                    .Select(x =&gt; x * 2);

        _yieldEnumerable = CreateYieldSequence(N);
    }

    private IEnumerable&lt;int&gt; CreateYieldSequence(int count)
    {
        for (int i = 1; i &lt;= count; i++)
        {
            if (i % 2 == 0)
                yield return i * 2;
        }
    }

    // -------------------------------
    // List - for (optimized)
    // -------------------------------
    [Benchmark(Baseline = true)]
    public int List_For()
    {
        int sum = 0;
        var list = _list;
        int count = list.Count;

        for (int i = 0; i &lt; count; i++)
        {
            var x = list[i];
            if (x % 2 == 0)
                sum += x * 2;
        }

        return sum;
    }

    // -------------------------------
    // List - foreach
    // -------------------------------
    [Benchmark]
    public int List_Foreach()
    {
        int sum = 0;

        foreach (var x in _list)
        {
            if (x % 2 == 0)
                sum += x * 2;
        }

        return sum;
    }

    // -------------------------------
    // IEnumerable - foreach
    // -------------------------------
    [Benchmark]
    public int IEnumerable_Foreach()
    {
        int sum = 0;

        foreach (var x in _interfaceEnumerable)
        {
            if (x % 2 == 0)
                sum += x * 2;
        }

        return sum;
    }

    // -------------------------------
    // Array - foreach
    // -------------------------------
    [Benchmark]
    public int Array_Foreach()
    {
        int sum = 0;

        foreach (var x in _array)
        {
            if (x % 2 == 0)
                sum += x * 2;
        }

        return sum;
    }

    // -------------------------------
    // LINQ Deferred Execution
    // -------------------------------
    [Benchmark]
    public int Linq_Deferred()
    {
        return _linqEnumerable.Sum();
    }

    // -------------------------------
    // Yield State Machine
    // -------------------------------
    [Benchmark]
    public int Yield_Enumeration()
    {
        int sum = 0;

        foreach (var x in _yieldEnumerable)
        {
            sum += x;
        }

        return sum;
    }

    // -------------------------------
    // Multiple Enumeration
    // -------------------------------
    [Benchmark]
    public int Multiple_Enumeration()
    {
        int sum = 0;

        if (_linqEnumerable.Any())
        {
            foreach (var x in _linqEnumerable)
            {
                sum += x;
            }
        }

        return sum;
    }

    // -------------------------------
    // Span&lt;T&gt;
    // -------------------------------
    [Benchmark]
    public int Span_For()
    {
        int sum = 0;
        var span = CollectionsMarshal.AsSpan(_list);

        for (int i = 0; i &lt; span.Length; i++)
        {
            var x = span[i];
            if (x % 2 == 0)
                sum += x * 2;
        }

        return sum;
    }
}
</code></pre><p>I defined a list and an <code>IEnumerable</code>. In the Setup, we will initialize each one with 1000 and 100000 elements. There are several other testers to get how <code>IEnumerable</code> performs as deferred and as materialized collections, as shown in the methods.</p><p><strong>Step 4: Run the project</strong></p><p>Let's run our project in release mode</p><pre><code class="language-console">dotnet run -c Release
</code></pre><p>So, the results show the cost of <code>IEnumerable</code>.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/02/image-20.png" class="kg-image" alt="Benchmark results" loading="lazy" width="1204" height="471" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/02/image-20.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/02/image-20.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/02/image-20.png 1204w" sizes="(min-width: 720px) 720px"></figure><p>As per the results, List outperformed <code>IEnumerable</code> by up to 7x. Thanks to contiguous memory allocation and JITs array optimization, arrays outclass the list, too. But flexibility and numerous handy methods make the list more usable in most scenarios. The single test does not prove that <code>IEnumerable</code> should be abandoned. In fact, you will not want to overload memory by loading the entire dataset if it is extremely large. Then the<code>IEnumerable</code>'s behaviour took effect. The <code>List</code> is a help for small to medium-sized collections and requires frequent manipulation, such as adding and removing items. As immediate execution loads data immediately, lists are ideal when you need data to be readily available and need frequent manipulation. </p><p>Using <code>IEnumerable</code> in hot paths slows performance and does not support data manipulation. Usually, applications do not load all data at once; instead, they use filtering or pagination when fetching data. In such cases, lists leverage immediate execution.</p><h2 id="conclusion">Conclusion</h2><p>We may not need to emphasize the importance of performance in any application. We already know the importance of keeping the operations as fast as possible. Identifying and mitigating the hot path is one link in this chain. I shared one underrated point about using the right collection in such bottleneck areas. Being cautious about <code>IEnumerable</code> in hot paths, can be good for the application and your peace of mind.</p><p>Code: <a href="https://github.com/elmahio-blog/IEnumerableBenchmark">https://github.com/elmahio-blog/IEnumerableBenchmark</a></p> ]]></content:encoded>
    </item>
    <item>
        <title><![CDATA[ Polymorphic Relationships in EF Core: Three Approaches ]]></title>
        <description><![CDATA[ Database schema and entity design are the pavement of most applications. If the entities are paved well, the application can provide great performance. Otherwise, it can lead to pitfalls. One key aspect of entity design is dealing with polymorphic relationships. EF Core supports several ways to implement inheritance, so in ]]></description>
        <link>https://blog.elmah.io/polymorphic-relationships-in-ef-core-three-approaches/</link>
        <guid isPermaLink="false">69762b2e7850e400012fbd37</guid>
        <category><![CDATA[  ]]></category>
        <dc:creator><![CDATA[ Ali Hamza Ansari ]]></dc:creator>
        <pubDate>Tue, 24 Feb 2026 08:56:27 +0100</pubDate>
        <media:content url="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/02/polymorphic-relationships-in-ef-core-three-approaches-o.png" medium="image"/>
        <content:encoded><![CDATA[ <p>This blog post is originally published on <a href="https://blog.elmah.io/polymorphic-relationships-in-ef-core-three-approaches/">https://blog.elmah.io/polymorphic-relationships-in-ef-core-three-approaches/</a></p> 
<!--kg-card-begin: html-->
<div class="toc"></div>
<!--kg-card-end: html-->
<p>Database schema and entity design are the pavement of most applications. If the entities are paved well, the application can provide great performance. Otherwise, it can lead to pitfalls. One key aspect of entity design is dealing with polymorphic relationships. EF Core supports several ways to implement inheritance, so in this post, I will explore the best ways to handle these relationships.</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/02/polymorphic-relationships-in-ef-core-three-approaches-o-1.png" class="kg-image" alt="Polymorphic Relationships in EF Core: Three Approaches" loading="lazy" width="1500" height="750" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/02/polymorphic-relationships-in-ef-core-three-approaches-o-1.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/02/polymorphic-relationships-in-ef-core-three-approaches-o-1.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/02/polymorphic-relationships-in-ef-core-three-approaches-o-1.png 1500w" sizes="(min-width: 1200px) 1200px"></figure><p>To see these concepts in action, we need to look at the specific implementation strategies EF Core offers. We will start with the most common approach, which maps an entire class hierarchy to a single database table.</p><h2 id="table-per-hierarchy-tph-inheritance-ef-core-native-polymorphic-relationship-implementation">Table-per-Hierarchy (TPH) Inheritance (EF Core-native) polymorphic relationship implementation</h2><p>One polymorphic relationship EF Core provides is Table-per-Hierarchy (TPH). A single table stores data for all inherited types, differentiated by a discriminator column. I will use an enum for the discriminator. In fact, <a href="https://learn.microsoft.com/en-us/ef/core/modeling/inheritance" rel="noreferrer">TPH is the default mapping of <em>EF </em>Core</a>. Let us create a project to showcase TPH.</p><p><strong>Step 1: Create a project</strong></p><pre><code class="language-console">dotnet new console -o EfCoreTph</code></pre><p> <strong>Step 2: Install the required packages</strong></p><pre><code class="language-console">dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
</code></pre><p><strong>Step 3: Define models</strong></p><p>Add the discriminator enum:</p><pre><code class="language-csharp">public enum EmployeeTypeEnum: byte
{
    FullTimeEmployee = 1,
    PartTimeEmployee = 2,
    Contractor = 3
}</code></pre><p>Add the model <code>Employee</code>:</p><pre><code class="language-csharp">public abstract class Employee
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
    public DateTime HireDate { get; set; } = DateTime.UtcNow.Date;
    public decimal BaseSalary { get; set; }
}</code></pre><p>I've made it abstract to function as a base class. Next, add the <code>Contractor</code> sub-class:</p><pre><code class="language-csharp">public class Contractor: Employee
{
    public DateTime ContractEndDate { get; set; }
    public string AgencyName { get; set; } = string.Empty;
}</code></pre><p>And add another subclass named <code>FullTimeEmployee</code>:</p><pre><code class="language-csharp">public class FullTimeEmployee: Employee
{
    public decimal AnnualBonus { get; set; }
    public int VacationDays { get; set; }
}</code></pre><p>And finally, add the <code>PartTimeEmployee</code> subclass:</p><pre><code class="language-csharp">public class PartTimeEmployee: Employee
{
    public decimal HourlyRate { get; set; }
    public int WeeklyHours { get; set; }
}</code></pre><p><strong>Step 4: Set up DbContext</strong></p><p>As a decisive step, I will specify how I want to handle relationships.</p><pre><code class="language-csharp">using Microsoft.EntityFrameworkCore;

public class ApplicationDbContext: DbContext
{
    public DbSet&lt;Employee&gt; Employees =&gt; Set&lt;Employee&gt;();

    protected override void OnConfiguring(DbContextOptionsBuilder options)
    {
        options.UseNpgsql(
            "Connection string with db name tphDb");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity&lt;Employee&gt;()
            .HasDiscriminator&lt;EmployeeTypeEnum&gt;("EmployeeType")
            .HasValue&lt;FullTimeEmployee&gt;(EmployeeTypeEnum.FullTimeEmployee)
            .HasValue&lt;PartTimeEmployee&gt;(EmployeeTypeEnum.PartTimeEmployee)
            .HasValue&lt;Contractor&gt;(EmployeeTypeEnum.Contractor);
    }
}</code></pre><p>Inside the <code>OnModelCreating</code> method, I configure EF Core to store all derived types in a single table and use a column to indicate which CLR type each row represents, following TPH. That discriminator is <code>EmployeeType</code>, which in this case is an enum.</p><p><strong>Step 5: Configure Program.cs</strong></p><pre><code class="language-csharp">using Microsoft.EntityFrameworkCore;

using var db = new ApplicationDbContext();

var fullTime = new FullTimeEmployee
{
    Name = "Ali Hamza",
    Email = "ali@company.com",
    HireDate = DateTime.UtcNow.AddYears(-2),
    BaseSalary = 150000,
    AnnualBonus = 30000,
    VacationDays = 25
};

var partTime = new PartTimeEmployee
{
    Name = "James Anderson",
    Email = "james@anderson.com",
    HireDate = DateTime.UtcNow.AddMonths(-6),
    BaseSalary = 0,
    HourlyRate = 1200,
    WeeklyHours = 20
};

var contractor = new Contractor
{
    Name = "Frank Doe",
    Email = "Frank@agency.com",
    HireDate = DateTime.UtcNow.AddMonths(-3),
    BaseSalary = 0,
    ContractEndDate = DateTime.UtcNow.AddMonths(9),
    AgencyName = "TechStaff Ltd"
};

db.Employees.AddRange(fullTime, partTime, contractor);
db.SaveChanges();

Console.WriteLine("Employees inserted.");

var partTimers = 
    await db.Employees.OfType&lt;PartTimeEmployee&gt;().ToListAsync();

foreach (var item in partTimers)
{
    Console.WriteLine(item.Name);
    Console.WriteLine(item.Email);
    Console.WriteLine(item.HireDate);
    Console.WriteLine(item.BaseSalary);
    Console.WriteLine(item.WeeklyHours);
    Console.WriteLine(item.HourlyRate);
}

var employees = await db.Employees.ToListAsync();

foreach (var emp in employees)
{
    Console.WriteLine($"[{emp.GetType().Name}] {emp.Name}");

    if (emp is FullTimeEmployee fte)
    {
        Console.WriteLine($"  Bonus: {fte.AnnualBonus}");
    }
    else if (emp is PartTimeEmployee pte)
    {
        Console.WriteLine($"  Hourly Rate: {pte.HourlyRate}");
    }
    else if (emp is Contractor c)
    {
        Console.WriteLine($"  Agency: {c.AgencyName}");
    }
}
</code></pre><p>We can fetch records either by a specific type, like <code>PartTimeEmployee</code> or with <code>Employees</code> using the polymorphic nature. </p><p><strong>Step 6: Run migration</strong></p><p>Migrate the database with all of the changes:</p><pre><code class="language-console">dotnet ef migrations add InitialDb</code></pre><pre><code class="language-console">dotnet ef database update</code></pre><p>Let us look inside the <code>InitialDb</code> migration to see the generated code:</p><pre><code class="language-csharp">using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;

#nullable disable

namespace EfCoreTph.Migrations
{
    /// &lt;inheritdoc /&gt;
    public partial class InitialDb : Migration
    {
        /// &lt;inheritdoc /&gt;
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "Employees",
                columns: table =&gt; new
                {
                    Id = table.Column&lt;int&gt;(type: "integer", nullable: false)
                        .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
                    Name = table.Column&lt;string&gt;(type: "text", nullable: false),
                    Email = table.Column&lt;string&gt;(type: "text", nullable: false),
                    HireDate = table.Column&lt;DateTime&gt;(type: "timestamp with time zone", nullable: false),
                    BaseSalary = table.Column&lt;decimal&gt;(type: "numeric", nullable: false),
                    EmployeeType = table.Column&lt;byte&gt;(type: "smallint", nullable: false),
                    ContractEndDate = table.Column&lt;DateTime&gt;(type: "timestamp with time zone", nullable: true),
                    AgencyName = table.Column&lt;string&gt;(type: "text", nullable: true),
                    AnnualBonus = table.Column&lt;decimal&gt;(type: "numeric", nullable: true),
                    VacationDays = table.Column&lt;int&gt;(type: "integer", nullable: true),
                    HourlyRate = table.Column&lt;decimal&gt;(type: "numeric", nullable: true),
                    WeeklyHours = table.Column&lt;int&gt;(type: "integer", nullable: true)
                },
                constraints: table =&gt;
                {
                    table.PrimaryKey("PK_Employees", x =&gt; x.Id);
                });
        }

        /// &lt;inheritdoc /&gt;
        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "Employees");
        }
    }
}
</code></pre><p>A single table is created, with all common properties non-nullable and type-specific properties nullable.</p><p>The table and seed data in the database look like this:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/02/image-8.png" class="kg-image" alt="Tables" loading="lazy" width="248" height="117"></figure><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/02/image-9.png" class="kg-image" alt="Employees table" loading="lazy" width="1075" height="260" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/02/image-9.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/02/image-9.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/02/image-9.png 1075w" sizes="(min-width: 720px) 720px"></figure><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/02/image-10.png" class="kg-image" alt="Employees table" loading="lazy" width="815" height="130" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/02/image-10.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/02/image-10.png 815w" sizes="(min-width: 720px) 720px"></figure><p>We observe that type-specific columns are null for records of other types.</p><p><strong>Step 7: Run and test the application</strong></p><p>Let's run the project:</p><pre><code class="language-console">dotnet run</code></pre><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/02/image-7.png" class="kg-image" alt="Result" loading="lazy" width="442" height="413"></figure><h3 id="when-is-tph-best">When is TPH best </h3><ul><li>For domain-driven design, TPH is best suited to lower complexity and is fully supported by EF Core.</li><li>It offers the least complexity, and LINQ works naturally.</li><li>Optimal when types are closely related, and there are fewer chances of null values.</li></ul><h3 id="when-to-avoid-tph">When to avoid TPH</h3><ul><li>Sometimes strength becomes a liability. So is the case with TPH. If your entities are unrelated, then a single table can be overwhelmed by null values.</li><li>Due to null columns, TPH can be inefficient if you have too many derived entities.</li></ul><h2 id="table-per-type-tpt-ef-core-polymorphic-relationship-implementation">Table-Per-Type (TPT) EF core polymorphic relationship implementation</h2><p>Another type of relationship EF offers is Table-Per-Type (TPT). As the name suggests, parent and child entities contain their own table joined via foreign keys. Let's check how we can do it with a new sample project.</p><p><strong>Step 1: Create a project</strong></p><pre><code class="language-console">dotnet new console -o EfCoreTpt</code></pre><p> <strong>Step 2: Install the required packages</strong></p><pre><code class="language-console">dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
</code></pre><p><strong>Step 3: Define models</strong></p><p>We will create the same models as in the sample above.</p><p><strong>Step 4: Set up DbContext</strong></p><pre><code class="language-csharp">using Microsoft.EntityFrameworkCore;

namespace EfCoreTpt.Data;

public class ApplicationDbContext: DbContext
{
    public DbSet&lt;Employee&gt; Employees =&gt; Set&lt;Employee&gt;();

    protected override void OnConfiguring(DbContextOptionsBuilder options)
    {
        options.UseNpgsql(
            "connection string with db name tptDb");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity&lt;Employee&gt;().UseTptMappingStrategy();

        modelBuilder.Entity&lt;FullTimeEmployee&gt;().ToTable("FullTimeEmployees");
        modelBuilder.Entity&lt;PartTimeEmployee&gt;().ToTable("PartTimeEmployees");
        modelBuilder.Entity&lt;Contractor&gt;().ToTable("Contractors");
    }
}</code></pre><p>Here, with <code>UseTptMappingStrategy</code> I have to specify the base type of <code>Employee</code> with TPT mapping. You can see that other types are also mapped to their table.</p><p><strong>Step 5: Configure Program.cs</strong></p><pre><code class="language-csharp">using var db = new ApplicationDbContext();

var fullTime = new FullTimeEmployee
{
    Name = "Ali Hamza",
    Email = "ali@company.com",
    HireDate = DateTime.UtcNow.AddYears(-2),
    BaseSalary = 150000,
    AnnualBonus = 30000,
    VacationDays = 25
};

var partTime = new PartTimeEmployee
{
    Name = "James Anderson",
    Email = "james@anderson.com",
    HireDate = DateTime.UtcNow.AddMonths(-6),
    BaseSalary = 0,
    HourlyRate = 1200,
    WeeklyHours = 20
};

var contractor = new Contractor
{
    Name = "Frank Doe",
    Email = "Frank@agency.com",
    HireDate = DateTime.UtcNow.AddMonths(-3),
    BaseSalary = 0,
    ContractEndDate = DateTime.UtcNow.AddMonths(9),
    AgencyName = "TechStaff Ltd"
};

db.Employees.AddRange(fullTime, partTime, contractor);
db.SaveChanges();

Console.WriteLine("Employees inserted.");

var employees = db.Employees.ToList();

foreach (var emp in employees)
{
    Console.WriteLine($"[{emp.GetType().Name}] {emp.Name}");

    switch (emp)
    {
        case FullTimeEmployee f:
            Console.WriteLine($"  Bonus: {f.AnnualBonus}");
            break;

        case PartTimeEmployee p:
            Console.WriteLine($"  Hourly: {p.HourlyRate}");
            break;

        case Contractor c:
            Console.WriteLine($"  Agency: {c.AgencyName}");
            break;
    }
}
</code></pre><p><strong>Step 6: Run migration</strong></p><p>Again, let us update the database:</p><pre><code class="language-console">dotnet ef migrations add InitialDb</code></pre><pre><code class="language-console">dotnet ef database update</code></pre><p>And look at the generated migration class:</p><pre><code class="language-csharp">using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;

#nullable disable

namespace EfCoreTpt.Migrations
{
    /// &lt;inheritdoc /&gt;
    public partial class InitialDb : Migration
    {
        /// &lt;inheritdoc /&gt;
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "Employees",
                columns: table =&gt; new
                {
                    Id = table.Column&lt;int&gt;(type: "integer", nullable: false)
                        .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
                    Name = table.Column&lt;string&gt;(type: "text", nullable: false),
                    Email = table.Column&lt;string&gt;(type: "text", nullable: false),
                    HireDate = table.Column&lt;DateTime&gt;(type: "timestamp with time zone", nullable: false),
                    BaseSalary = table.Column&lt;decimal&gt;(type: "numeric", nullable: false)
                },
                constraints: table =&gt;
                {
                    table.PrimaryKey("PK_Employees", x =&gt; x.Id);
                });

            migrationBuilder.CreateTable(
                name: "Contractors",
                columns: table =&gt; new
                {
                    Id = table.Column&lt;int&gt;(type: "integer", nullable: false),
                    ContractEndDate = table.Column&lt;DateTime&gt;(type: "timestamp with time zone", nullable: false),
                    AgencyName = table.Column&lt;string&gt;(type: "text", nullable: false)
                },
                constraints: table =&gt;
                {
                    table.PrimaryKey("PK_Contractors", x =&gt; x.Id);
                    table.ForeignKey(
                        name: "FK_Contractors_Employees_Id",
                        column: x =&gt; x.Id,
                        principalTable: "Employees",
                        principalColumn: "Id",
                        onDelete: ReferentialAction.Cascade);
                });

            migrationBuilder.CreateTable(
                name: "FullTimeEmployees",
                columns: table =&gt; new
                {
                    Id = table.Column&lt;int&gt;(type: "integer", nullable: false),
                    AnnualBonus = table.Column&lt;decimal&gt;(type: "numeric", nullable: false),
                    VacationDays = table.Column&lt;int&gt;(type: "integer", nullable: false)
                },
                constraints: table =&gt;
                {
                    table.PrimaryKey("PK_FullTimeEmployees", x =&gt; x.Id);
                    table.ForeignKey(
                        name: "FK_FullTimeEmployees_Employees_Id",
                        column: x =&gt; x.Id,
                        principalTable: "Employees",
                        principalColumn: "Id",
                        onDelete: ReferentialAction.Cascade);
                });

            migrationBuilder.CreateTable(
                name: "PartTimeEmployees",
                columns: table =&gt; new
                {
                    Id = table.Column&lt;int&gt;(type: "integer", nullable: false),
                    HourlyRate = table.Column&lt;decimal&gt;(type: "numeric", nullable: false),
                    WeeklyHours = table.Column&lt;int&gt;(type: "integer", nullable: false)
                },
                constraints: table =&gt;
                {
                    table.PrimaryKey("PK_PartTimeEmployees", x =&gt; x.Id);
                    table.ForeignKey(
                        name: "FK_PartTimeEmployees_Employees_Id",
                        column: x =&gt; x.Id,
                        principalTable: "Employees",
                        principalColumn: "Id",
                        onDelete: ReferentialAction.Cascade);
                });
        }

        /// &lt;inheritdoc /&gt;
        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "Contractors");

            migrationBuilder.DropTable(
                name: "FullTimeEmployees");

            migrationBuilder.DropTable(
                name: "PartTimeEmployees");

            migrationBuilder.DropTable(
                name: "Employees");
        }
    }
}
</code></pre><p>One notable aspect here is the use of foreign keys to link tables. TPT is the only one that uses foreign keys to represent the inheritance itself. Other strategies can still use FKs for normal relationships (like <code>Employee</code> has a <code>Laptop</code>).</p><p>The database and data now look like this:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/02/image-12.png" class="kg-image" alt="Tables" loading="lazy" width="224" height="147"></figure><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/02/image-13.png" class="kg-image" alt="Contractors table" loading="lazy" width="460" height="175"></figure><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/02/image-14.png" class="kg-image" alt="Full time employees table" loading="lazy" width="407" height="199"></figure><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/02/image-15.png" class="kg-image" alt="Part time employees table" loading="lazy" width="358" height="188"></figure><p>Although I added records using the base class, they are written into their respective tables.</p><p><strong>Step 7: Run and test the application</strong></p><pre><code class="language-console">dotnet run</code></pre><p>EF Core generates joins in every query, such as:</p><pre><code class="language-SQL">SELECT ...
FROM Employees e
LEFT JOIN FullTimeEmployees f ON e.Id = f.Id
LEFT JOIN PartTimeEmployees p ON e.Id = p.Id
LEFT JOIN Contractors c ON e.Id = c.Id</code></pre><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/02/image-11.png" class="kg-image" alt="Results" loading="lazy" width="564" height="270"></figure><h3 id="when-is-tpt-best">When is TPT best</h3><ul><li>With each type having its own table, the database schema remained clean.</li><li>The database is inherently well normalized.</li><li>Records contain minimal nulls. TPT is optimal when your application has big inheritance trees.</li></ul><h3 id="when-to-avoid-tpt">When to avoid TPT</h3><ul><li>TPT can be problematic in performance-critical systems, potentially slowing data reading.</li><li>Queries heavily rely on joins.</li></ul><h2 id="table-per-concrete-tpc-ef-core-polymorphic-relationship-implementation">Table-Per-Concrete (TPC) EF Core polymorphic relationship implementation</h2><p>The last approach in EF Core polymorphic relationships is Table-Per-Concrete-Class (TPC). The parent class has no table, while each concrete class contains its own table. Each table repeats inherited fields as its columns. Like with the previous types, let us create a sample project to show how it works.</p><p><strong>Step 1: Create a project</strong></p><pre><code class="language-console">dotnet new console -o EfCoreTpc</code></pre><p> <strong>Step 2: Install the required packages</strong></p><pre><code class="language-console">dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
</code></pre><p><strong>Step 3: Define models</strong></p><p>We will use the same models as in the sample above.</p><p><strong>Step 4: Set up DbContext</strong></p><p>This is the most crucial step, as with the others. Here, I will specify how I want to deal with relationships:</p><pre><code class="language-csharp">using Microsoft.EntityFrameworkCore;

public class ApplicationDbContext: DbContext
{
    public DbSet&lt;Employee&gt; Employees =&gt; Set&lt;Employee&gt;();
    public DbSet&lt;FullTimeEmployee&gt; FullTimeEmployees =&gt; Set&lt;FullTimeEmployee&gt;();
    public DbSet&lt;PartTimeEmployee&gt; PartTimeEmployees =&gt; Set&lt;PartTimeEmployee&gt;();
    public DbSet&lt;Contractor&gt; Contractors =&gt; Set&lt;Contractor&gt;();

    protected override void OnConfiguring(DbContextOptionsBuilder options)
    {
        options.UseNpgsql(
            "Connectionstring with db name tpcDb");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity&lt;Employee&gt;()
            .UseTpcMappingStrategy();
    }
}</code></pre><p>The code <code>modelBuilder.Entity().UseTpcMappingStrategy()</code> instructs EF Core to use TPC mappings.</p><p><strong>Step 5: Configure Program.cs</strong></p><pre><code class="language-csharp">using var db = new ApplicationDbContext();

var fullTime = new FullTimeEmployee
{
    Name = "Ali Hamza",
    Email = "ali@company.com",
    HireDate = DateTime.UtcNow.AddYears(-2),
    BaseSalary = 150000,
    AnnualBonus = 30000,
    VacationDays = 25
};

var partTime = new PartTimeEmployee
{
    Name = "James Anderson",
    Email = "james@anderson.com",
    HireDate = DateTime.UtcNow.AddMonths(-6),
    BaseSalary = 0,
    HourlyRate = 1200,
    WeeklyHours = 20
};

var contractor = new Contractor
{
    Name = "Frank Doe",
    Email = "Frank@agency.com",
    HireDate = DateTime.UtcNow.AddMonths(-3),
    BaseSalary = 0,
    ContractEndDate = DateTime.UtcNow.AddMonths(9),
    AgencyName = "TechStaff Ltd"
};

db.Employees.AddRange(fullTime, partTime, contractor);
db.SaveChanges();

Console.WriteLine("Employees inserted.");

var employees = db.Employees.ToList();

foreach (var emp in employees)
{
    Console.WriteLine($"[{emp.GetType().Name}] {emp.Name}");
}
</code></pre><p>Here, I am leveraging polymorphic behaviour by inserting records into the <code>Employees</code> data set.</p><p><strong>Step 6: Run migration</strong></p><p>Create a migration and update the database:</p><pre><code class="language-console">dotnet ef migrations add InitialDb</code></pre><pre><code class="language-console">dotnet ef database update</code></pre><p>The new migration class looks like this:</p><pre><code class="language-csharp">using System;
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace EfCoreTpc.Migrations
{
    /// &lt;inheritdoc /&gt;
    public partial class InitialDb : Migration
    {
        /// &lt;inheritdoc /&gt;
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateSequence(
                name: "EmployeeSequence");

            migrationBuilder.CreateTable(
                name: "Contractors",
                columns: table =&gt; new
                {
                    Id = table.Column&lt;int&gt;(type: "integer", nullable: false, defaultValueSql: "nextval('\"EmployeeSequence\"')"),
                    Name = table.Column&lt;string&gt;(type: "text", nullable: false),
                    Email = table.Column&lt;string&gt;(type: "text", nullable: false),
                    HireDate = table.Column&lt;DateTime&gt;(type: "timestamp with time zone", nullable: false),
                    BaseSalary = table.Column&lt;decimal&gt;(type: "numeric", nullable: false),
                    ContractEndDate = table.Column&lt;DateTime&gt;(type: "timestamp with time zone", nullable: false),
                    AgencyName = table.Column&lt;string&gt;(type: "text", nullable: false)
                },
                constraints: table =&gt;
                {
                    table.PrimaryKey("PK_Contractors", x =&gt; x.Id);
                });

            migrationBuilder.CreateTable(
                name: "FullTimeEmployees",
                columns: table =&gt; new
                {
                    Id = table.Column&lt;int&gt;(type: "integer", nullable: false, defaultValueSql: "nextval('\"EmployeeSequence\"')"),
                    Name = table.Column&lt;string&gt;(type: "text", nullable: false),
                    Email = table.Column&lt;string&gt;(type: "text", nullable: false),
                    HireDate = table.Column&lt;DateTime&gt;(type: "timestamp with time zone", nullable: false),
                    BaseSalary = table.Column&lt;decimal&gt;(type: "numeric", nullable: false),
                    AnnualBonus = table.Column&lt;decimal&gt;(type: "numeric", nullable: false),
                    VacationDays = table.Column&lt;int&gt;(type: "integer", nullable: false)
                },
                constraints: table =&gt;
                {
                    table.PrimaryKey("PK_FullTimeEmployees", x =&gt; x.Id);
                });

            migrationBuilder.CreateTable(
                name: "PartTimeEmployees",
                columns: table =&gt; new
                {
                    Id = table.Column&lt;int&gt;(type: "integer", nullable: false, defaultValueSql: "nextval('\"EmployeeSequence\"')"),
                    Name = table.Column&lt;string&gt;(type: "text", nullable: false),
                    Email = table.Column&lt;string&gt;(type: "text", nullable: false),
                    HireDate = table.Column&lt;DateTime&gt;(type: "timestamp with time zone", nullable: false),
                    BaseSalary = table.Column&lt;decimal&gt;(type: "numeric", nullable: false),
                    HourlyRate = table.Column&lt;decimal&gt;(type: "numeric", nullable: false),
                    WeeklyHours = table.Column&lt;int&gt;(type: "integer", nullable: false)
                },
                constraints: table =&gt;
                {
                    table.PrimaryKey("PK_PartTimeEmployees", x =&gt; x.Id);
                });
        }

        /// &lt;inheritdoc /&gt;
        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "Contractors");

            migrationBuilder.DropTable(
                name: "FullTimeEmployees");

            migrationBuilder.DropTable(
                name: "PartTimeEmployees");

            migrationBuilder.DropSequence(
                name: "EmployeeSequence");
        }
    }
}
</code></pre><p>The code <code>migrationBuilder.CreateSequence(name: "EmployeeSequence")</code> specifies global ID generation across the hierarchy. Instead of independent identity generators, EF Core uses one shared sequence.</p><p>The database now looks like this:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/02/image-1.png" class="kg-image" alt="Tables" loading="lazy" width="229" height="168"></figure><p><strong>Step 7: Run and test the application</strong></p><pre><code class="language-console">dotnet run</code></pre><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/02/image-2.png" class="kg-image" alt="Results" loading="lazy" width="437" height="152"></figure><p>A high-level view of the generated query is:</p><pre><code class="language-SQL">SELECT ... FROM FullTimeEmployees
UNION ALL
SELECT ... FROM PartTimeEmployees
UNION ALL
SELECT ... FROM Contractors</code></pre><p>And the rows returned look like in the following screenshots:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/02/image-3.png" class="kg-image" alt="Contractors table" loading="lazy" width="934" height="213" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/02/image-3.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/02/image-3.png 934w" sizes="(min-width: 720px) 720px"></figure><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/02/image-4.png" class="kg-image" alt="Fulltime employees table" loading="lazy" width="844" height="197" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/02/image-4.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/02/image-4.png 844w" sizes="(min-width: 720px) 720px"></figure><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/02/image-5.png" class="kg-image" alt="PartTime Employees" loading="lazy" width="895" height="210" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/02/image-5.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/02/image-5.png 895w" sizes="(min-width: 720px) 720px"></figure><h3 id="when-is-tpc-best">When is TPC best</h3><ul><li>TPC applies when the application requires performing extensive queries on concrete types.</li><li>When you are designing a fast read-heavy system.</li><li>When you want to keep clean tables with no nulls.</li></ul><h3 id="when-to-avoid-tpc">When to avoid TPC</h3><ul><li>TPC can slow down when the application has many polymorphic queries.</li><li>Do not use with large inheritance trees.</li><li>Not optimal, frequent schema changes.</li><li>Duplicated columns can be problematic for some users.</li></ul><h2 id="conclusion">Conclusion</h2><p>Entity Framework provides different ways to design entities. They are not fixed for any use, but we tried to see by example how each one creates tables and how its internal relationships work. A quick summary of the types and features can be seen here:</p>
<!--kg-card-begin: html-->
<table data-path-to-node="11" style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 0px none rgb(31, 31, 31); inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: table; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 32px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 0px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;"><thead style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 0px none rgb(31, 31, 31); inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: table-header-group; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 0px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;"><tr style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 0px none rgb(31, 31, 31); inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: table-row; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 0px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;"><td style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgb(239, 239, 239); border: 1px solid; inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: table-cell; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 8px 12px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;"><strong style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 0px none rgb(31, 31, 31); inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: inline; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px !important; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 0px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;">Feature</strong></td><td style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgb(239, 239, 239); border: 1px solid; inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: table-cell; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 8px 12px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;"><strong style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 0px none rgb(31, 31, 31); inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: inline; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px !important; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 0px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;">TPH (Hierarchy)</strong></td><td style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgb(239, 239, 239); border: 1px solid; inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: table-cell; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 8px 12px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;"><strong style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 0px none rgb(31, 31, 31); inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: inline; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px !important; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 0px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;">TPT (Type)</strong></td><td style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgb(239, 239, 239); border: 1px solid; inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: table-cell; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 8px 12px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;"><strong style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 0px none rgb(31, 31, 31); inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: inline; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px !important; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 0px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;">TPC (Concrete)</strong></td></tr></thead><tbody style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 0px none rgb(31, 31, 31); inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: table-row-group; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 0px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;"><tr style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 0px none rgb(31, 31, 31); inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: table-row; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 0px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;"><td style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 1px solid; inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: table-cell; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 8px 12px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;"><span data-path-to-node="11,1,0,0" style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 0px none rgb(31, 31, 31); inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: inline; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 0px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;"><b data-path-to-node="11,1,0,0" data-index-in-node="0" style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 0px none rgb(31, 31, 31); inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: inline; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 0px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;">Tables</b></span></td><td style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 1px solid; inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: table-cell; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 8px 12px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;"><span data-path-to-node="11,1,1,0" style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 0px none rgb(31, 31, 31); inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: inline; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 0px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;">One single table</span></td><td style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 1px solid; inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: table-cell; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 8px 12px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;"><span data-path-to-node="11,1,2,0" style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 0px none rgb(31, 31, 31); inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: inline; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 0px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;">One base + One per type</span></td><td style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 1px solid; inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: table-cell; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 8px 12px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;"><span data-path-to-node="11,1,3,0" style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 0px none rgb(31, 31, 31); inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: inline; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 0px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;">One per concrete type</span></td></tr><tr style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 0px none rgb(31, 31, 31); inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: table-row; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 0px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;"><td style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 1px solid; inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: table-cell; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 8px 12px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;"><span data-path-to-node="11,2,0,0" style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 0px none rgb(31, 31, 31); inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: inline; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 0px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;"><b data-path-to-node="11,2,0,0" data-index-in-node="0" style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 0px none rgb(31, 31, 31); inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: inline; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 0px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;">Performance</b></span></td><td style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 1px solid; inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: table-cell; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 8px 12px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;"><span data-path-to-node="11,2,1,0" style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 0px none rgb(31, 31, 31); inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: inline; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 0px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;">Fastest (No joins)</span></td><td style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 1px solid; inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: table-cell; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 8px 12px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;"><span data-path-to-node="11,2,2,0" style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 0px none rgb(31, 31, 31); inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: inline; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 0px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;">Slowest (Many joins)</span></td><td style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 1px solid; inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: table-cell; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 8px 12px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;"><span data-path-to-node="11,2,3,0" style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 0px none rgb(31, 31, 31); inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: inline; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 0px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;">Fast for specific types</span></td></tr><tr style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 0px none rgb(31, 31, 31); inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: table-row; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 0px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;"><td style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 1px solid; inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: table-cell; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 8px 12px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;"><span data-path-to-node="11,3,0,0" style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 0px none rgb(31, 31, 31); inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: inline; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 0px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;"><b data-path-to-node="11,3,0,0" data-index-in-node="0" style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 0px none rgb(31, 31, 31); inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: inline; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 0px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;">Nullability</b></span></td><td style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 1px solid; inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: table-cell; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 8px 12px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;"><span data-path-to-node="11,3,1,0" style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 0px none rgb(31, 31, 31); inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: inline; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 0px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;">Many nullable columns</span></td><td style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 1px solid; inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: table-cell; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 8px 12px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;"><span data-path-to-node="11,3,2,0" style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 0px none rgb(31, 31, 31); inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: inline; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 0px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;">No nulls (normalized)</span></td><td style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 1px solid; inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: table-cell; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 8px 12px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;"><span data-path-to-node="11,3,3,0" style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 0px none rgb(31, 31, 31); inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: inline; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 0px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;">No nulls (denormalized)</span></td></tr><tr style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 0px none rgb(31, 31, 31); inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: table-row; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 0px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;"><td style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 1px solid; inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: table-cell; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 8px 12px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;"><span data-path-to-node="11,4,0,0" style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 0px none rgb(31, 31, 31); inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: inline; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 0px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;"><b data-path-to-node="11,4,0,0" data-index-in-node="0" style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 0px none rgb(31, 31, 31); inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: inline; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 0px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;">Best For</b></span></td><td style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 1px solid; inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: table-cell; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 8px 12px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;"><span data-path-to-node="11,4,1,0" style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 0px none rgb(31, 31, 31); inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: inline; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 0px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;">Simple hierarchies</span></td><td style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 1px solid; inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: table-cell; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 8px 12px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;"><span data-path-to-node="11,4,2,0" style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 0px none rgb(31, 31, 31); inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: inline; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 0px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;">Complex, strict schemas</span></td><td style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 1px solid; inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: table-cell; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 8px 12px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;"><span data-path-to-node="11,4,3,0" style="animation: auto ease 0s 1 normal none running none; appearance: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 0px none rgb(31, 31, 31); inset: auto; clear: none; clip: auto; color: rgb(31, 31, 31); columns: auto; contain: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: inline; fill: rgb(0, 0, 0); filter: none; flex: 0 1 auto; float: none; gap: normal; hyphens: manual; interactivity: auto; isolation: auto; margin-top: 0px !important; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; marker: none; mask: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(31, 31, 31) none 0px; overlay: none; padding: 0px; page: auto; perspective: none; position: static; quotes: auto; r: 0px; resize: none; rotate: none; rx: auto; ry: auto; scale: none; speak: normal; stroke: none; transform: none; transition: all; translate: none; visibility: visible; widows: 2; x: 0px; y: 0px; zoom: 1; font-family: &quot;Google Sans Text&quot;, sans-serif !important; line-height: 1.15 !important;">Large sets, specific queries</span></td></tr></tbody></table>
<!--kg-card-end: html-->
<p>In this blog post, I delved deeper into TPH, TPT, and TPC. I hope it helps you decide on which strategy to use for your database design.</p><p>Code: <a href="https://github.com/elmahio-blog/PolymorphicRelEfCore">https://github.com/elmahio-blog/PolymorphicRelEfCore</a></p> ]]></content:encoded>
    </item>
    <item>
        <title><![CDATA[ Implementing strongly-typed IDs in .NET for safer domain models ]]></title>
        <description><![CDATA[ As developers, we know that user requests can be unpredictable. When they request data, it is either successfully returned or not found. However, a &quot;not found&quot; result usually happens in two ways. First, the user might request a record, such as a basketball player, by passing an ID ]]></description>
        <link>https://blog.elmah.io/pimplementing-strongly-typed-ids-in-net-for-safer-domain-models/</link>
        <guid isPermaLink="false">6963d6ddd15e1a0001489057</guid>
        <category><![CDATA[  ]]></category>
        <dc:creator><![CDATA[ Ali Hamza Ansari ]]></dc:creator>
        <pubDate>Tue, 10 Feb 2026 08:44:05 +0100</pubDate>
        <media:content url="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/01/implementing-strongly-typed-ids-in-dot.net-for-safer-domain-o.png" medium="image"/>
        <content:encoded><![CDATA[ <p>This blog post is originally published on <a href="https://blog.elmah.io/pimplementing-strongly-typed-ids-in-net-for-safer-domain-models/">https://blog.elmah.io/pimplementing-strongly-typed-ids-in-net-for-safer-domain-models/</a></p> 
<!--kg-card-begin: html-->
<div class="toc"></div>
<!--kg-card-end: html-->
<p>As developers, we know that user requests can be unpredictable. When they request data, it is either successfully returned or not found. However, a "not found" result usually happens in two ways. First, the user might request a record, such as a basketball player, by passing an ID that doesn't exist. Second, they might pass the wrong type of ID, like a Team ID, while trying to fetch a Player. The first situation is common, but the second should be handled to avoid logic pitfalls. Enforcing this validation for every entity can be a humongous task when your application contains hundreds of entities. In this post, I will provide a way out of this situation so you can restrict your application from passing IDs interchangeably among entities.</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/01/implementing-strongly-typed-ids-in-dot.net-for-safer-domain-o-1.png" class="kg-image" alt="Implementing strongly-typed IDs in .NET for safer domain models" loading="lazy" width="1500" height="750" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/01/implementing-strongly-typed-ids-in-dot.net-for-safer-domain-o-1.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/01/implementing-strongly-typed-ids-in-dot.net-for-safer-domain-o-1.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/01/implementing-strongly-typed-ids-in-dot.net-for-safer-domain-o-1.png 1500w" sizes="(min-width: 1200px) 1200px"></figure><h2 id="primitive-obsession">Primitive obsession</h2><p>Primitive obsession refers to a situation where we use <em>primitive</em>&nbsp;data types, such as int, string, or bool, to represent complex data. Usually, values like URLs, Addresses, or identities are saved as strings. This creates a hidden breach of encapsulation because it forces you to add an extra layer of validation somewhere else in the code.</p><p>For domain-specific entities like URLs or addresses, <a href="https://blog.elmah.io/mastering-owned-entities-in-ef-core-cleaner-complex-types/" rel="noreferrer">you can use owned entities</a>. However, to define domain-specific identities, we need strongly-typed IDs.</p><h2 id="primitive-id-implementation-for-ef-core-model-entity">Primitive Id implementation for EF Core model entity</h2><p>Let's first understand the problem with the traditional way. I will create a console application with a PostgreSQL database to catch the issue at hand.</p><p><strong>Step 1: Create the project</strong></p><pre><code class="language-console">dotnet new console -n IntIdsDemo
cd IntIdsDemo
</code></pre><p><strong>Step 2: Install necessary packages</strong></p><pre><code class="language-console">dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
</code></pre><p><strong>Step 3: Define models</strong> </p><p>I'll create models to represent a player, a team, and the relationship between them.</p><p>Player</p><pre><code class="language-csharp">namespace IntIdsDemo.Domain.Entities;

public class Player
{
    public int Id { get; private set; }
    public string Name { get; private set; } = string.Empty;

    private Player() { } // Required by EF Core

    public Player(string name)
    {
        Name = name;
    }
}</code></pre><p>Team</p><pre><code class="language-csharp">namespace IntIdsDemo.Domain.Entities;

public class Team
{
    public int Id { get; private set; }
    public string Name { get; private set; } = string.Empty;

    private Team() { } 

    public Team(string name)
    {
        Name = name;
    }
}</code></pre><p>PlayerTeam to join them</p><pre><code class="language-csharp">namespace IntIdsDemo.Domain.Entities;

public class PlayerTeam
{
    public int Id { get; private set; }
    public int PlayerId { get; set; }
    public int TeamId { get; set; }

    private PlayerTeam() { }

    public PlayerTeam(int playerId, int teamId)
    {
        PlayerId = playerId;
        TeamId = teamId;
    }
}</code></pre><p><strong>Step 4: Configure ApplicationDbContext</strong></p><pre><code class="language-csharp">using IntIdsDemo.Domain.Entities;
using Microsoft.EntityFrameworkCore;

namespace IntIdsDemo.Data;

public class ApplicationDbContext: DbContext
{
    public DbSet&lt;Player&gt; Players =&gt; Set&lt;Player&gt;();
    public DbSet&lt;Team&gt; Teams =&gt; Set&lt;Team&gt;();
    public DbSet&lt;PlayerTeam&gt; PlayerTeams =&gt; Set&lt;PlayerTeam&gt;();

    protected override void OnConfiguring(DbContextOptionsBuilder options)
    {
        options.UseNpgsql(
            "Connection string with database simpleIdsDb");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {        
    }
}</code></pre><p><strong>Step 5: Define Program.cs</strong></p><pre><code class="language-csharp">using IntIdsDemo.Data;
using IntIdsDemo.Domain.Entities;

using var db = new ApplicationDbContext();

// Seed data
var player1 = new Player("Ali");
var player2 = new Player("Hamza");
var team = new Team("Warriors");

db.Players.Add(player1);
db.Players.Add(player2);
db.Teams.Add(team);
db.SaveChanges();

Console.WriteLine("Player &amp; Team saved");

// Simple assignment method
void AssignPlayerToTeam(int playerId, int teamId)
{
    var playerTeam = new PlayerTeam(playerId, teamId);
    db.PlayerTeams.Add(playerTeam);
    db.SaveChanges();
}

AssignPlayerToTeam(player2.Id, team.Id);
AssignPlayerToTeam(team.Id, player2.Id);

Player player = db.Players.Find(team.Id);

if (player != null)
{
    Console.WriteLine($"Player Id (int): {player.Id}");
    Console.WriteLine($"Player Name: {player.Name}");
}

player = db.Players.Find(player2.Id);

if (player != null)
{
    Console.WriteLine($"Player Id (int): {player.Id}");
    Console.WriteLine($"Player Name: {player.Name}");
}</code></pre><p>I am initializing 2 players and 1 team here. Later, I assign player 2 to the team. First, I did it correctly as per the method's requirement, with <code>AssignPlayerToTeam(player2.Id, team.Id);</code> While in the second part, which is logically wrong, the compiler will give a green signal to <code>AssignPlayerToTeam(team.Id, player2.Id)</code>. Next, I am observing an issue with fetching data. I can easily pass <code>teamId</code> as the parameter for the <code>Find</code> method, where <code>playerId</code> (<code>db.Players.Find(team.Id)</code>) is expected. But since we are using the primitive type of int, it will compile without any errors.</p><p><strong>Step 6: Run the migration</strong></p><p>We need to create an empty database </p><pre><code class="language-sql">CREATE DATABASE simpleIdsDb;
</code></pre><p>And create a migration and update the database</p><pre><code class="language-console">dotnet ef migrations add InitialCreate
dotnet ef database update
</code></pre><p><strong>Step 7: run the project</strong></p><pre><code class="language-console">dotnet run</code></pre><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/01/image-11.png" class="kg-image" alt="Output" loading="lazy" width="283" height="133"></figure><p>Let's check the database. The Players table contains the following rows:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/01/image-8.png" class="kg-image" alt="Players table" loading="lazy" width="219" height="106"></figure><p>The Teams table contains a single row:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/01/image-9.png" class="kg-image" alt="Teams table" loading="lazy" width="228" height="82"></figure><p>And finally, the PlayerTeams table contains two relationship rows:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/01/image-10.png" class="kg-image" alt="PlayerTeams table" loading="lazy" width="410" height="151"></figure><p>As you can see, the assignment is done, and <code>PlayerId</code> and <code>TeamId</code> were assigned interchangeably. To prevent this at the database level, we would have to add specific constraints. However, at the coding level, this remains a significant issue. While the console output shows that the player was not found with <code>team.Id</code>, this behavior can still lead to data inconsistency or unexpected runtime errors. </p><h2 id="strongly-typed-id-implementation-for-safer-domain-model">Strongly-Typed ID implementation for safer domain model</h2><p>To keep focus on strongly-typed ID implementations, I will continue a similar project.</p><p><strong>Step 1: Create the project</strong></p><pre><code class="language-console">dotnet new console -n StronglyTypedIdsDemo
cd StronglyTypedIdsDemo
</code></pre><p><strong>Step 2: Install necessary packages</strong></p><pre><code class="language-console">dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
</code></pre><p><strong>Step 3: Create the Strongly-Typed IDs</strong></p><p>Traversing to the directory <code>Domain/Ids/</code> and add the following code for creating a player ID</p><pre><code class="language-csharp">namespace StronglyTypedIdsDemo.Domain.Ids;

public readonly record struct PlayerId(int Value)
{
    public override string ToString() =&gt; Value.ToString();
}</code></pre><p>And the following for creating a team ID</p><pre><code class="language-csharp">namespace StronglyTypedIdsDemo.Domain.Ids;

public readonly record struct TeamId(int Value)
{
    public override string ToString() =&gt; Value.ToString();
}</code></pre><p>And finally, the player/team relational ID</p><pre><code class="language-csharp">namespace StronglyTypedIdsDemo.Domain.Ids;

public readonly record struct PlayerTeamId(int Value)
{
    public override string ToString() =&gt; Value.ToString();
}</code></pre><p><code>struct</code> are the best fit for strongly-typed IDs. A struct cannot contain null, while a  class would allow it <code>null</code>, heap allocation, and reference semantics. <code>readonly</code> ensures immutability and cannot be modified after creation. <a href="https://blog.elmah.io/exploring-c-records-and-their-use-cases/" rel="noreferrer">As we know record is a value object</a>, <code>record struct</code> will give you value-based equality automatically.</p><p><strong>Step 4: Create the Entity</strong></p><p>Go to the directory <code>StronglyTypedIdsDemo.Domain.Entities</code> and add the following model classes</p><pre><code class="language-csharp">using StronglyTypedIdsDemo.Domain.Ids;

namespace StronglyTypedIdsDemo.Domain.Entities;

public class Player
{
    public PlayerId Id { get; private set; }
    public string Name { get; private set; } = string.Empty;

    private Player() { } 

    public Player(string name)
    {
        Name = name;
    }
}</code></pre><pre><code class="language-csharp">using StronglyTypedIdsDemo.Domain.Ids;

namespace StronglyTypedIdsDemo.Domain.Entities;

public class Team
{
    public TeamId Id { get; private set; }
    public string Name { get; private set; } = string.Empty;

    private Team() { } 

    public Team(string name)
    {
        Name = name;
    }
}</code></pre><pre><code class="language-csharp">using StronglyTypedIdsDemo.Domain.Ids;

namespace StronglyTypedIdsDemo.Domain.Entities;

public class PlayerTeam
{
    public PlayerTeamId Id { get; private set; }
    public PlayerId PlayerId { get; private set; }
    public TeamId TeamId { get; private set; }

    private PlayerTeam() { }

    public PlayerTeam(PlayerId playerId, TeamId teamId)
    {
        PlayerId = playerId;
        TeamId = teamId;
    }
}</code></pre><p>These classes look similar to the first application but all ID types have been switched out for the strongly typed versions.</p><p><strong>Step 5: Configure the DbContext</strong></p><pre><code class="language-csharp">using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using StronglyTypedIdsDemo.Domain.Entities;
using StronglyTypedIdsDemo.Domain.Ids;

namespace StronglyTypedIdsDemo.Data;

public class ApplicationDbContext: DbContext
{
    public DbSet&lt;Player&gt; Players =&gt; Set&lt;Player&gt;();
    public DbSet&lt;Team&gt; Teams =&gt; Set&lt;Team&gt;();
    public DbSet&lt;PlayerTeam&gt; PlayerTeams =&gt; Set&lt;PlayerTeam&gt;();

    protected override void OnConfiguring(DbContextOptionsBuilder options)
    {
        options.UseNpgsql(
            "Connection string with db name strongIdsDb");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        var playerIdConverter = new ValueConverter&lt;PlayerId, int&gt;(
            id =&gt; id.Value,
            value =&gt; new PlayerId(value));

        modelBuilder.Entity&lt;Player&gt;(entity =&gt;
        {
            entity.HasKey(x =&gt; x.Id);

            entity.Property(x =&gt; x.Id)
                .HasConversion(playerIdConverter)
                .ValueGeneratedOnAdd();

            entity.Property(x =&gt; x.Name)
                .IsRequired();
        });
        
        var teamIdConverter = new ValueConverter&lt;TeamId, int&gt;(
            id =&gt; id.Value,
            value =&gt; new TeamId(value));

        modelBuilder.Entity&lt;Team&gt;(entity =&gt;
        {
            entity.HasKey(x =&gt; x.Id);

            entity.Property(x =&gt; x.Id)
                .HasConversion(teamIdConverter)
                .ValueGeneratedOnAdd();

            entity.Property(x =&gt; x.Name)
                .IsRequired();
        });
        
        var playerTeamIdConverter = new ValueConverter&lt;PlayerTeamId, int&gt;(
            id =&gt; id.Value,
            value =&gt; new PlayerTeamId(value));

        modelBuilder.Entity&lt;PlayerTeam&gt;(entity =&gt;
        {
            entity.HasKey(x =&gt; x.Id);

            entity.Property(x =&gt; x.Id)
                .HasConversion(playerTeamIdConverter)
                .ValueGeneratedOnAdd();

            entity.Property(pt =&gt; pt.PlayerId)
                .HasConversion(playerIdConverter)
                .IsRequired();

            entity.Property(pt =&gt; pt.TeamId)
                .HasConversion(teamIdConverter)
                .IsRequired();
        });
    }
}</code></pre><p>I used <code>ValueConverter&lt;PlayerId, int&gt;</code> that tells EF Core to use <code>PlayerId</code> but save the value as int. similarly <code>TeamId</code> and <code>PlayerTeamId</code> are configured. Notice that adding a connection string through code is not recommended and only done for simplicity. Always use external configuration for this.</p><p><strong>Step 6: Configure Program.cs</strong></p><pre><code class="language-csharp">using StronglyTypedIdsDemo.Data;
using StronglyTypedIdsDemo.Domain.Entities;
using StronglyTypedIdsDemo.Domain.Ids;

using var db = new ApplicationDbContext();

// Seed data
var player1 = new Player("Ali");
var player2 = new Player("Hamza");
var team = new Team("Warriors");

db.Players.Add(player1);
db.Players.Add(player2);
db.Teams.Add(team);
db.SaveChanges();

Console.WriteLine("Player &amp; Team saved");

// Simple assignment method
void AssignPlayerToTeam(PlayerId playerId, TeamId teamId)
{
    var playerTeam = new PlayerTeam(playerId, teamId);
    db.PlayerTeams.Add(playerTeam);
    db.SaveChanges();
}

AssignPlayerToTeam(player2.Id, team.Id);

// ⚠️ Wrong order of parameter
AssignPlayerToTeam(team.Id, player2.Id);

// ⚠️ Wrong ID type for Find
Player player = db.Players.Find(team.Id);

if (player != null)
{
    Console.WriteLine($"Player Id (int): {player.Id}");
    Console.WriteLine($"Player Name: {player.Name}");
}

player = db.Players.Find(player2.Id);

if (player != null)
{
    Console.WriteLine($"Player Id (int): {player.Id}");
    Console.WriteLine($"Player Name: {player.Name}");
}</code></pre><p>In the code, I'm adding a record manually . In the line <code>AssignPlayerToTeam(team.Id, player2.Id)</code>, a compiler error is shown.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/01/image-14.png" class="kg-image" alt="Compile error" loading="lazy" width="938" height="31" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/01/image-14.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/01/image-14.png 938w" sizes="(min-width: 720px) 720px"></figure><p>The compiler now detects when we try to use the incorrect IDs when adding a user to a team.</p><p><strong>Sep 7: run migrations</strong></p><p>We need to create an empty database </p><pre><code class="language-sql">CREATE DATABASE strongIdsDb;
</code></pre><p>And add the migration and run it</p><pre><code class="language-console">dotnet ef migrations add InitialCreate
dotnet ef database update
</code></pre><p><strong>Step 8: Run and test</strong></p><pre><code class="language-console">dotnet run</code></pre><p>The second error is now revealed, since we are trying to use a team ID as the parameter for the <code>Find</code> method on the <code>Players</code> table</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/01/image-16.png" class="kg-image" alt="ArgumentException" loading="lazy" width="581" height="303"></figure><p>This means we are on the right track. Remove erroneous code and everything looks great</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/01/image-17.png" class="kg-image" alt="Output" loading="lazy" width="300" height="137"></figure><h2 id="what-strongly-typed-ids-have-improved">What strongly-typed IDs have improved?</h2><ul><li>Primitive IDs can lead to id-mixup at compile time. Defining identity structure. Each entity eliminates this shortcoming.</li><li>The traditional approach lacks domain meaning, whereas a strongly typed ID contains explicit intent.</li><li>Strongly-typed IDs removed the ambiguity of method signatures that happens when int, Guid, or other primitive types are used.</li><li>Traditionally, bugs are skipped at compile time, leading to runtime errors, while strongly-typed IDs catch ID mismatches at compile time.</li></ul><h2 id="conclusion">Conclusion</h2><p>Robustness is a big win for any application. Your code should handle issues right at the gateway. One common problem most people overlook is primitive obsession. Using types like ints for IDs allows you to pass IDs of different entities interchangeably. You can validate them manually, but that requires extra verbose code elsewhere, which breaks encapsulation. In this post, I have provided a solution using strongly-typed IDs to resolve that problem. We first looked at the traditional approach and observed the real-world issues caused by primitive obsession. Using separate value-object IDs makes your domain model much safer.</p><p>Code: <a href="https://github.com/elmahio-blog/StronglyTypedIdsDemo">https://github.com/elmahio-blog/StronglyTypedIdsDemo</a> </p> ]]></content:encoded>
    </item>
    <item>
        <title><![CDATA[ New in .NET 10 and C# 14: Multi-Tenant Rate Limiting ]]></title>
        <description><![CDATA[ .NET 10 is officially out, along with C# 14. Microsoft has released .NET 10 as Long-Term Support (LTS) as a successor to .NET 8. Like every version, it is not just an update but brings something new to the table. In this series, we will explore which aspects of software ]]></description>
        <link>https://blog.elmah.io/new-in-net-10-and-c-14-multi-tenant-rate-limiting/</link>
        <guid isPermaLink="false">69635c4cd15e1a0001488f69</guid>
        <category><![CDATA[  ]]></category>
        <dc:creator><![CDATA[ Ali Hamza Ansari ]]></dc:creator>
        <pubDate>Wed, 28 Jan 2026 10:06:50 +0100</pubDate>
        <media:content url="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/01/new-in-dotnet-10-and-csharp-14-multi-tenant-rate-limiting-o-1.png" medium="image"/>
        <content:encoded><![CDATA[ <p>This blog post is originally published on <a href="https://blog.elmah.io/new-in-net-10-and-c-14-multi-tenant-rate-limiting/">https://blog.elmah.io/new-in-net-10-and-c-14-multi-tenant-rate-limiting/</a></p> 
<!--kg-card-begin: html-->
<div class="toc">

</div>
<!--kg-card-end: html-->
<p>.NET 10 is officially out, along with C# 14. Microsoft has released .NET 10 as Long-Term Support (LTS) as a successor to .NET 8. Like every version, it is not just an update but brings something new to the table. <a href="https://blog.elmah.io/tag/whats-new-in-net-10/" rel="noreferrer">In this series</a>, we will explore which aspects of software can be upgraded with the latest release. Today, we will explore a multi-tenant rate limiter with .NET 10.</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/01/new-in-dotnet-10-and-csharp-14-multi-tenant-rate-limiting-o.png" class="kg-image" alt="New in .NET 10 and C# 14: Multi-Tenant Rate Limiting" loading="lazy" width="1500" height="750" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/01/new-in-dotnet-10-and-csharp-14-multi-tenant-rate-limiting-o.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/01/new-in-dotnet-10-and-csharp-14-multi-tenant-rate-limiting-o.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/01/new-in-dotnet-10-and-csharp-14-multi-tenant-rate-limiting-o.png 1500w" sizes="(min-width: 1200px) 1200px"></figure><p>APIs power your business. The statement is mostly true for all of your applications. The user accesses the information via mobile or web, and you provide it to them via an API. For SaaS, the access and information have more to manage from client to client. A premium client pays more and needs more throughput. Similarly, others will use the system according to their plan and user base. One factor of fair use among clients or tenants is rate limiting, that is, how many requests users of a client can hit the server. .NET 10 offers improvements in rate-limiting with its throttling middleware.</p><h2 id="designing-a-multi-tenant-rate-limiter">Designing a multi-tenant rate limiter</h2><p>To understand the multi-tenant rate limiter, we will create a simple web API. I will provide step-by-step instructions to break this down with .NET 10.</p><p><strong>Step 1: Create a .NET API project</strong></p><pre><code class="language-console">dotnet new webapi -n Net10RateLimiter -f net10.0
cd Net10RateLimiter
</code></pre><p><strong>Step 2: Create Models</strong></p><p>I organized models in the Models folder and created the following class:</p><pre><code class="language-csharp">namespace Net10RateLimiter.Models;

public class TenantRateLimit
{
    public string TenantId { get; set; } = string.Empty;
    public int RequestsPerMinute { get; set; }
}</code></pre><p>The class <code>TenantRateLimit</code> will define a tenant-specific rate using OOP. For a database-based application, we can save it as well.</p><p><strong>Step 3: Create a Limit resolver service</strong> </p><p>For that logic, I created a folder named Services, and the resolver class look like this:</p><pre><code class="language-csharp">namespace Net10RateLimiter.Services;

public sealed class TenantResolver
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public TenantResolver(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public string ResolveTenant()
    {
        var context = _httpContextAccessor.HttpContext;

        if (context is null)
            return "default";

        if (context.Request.Headers.TryGetValue("X-Tenant-ID", out var tenantId))
            return tenantId.ToString();

        return "default";
    }
}</code></pre><p>The resolver works as request-scoped logic to invalidate limits on each request. It extracts the tenant identity from the incoming request and returns.</p><p><strong>Step 4: Register Required Services</strong></p><p>Inject the required dependencies in <code>Program.cs</code></p><pre><code class="language-csharp">builder.Services.AddControllers();

// Required for HttpContext access
builder.Services.AddHttpContextAccessor();

// Tenant resolver must be scoped
builder.Services.AddScoped&lt;TenantResolver&gt;();
</code></pre><p><strong>Step 5: Add rate limiting logic</strong></p><p>Just after the dependency injections, add the following code:</p><pre><code class="language-csharp">builder.Services.AddRateLimiter(options =&gt;
{
    options.OnRejected = async (context, token) =&gt;
    {
        context.HttpContext.Response.StatusCode = 429;
        await context.HttpContext.Response.WriteAsync("Too Many Requests");
    };

    options.AddPolicy("TenantPolicy", context =&gt;
    {
        var tenantResolver =
            context.RequestServices.GetRequiredService&lt;TenantResolver&gt;();

        var tenantId = tenantResolver.ResolveTenant();
        var limit = GetTenantLimit(tenantId);

        return RateLimitPartition.GetTokenBucketLimiter(
            tenantId,
            _ =&gt; new TokenBucketRateLimiterOptions
            {
                TokenLimit = limit,
                TokensPerPeriod = limit,
                ReplenishmentPeriod = TimeSpan.FromMinutes(1),
                AutoReplenishment = true
            });
    });
});</code></pre><p><code>RateLimitPartition.GetTokenBucketLimiter(...)</code> is the throttling engine using fully native ASP.NET Core 10.</p><p>While at the top, add the <code>GetTenantLimit</code> method:</p><pre><code class="language-csharp">static int GetTenantLimit(string tenantId)
{
    return tenantId switch
    {
        "tenantA" =&gt; 10, // Premium tenant
        "tenantB" =&gt; 5,  // Standard tenant
        _ =&gt; 2           // Default / anonymous
    };
}</code></pre><p>We are defining the maximum request per minute for each client by extracting <code>tenantId</code> from the header of a request. <code>GetTenantLimit</code> is kept hard-coded to simplify our implementation. In real scenarios, you will use a Redis cache or database for that.</p><p><strong>Step 6: Add controller</strong></p><p>Our controller is as follows:</p><pre><code class="language-csharp">using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;

namespace Net10RateLimiter.Controllers;

[EnableRateLimiting("TenantPolicy")]
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    [HttpGet]
    public IEnumerable&lt;string&gt; Get()
        =&gt; ["Sunny", "Windy", "Breezy"];
}</code></pre><p>The attribute <code>[EnableRateLimiting("TenantPolicy")]</code> associates our tenant policy with the controller.</p><p><strong>Step 7: Enable the Middleware</strong></p><p>Insert rate limiter middleware:</p><pre><code class="language-csharp">var app = builder.Build();

app.UseRateLimiter();

app.MapControllers();

app.Run();
</code></pre><p>Make sure to add this just before controllers, so it blocks the request before hitting the controller if the rate limit exceeds.</p><p>Our final <code>Program.cs</code> looks like this:</p><pre><code class="language-csharp">using System.Threading.RateLimiting;
using Net10RateLimiter.Services;

static int GetTenantLimit(string tenantId)
{
    return tenantId switch
    {
        "tenantA" =&gt; 10, // Premium tenant
        "tenantB" =&gt; 5,  // Standard tenant
        _ =&gt; 2           // Default / anonymous
    };
}

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
builder.Services.AddControllers();
// Required for HttpContext access
builder.Services.AddHttpContextAccessor();

// Tenant resolver must be scoped
builder.Services.AddScoped&lt;TenantResolver&gt;();
builder.Services.AddRateLimiter(options =&gt;
{
    options.OnRejected = async (context, token) =&gt;
    {
        context.HttpContext.Response.StatusCode = 429;
        await context.HttpContext.Response.WriteAsync("Too Many Requests");
    };

    options.AddPolicy("TenantPolicy", context =&gt;
    {
        var tenantResolver =
            context.RequestServices.GetRequiredService&lt;TenantResolver&gt;();

        var tenantId = tenantResolver.ResolveTenant();
        var limit = GetTenantLimit(tenantId);

        return RateLimitPartition.GetTokenBucketLimiter(
            tenantId,
            _ =&gt; new TokenBucketRateLimiterOptions
            {
                TokenLimit = limit,
                TokensPerPeriod = limit,
                ReplenishmentPeriod = TimeSpan.FromMinutes(1),
                AutoReplenishment = true
            });
    });
});

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.UseHttpsRedirection();

app.UseRateLimiter();

app.MapControllers();
app.Run();</code></pre><p>And the file structure looks like this:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/01/image-2.png" class="kg-image" alt="File structure" loading="lazy" width="332" height="336"></figure><p><strong>Step 8: Run and test</strong></p><pre><code class="language-console">dotnet run</code></pre><p>Let us test this in Postman. Tenant A can call the endpoint:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/01/image-3.png" class="kg-image" alt="WeatherForecast endpoint from Tenant A" loading="lazy" width="942" height="507" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/01/image-3.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/01/image-3.png 942w" sizes="(min-width: 720px) 720px"></figure><p>So can Tenant B:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/01/image-4.png" class="kg-image" alt="WeatherForecast endpoint from Tenant B" loading="lazy" width="930" height="485" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/01/image-4.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/01/image-4.png 930w" sizes="(min-width: 720px) 720px"></figure><p>Once Tenant A reached the limit</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/01/image-1.png" class="kg-image" alt="Too Many Requests" loading="lazy" width="935" height="484" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/01/image-1.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/01/image-1.png 935w" sizes="(min-width: 720px) 720px"></figure><p>Once tenant B reached the limit, you will see the endpoint returning a status code of 429, meaning too many requests:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/01/image.png" class="kg-image" alt="Too Many Requests" loading="lazy" width="924" height="427" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/01/image.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/01/image.png 924w" sizes="(min-width: 720px) 720px"></figure><h2 id="what-has-net-10-improved-in-the-rate-limiter">What has .NET 10 improved in the rate limiter?</h2><p>.NET 10 has changed the game of rate limiter in many ways, such as:</p><ul><li>While .NET 8 introduced native rate-limiting, .NET 10 improved throttling middleware, eliminating verbose and third-party reliance. You can simply use <code>AddRateLimiter</code> and add <code>UseRateLimiter</code> to the pipeline now. </li><li>Earlier, developers had to implement counter expiration, sliding windows, and token buckets for handling race conditions and concurrency-safe code. The practice was tiring and risky to maintain. .NET 10 flattens these jobs.</li><li>Rate limiting was mostly IP-based with no native concept of tenants. If you had to design it for a SaaS multitenant application, you would have to do it manually. In .NET 10, partitioned rate limiters allow each tenant to have an isolated limiter, preventing noisy-neighbor issues by design.</li><li>Scalability of rate limiting demands Redis, distributed locks, and careful atomic operations with prior .NET versions. .NET 10 relieved the pain with its optimized high concurrency ability.</li><li>Custom logic and third-party usage in rate limiting caused extra overhead and performance penalty. .NET 10, with its allocation-efficient and optimized middleware, runs efficiently and fast.</li></ul><h2 id="conclusion">Conclusion</h2><p>.NET 10 was released in November 2025 and is supported for three years as a long-term support (LTS) release. It brings the latest version of C# with many refinements. In this blog post for our .NET 10 and C# 14 series,&nbsp;we discovered refinements in the rate limiter and throttling middleware. Built-in support and middleware native implementation have improved the maintainability and performance of rate limiters.</p><p>Code: <a href="https://github.com/elmahio-blog/Net10RateLimiter.git">https://github.com/elmahio-blog/Net10RateLimiter.git</a> </p><p></p><p></p> ]]></content:encoded>
    </item>
    <item>
        <title><![CDATA[ New in .NET 10 and C# 14: Fast Model Validation for APIs ]]></title>
        <description><![CDATA[ .NET 10 is officially out, along with C# 14. Microsoft has released .NET 10 as Long-Term Support (LTS) as a successor to .NET 8. Like every version, it is not just an update but brings something new to the table. In this series, we will explore which aspects of software ]]></description>
        <link>https://blog.elmah.io/new-in-net-10-and-c-14-fast-model-validation-for-apis/</link>
        <guid isPermaLink="false">694ecd35619fce000107f48c</guid>
        <category><![CDATA[  ]]></category>
        <dc:creator><![CDATA[ Ali Hamza Ansari ]]></dc:creator>
        <pubDate>Tue, 20 Jan 2026 06:59:09 +0100</pubDate>
        <media:content url="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/01/new-in-dotnet-10-and-csharp-14-fast-model-validation-for-apis-o-1.png" medium="image"/>
        <content:encoded><![CDATA[ <p>This blog post is originally published on <a href="https://blog.elmah.io/new-in-net-10-and-c-14-fast-model-validation-for-apis/">https://blog.elmah.io/new-in-net-10-and-c-14-fast-model-validation-for-apis/</a></p> 
<!--kg-card-begin: html-->
<div class="toc"></div>
<!--kg-card-end: html-->
<p>.NET 10 is officially out, along with C# 14. Microsoft has released .NET 10 as Long-Term Support (LTS) as a successor to .NET 8. Like every version, it is not just an update but brings something new to the table. <a href="https://blog.elmah.io/tag/whats-new-in-net-10/" rel="noreferrer">In this series</a>, we will explore which aspects of software can be upgraded with the latest release.</p><p>Model validation is a constant performance "tax" on every request, yet it remains non-negotiable for ensuring data integrity and a good user experience. While developers once faced a trade-off between strict validation and high throughput, .NET 10 eliminates this friction. In this post, I'll examine the benchmarks to see how these shifts dramatically reduce latency in .NET 10.</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/01/new-in-dotnet-10-and-csharp-14-fast-model-validation-for-apis-o.png" class="kg-image" alt="New in .NET 10 and C# 14: Fast Model Validation for APIs" loading="lazy" width="1500" height="750" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/01/new-in-dotnet-10-and-csharp-14-fast-model-validation-for-apis-o.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/01/new-in-dotnet-10-and-csharp-14-fast-model-validation-for-apis-o.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/01/new-in-dotnet-10-and-csharp-14-fast-model-validation-for-apis-o.png 1500w" sizes="(min-width: 1200px) 1200px"></figure><h2 id="apis-model-validation-benchmarking-with-net-8-and-net-10">APIs Model Validation Benchmarking with .NET 8 and .NET 10</h2><p>To start testing the improvements, I have created a solution named <code>ValidationBenchmarks</code> where three projects will reside. The structure looks like this:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/12/image-17.png" class="kg-image" alt="Folder structure" loading="lazy" width="272" height="107"></figure><h3 id="net-8-api-project">.NET 8 API project</h3><p>The project will contain an API with a single endpoint. Let's create it.</p><p><strong>Step 1: Create a .NET 8 project </strong></p><pre><code class="language-console">mkdir ApiNet8
cd ApiNet8

dotnet new webapi -n ApiNet8 --framework net8.0
</code></pre><p><strong>Step 2: Create DTO (validation target)</strong></p><pre><code class="language-csharp">using System.ComponentModel.DataAnnotations;

namespace ApiNet8.Models;

public sealed class CreateDeviceRequest
{
    [Required]
    public string DeviceId { get; set; } = default!;

    [Range(-40, 85)]
    public double Temperature { get; set; }

    [Range(0, 100)]
    public int BatteryLevel { get; set; }

    [Required]
    public DateTime Timestamp { get; set; }
}</code></pre><p>The model is a minimal representation of telemetry data where <code>DeviceId</code> and <code>Timestamp</code> are mandatory while <code>Temperature</code> and <code>BatteryLevel</code> are restricted to be in a valid range.</p><p><strong>Step 3: Create controller</strong></p><pre><code class="language-csharp">using ApiNet8.Models;
using Microsoft.AspNetCore.Mvc;

namespace ApiNet8.Controllers;

[ApiController]
[Route("devices")]
public class DevicesController : ControllerBase
{
    [HttpPost]
    public IActionResult Create(CreateDeviceRequest request)
    {
        return Ok(new { Message = "Device accepted (.NET 8)" });
    }
}</code></pre><p>The controller contains a single POST endpoint to create a device. The method takes our model as input.</p><p><strong>Step 4: Configure <code>Progam.cs</code></strong></p><pre><code class="language-csharp">var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

var app = builder.Build();

app.MapControllers();
app.Run();</code></pre><p>If you have create a MVC API before, you will see nothing different from the default template here.</p><h3 id="net-10-api-project">.NET 10 API project</h3><p>Let's create a similar project for .NET 10.</p><p><strong>Step 1: Create a .NET 10 project </strong></p><pre><code class="language-console">mkdir ApiNet10
cd ApiNet10

dotnet new webapi -n ApiNet10 --framework net10.0
</code></pre><p>Steps 2, 3, and 4 are exactly the same as from the .NET 8 steps.</p><h3 id="benchmark-project">Benchmark project </h3><p>Once we have set up both target projects, we will set up a benchmark to evaluate them.</p><p><strong>Step 1: Create the project</strong></p><p>Our benchmark project will be a console application using the <code>BenchmarkDotNet</code> package.</p><pre><code class="language-console">dotnet new console -n ApiBenchmarks
cd ApiBenchmarks</code></pre><p><strong>Step 2: Install the </strong><code>BenchmarkDotNet</code> <strong>package</strong></p><pre><code class="language-console">dotnet add package BenchmarkDotNet</code></pre><p>For more information about benchmarking, check out <a href="https://blog.elmah.io/how-to-monitor-your-apps-performance-with-net-benchmarking/" rel="noreferrer">How to Monitor Your App's Performance with .NET Benchmarking</a>.</p><p><strong>Step 3: Add Benchmarking code</strong></p><pre><code class="language-csharp">using System.Text;
using BenchmarkDotNet.Attributes;

namespace ApiBenchmarks;
[MemoryDiagnoser]
public class ValidationBenchmarks
{
    private HttpClient _client8 = default!;
    private HttpClient _client10 = default!;
    private StringContent _invalidPayload = default!;

    [GlobalSetup]
    public void Setup()
    {
        _client8 = new HttpClient
        {
            BaseAddress = new Uri("http://localhost:5008")
        };

        _client10 = new HttpClient
        {
            BaseAddress = new Uri("http://localhost:5010")
        };

        var json = """
                   {
                       "deviceId": "",
                       "temperature": 120,
                       "batteryLevel": 150,
                       "timestamp": null
                   }
                   """;

        _invalidPayload = new StringContent(
            json,
            Encoding.UTF8,
            "application/json"
        );
    }

    [Benchmark]
    public async Task Net8_Invalid_Model()
        =&gt; await _client8.PostAsync("/devices", _invalidPayload);

    [Benchmark]
    public async Task Net10_Invalid_Model()
        =&gt; await _client10.PostAsync("/devices", _invalidPayload);
}</code></pre><p>Here, I initialized two clients, <code>_client8</code> for calling the API of the .NET 8 project and <code>_client10</code> to call the endpoint of the .NET 10 project. To represent an invalid payload, I created an <code>_invalidPayload</code> field. To test the worst case, I violated all the validations.</p><p><strong>Step 4: Call the benchmark in <code>Program.cs</code></strong></p><pre><code class="language-csharp">using ApiBenchmarks;
using BenchmarkDotNet.Running;

BenchmarkRunner.Run&lt;ValidationBenchmarks&gt;();</code></pre><p><strong>Step 5: Run the projects</strong></p><p>Open 3 terminals, on the first one run</p><pre><code class="language-console">cd ApiNet8
dotnet run --urls "http://localhost:5100"</code></pre><p>Run the following on the Second one</p><pre><code class="language-console">cd ApiNet10
dotnet run --urls "http://localhost:5200"
</code></pre><p>While on the third one</p><pre><code class="language-console">cd ApiBenchmarks
dotnet run -c Release
</code></pre><p>So each of the API will be running on the designated port.</p><p><strong>Result</strong></p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/12/image-16.png" class="kg-image" alt="Results" loading="lazy" width="788" height="137" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2025/12/image-16.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/12/image-16.png 788w" sizes="(min-width: 720px) 720px"></figure><p>Let's break down how .NET performed about 3x faster in several ways.</p><ul><li>Earlier model validation relied heavily on reflection, such as discovering attributes like <code>[Required]</code>, <code>[Range]</code>, and <code>[StringLength]</code>. Applying reflection on properties adds to the time. .NET 10 generates validation metadata at build time and replaces reflection with precomputed accessors. This actually saves a runtime check that happened earlier due to reflection.</li><li>.NET 10 uses <code>Span&lt;T&gt;</code> where possible, and avoids LINQ in hot paths.</li><li>Attributes executed as object-oriented (OO) components, relying on virtual method calls and object-based values in prior versions. Classes provide a flexible solution, but they were expensive to the CPU. .NET 10 precomputes validation metadata and replaces virtual, object-based execution with direct, typed method calls. That means the .NET 10 keeps validation attributes as an OO API but replaces their runtime execution with precomputed, direct calls that are cheaper for the CPU.</li><li>During attribute invocation, <a href="https://blog.elmah.io/hidden-costs-of-boxing-in-c-how-to-detect-and-avoid-them/" rel="noreferrer">expensive boxed values</a> were used along with context objects. .NET 10 eliminated boxing for value types and introduced a shared validation context where safe.</li><li>.NET 10 replaced repeated metadata traversal of evaluating each attribute and building <code>ModelState</code> entries with fewer temporary strings, lazy creation of error messages, and smarter reuse of <code>ModelError</code> objects. This ensured low GC pressure and better stability of latency. The provision helps in APIs where validation errors often occur, such as auth endpoints, ingestion APIs, and public APIs.</li><li>.NET 10 has better p95 / p99 latency, less jitter, more predictable execution, fewer unpredictable branches, and less runtime metadata walking. The results are visible in StdDev, which is 3x less than its counterpart.</li></ul><h2 id="conclusion">Conclusion</h2><p>.NET 10 is released in November 2025 and is supported for three years as a long-term support (LTS) release. It brings the latest version of C# with many refinements. In the blog post for our .NET 10 and C# 14 series, I highlighted improvements to the model validations. .NET 10 has changed the nature of the validation process from reflection to precomputed, faster calls. We saw a dramatic cut down of 3x in parameters like StdDev and Error due to better p99 latency and less jittering in the latest version.</p><p>Code: <a href="https://github.com/elmahio-blog/ValidationBenchmarks">https://github.com/elmahio-blog/ValidationBenchmarks</a></p> ]]></content:encoded>
    </item>
    <item>
        <title><![CDATA[ New in .NET 10 and C# 14: Enhancements in APIs Request/Response Pipeline ]]></title>
        <description><![CDATA[ .NET 10 is officially out, along with C# 14. Microsoft has released .NET 10 as Long-Term Support (LTS) as a successor to .NET 8. Like every version, it is not just an update but brings something new to the table. In this series, we will explore which aspects of software ]]></description>
        <link>https://blog.elmah.io/new-in-net-10-and-c-14-enhancements-in-apis-request-response-pipeline/</link>
        <guid isPermaLink="false">6935cb20c169b400012c8f6a</guid>
        <category><![CDATA[  ]]></category>
        <dc:creator><![CDATA[ Ali Hamza Ansari ]]></dc:creator>
        <pubDate>Wed, 14 Jan 2026 09:53:37 +0100</pubDate>
        <media:content url="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/12/new-in-dotnet-10-and-csharp-14-enhancements-in-apis-request-response-pipeline-o.png" medium="image"/>
        <content:encoded><![CDATA[ <p>This blog post is originally published on <a href="https://blog.elmah.io/new-in-net-10-and-c-14-enhancements-in-apis-request-response-pipeline/">https://blog.elmah.io/new-in-net-10-and-c-14-enhancements-in-apis-request-response-pipeline/</a></p> 
<!--kg-card-begin: html-->
<div class="toc"></div>
<!--kg-card-end: html-->
<p>.NET 10 is officially out, along with C# 14. Microsoft has released .NET 10 as Long-Term Support (LTS) as a successor to .NET 8. Like every version, it is not just an update but brings something new to the table. <a href="https://blog.elmah.io/tag/whats-new-in-net-10/" rel="noreferrer">In this series</a>, we will explore which aspects of software can be upgraded with the latest release. Today, in the series of What is new in .NET 10 and C#14, we will evaluate on-ground changes in the APIs using minimal examples.</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/12/new-in-dotnet-10-and-csharp-14-enhancements-in-apis-request-response-pipeline-o-1.png" class="kg-image" alt="New in .NET 10 and C# 14: Enhancements in APIs Request/Response Pipeline" loading="lazy" width="1500" height="750" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2025/12/new-in-dotnet-10-and-csharp-14-enhancements-in-apis-request-response-pipeline-o-1.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2025/12/new-in-dotnet-10-and-csharp-14-enhancements-in-apis-request-response-pipeline-o-1.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/12/new-in-dotnet-10-and-csharp-14-enhancements-in-apis-request-response-pipeline-o-1.png 1500w" sizes="(min-width: 1200px) 1200px"></figure><p>APIs don't need any introduction. From loading the cart to your mobile to showing this blog on your computer, everything is fetched by APIs. In today's dynamic websites, a simple action requires multiple API calls. So, a simple lag can hurt users' experience, no matter what application you build. .NET 10 brings some thoughtful changes for APIs. Some major improvements, like JIT inlining and lower GC burden, made things faster across operations. However, there are enhancements in API pipelining as well. Let's observe how API operations are improved in .NET 10.</p><h2 id="benchmarking-minimal-apis-with-net-8-and-net-10">Benchmarking Minimal APIs with .NET 8 and .NET 10</h2><p>I have created a directory <code>MinimalApiComparison</code> where three projects will reside. Consider the structure:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/12/image-10.png" class="kg-image" alt="API structure" loading="lazy" width="284" height="124"></figure><p>One folder per API version and another for the benchmarks.</p><h3 id="net-8-minimal-api-project">.NET 8 minimal API project</h3><p>Let's start by creating a minimal API on .NET 8.</p><p><strong>Step 1: Create a .NET 8 project </strong></p><pre><code class="language-console">mkdir ApiNet8
cd ApiNet8

dotnet new webapi -n ApiNet8 --framework net8.0
</code></pre><p><strong>Step 2: Set up the code</strong></p><pre><code class="language-csharp">var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/weather", () =&gt;
{
    return new WeatherForecast(DateTime.Now, 25, "Sunny");
});

app.Run();

public record WeatherForecast(DateTime Date, int TemperatureC, string Summary);
</code></pre><p>Just a single endpoint named <code>/weather</code>.</p><h3 id="net-10-minimal-api-project">.NET 10 Minimal API project</h3><p>Now, let's set up a similar project for .NET 10.</p><p><strong>Step 1: Create a .NET 10 project</strong></p><pre><code class="language-console">mkdir ApiNet10
cd ApiNet10

dotnet new webapi -n ApiNet10 --framework net10.0
</code></pre><p><strong>Step 2: Set up the code</strong></p><pre><code class="language-csharp">var builder = WebApplication.CreateBuilder(args);

builder.Services.ConfigureHttpJsonOptions(o =&gt;
{
    o.SerializerOptions.AllowOutOfOrderMetadataProperties = true;
});

var app = builder.Build();

app.MapGet("/weather", static () =&gt;
    {
        return new WeatherForecast(DateTime.Now, 25, "Sunny");
    })
    .WithName("GetWeather")
    .WithSummary("Faster pipeline in .NET 10");

app.Run();

public record WeatherForecast(DateTime Date, int TemperatureC, string Summary);</code></pre><p>The property <code>AllowOutOfOrderMetadataProperties</code> allows skipping strict ordering checks in JSON deserialization. It is an optimisation step that reduces parsing branches in compilation and speeds up model binding. <code>static () =&gt;</code> Prevents closure allocation by restricting the lambda from capturing variables. In preceding versions, lambda saves the inner variable in <a href="https://blog.elmah.io/how-net-garbage-collector-works-and-when-you-should-care/" rel="noreferrer">Gen 0 memory</a>. While in the latest updates, static lambda results in 0 allocation and a faster, memory-efficient API.</p><h3 id="benchmark-project">Benchmark project </h3><p>Once we set up both target projects, let us move to the benchmark to evaluate them.</p><p><strong>Step 1: Create the project</strong></p><p>Our benchmark project will be a console application.</p><pre><code class="language-console">dotnet new console -n ApiBenchmarks
cd ApiBenchmarks</code></pre><p><strong>Step 2: Install the <code>BenchmarkDotNet</code> package</strong></p><p>We are using `BenchmarkDotNet  to observe the results. For more information about this package, check out <a href="https://blog.elmah.io/how-to-monitor-your-apps-performance-with-net-benchmarking/" rel="noreferrer">How to Monitor Your App's Performance with .NET Benchmarking</a>.</p><pre><code class="language-console">dotnet add package BenchmarkDotNet</code></pre><p><strong>Step 3: Set up the URLs benchmarking code</strong></p><pre><code class="language-csharp">using System.Net.Http.Json;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Order;

[MemoryDiagnoser]
[SimpleJob(warmupCount: 3, iterationCount: 10)]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
public class ApiComparisonBenchmarks
{
    private readonly HttpClient _client8;
    private readonly HttpClient _client10;

    public ApiComparisonBenchmarks()
    {
        _client8 = new HttpClient { BaseAddress = new Uri("http://localhost:5100") };
        _client10 = new HttpClient { BaseAddress = new Uri("http://localhost:5200") };
    }

    [Benchmark]
    public async Task WeatherNet8()
    {
        var result = await _client8.GetFromJsonAsync&lt;WeatherForecast&gt;("/weather");
    }

    [Benchmark]
    public async Task WeatherNet10()
    {
        var result = await _client10.GetFromJsonAsync&lt;WeatherForecast&gt;("/weather");
    }
}

public record WeatherForecast(DateTime Date, int TemperatureC, string Summary);</code></pre><p>I created two methods <code>WeatherNet8</code> to hit the .NET 8 minimal API and <code>WeatherNet10</code> for the .NET 10 version. I have specified server URLs with each client explicitly. The <code>[Benchmark]</code> decorator will do the rest of the job.</p><p><strong>Step 4: Run the projects</strong></p><p>Open 3 terminals, on the first one run:</p><pre><code class="language-console">cd ApiNet8
dotnet run --urls "http://localhost:5100"</code></pre><p>Run the following on the second one:</p><pre><code class="language-console">cd ApiNet10
dotnet run --urls "http://localhost:5200"
</code></pre><p>While on the third one:</p><pre><code class="language-console">cd ApiBenchmarks
dotnet run -c Release
</code></pre><p>So each of the API will be running on the designated port. ApiBenchmark hits its endpoint at <code>/weather</code> and performs benchmarking. </p><p><strong>Output</strong></p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/12/image-9.png" class="kg-image" alt="Output" loading="lazy" width="760" height="152" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2025/12/image-9.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/12/image-9.png 760w" sizes="(min-width: 720px) 720px"></figure><p>We can observe a difference in the same api of both projects. .NET 10 did the job quite fast, and other parameters are also lower, indicating the performance improvements in the latest version. .NET 10 has 3x smaller Standard deviation and error due to less jitter in execution times by the grace of JIT improvements and JSON parsing novelty. Pipeline configurations like <code>AllowOutOfOrderMetadataProperties</code> and  <code>static () =&gt;</code> reduced the per-request overhead. <code>AllowOutOfOrderMetadataProperties</code> lowers CPU burden and reduces <code>System.Text.Json</code> paths in compilation. And static lambda not only eliminated closure allocation but made the delegate work faster with lower <a href="https://blog.elmah.io/how-net-garbage-collector-works-and-when-you-should-care/" rel="noreferrer">Garbage Collector pressure</a>.</p><p>Our big win was not only in the pipeline configuration but also in faster request-response improvements in ASP.NET Core. .NET 10 has optimized middleware dispatch overhead, added static pipeline analysis and faster endpoint selection, resulting in lower mean and tail latency. Our observation was just on a minimal API with small objects. These enhancements will bring noticeable results when you work on real projects with larger API operations.</p><h2 id="conclusion">Conclusion </h2><p>.NET 10 is released in November 2025 and is supported for three years as a long-term support (LTS) release. For the APIs pipeline, .NET 10 brought key enhancements. In this post, I showed these enhancements with on-ground benchmark analysis. Better JIT inlining, lower CPU and GC pressure, and numerous other factors have contributed to better APIs.</p><p>Source code: <a href="https://github.com/elmahio-blog/MinimalApiComparison-.git">https://github.com/elmahio-blog/MinimalApiComparison-.git</a></p> ]]></content:encoded>
    </item>
    <item>
        <title><![CDATA[ New in .NET 10 and C# 14: EF Core 10&#x27;s Faster Production Queries ]]></title>
        <description><![CDATA[ .NET 10 is officially out, along with C# 14. Microsoft has released .NET 10 as Long-Term Support (LTS) as a successor to .NET 8. Like every version, it is not just an update but brings something new to the table. In this series, we will explore which aspects of software ]]></description>
        <link>https://blog.elmah.io/new-in-net-10-and-c-14-ef-core-10s-faster-production-queries/</link>
        <guid isPermaLink="false">69482b14619fce000107f477</guid>
        <category><![CDATA[  ]]></category>
        <dc:creator><![CDATA[ Ali Hamza Ansari ]]></dc:creator>
        <pubDate>Tue, 06 Jan 2026 09:06:26 +0100</pubDate>
        <media:content url="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/12/new-in-dotnet-10-and-csharp-14-ef-core-10-s-faster-production-queries-o.png" medium="image"/>
        <content:encoded><![CDATA[ <p>This blog post is originally published on <a href="https://blog.elmah.io/new-in-net-10-and-c-14-ef-core-10s-faster-production-queries/">https://blog.elmah.io/new-in-net-10-and-c-14-ef-core-10s-faster-production-queries/</a></p> 
<!--kg-card-begin: html-->
<div class="toc"></div>
<!--kg-card-end: html-->
<p>.NET 10 is officially out, along with C# 14. Microsoft has released .NET 10 as Long-Term Support (LTS) as a successor to .NET 8. Like every version, it is not just an update but brings something new to the table. In this <a href="https://blog.elmah.io/tag/whats-new-in-net-10/" rel="noreferrer">series</a>, we will explore which aspects of software can be upgraded with the latest release. Today, I will evaluate the improvements in EF Core's data fetching that .NET 10 brought. </p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/12/new-in-dotnet-10-and-csharp-14-ef-core-10-s-faster-production-queries-o-1.png" class="kg-image" alt="New in .NET 10 and C# 14: EF Core 10's Faster Production Queries" loading="lazy" width="1500" height="750" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2025/12/new-in-dotnet-10-and-csharp-14-ef-core-10-s-faster-production-queries-o-1.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2025/12/new-in-dotnet-10-and-csharp-14-ef-core-10-s-faster-production-queries-o-1.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/12/new-in-dotnet-10-and-csharp-14-ef-core-10-s-faster-production-queries-o-1.png 1500w" sizes="(min-width: 1200px) 1200px"></figure><p>If you are a .NET developer, you will most likely choose EF Core for database operations in your application. You are not alone, actually EF Core is a leading ORM for .NET applications due to its handy snippets and extensive features. Any data fetching operation looks easy with the ORM. However, it is not that simple under the hood. Lots of work goes into fetching the data and posting it to your application. .NET 10 made several improvements to support the process and enhance EF Core's part of the task. From JIT's better inlining to cleaner row materialization, we will calculate with benchmark the older version of the framework and the latest one.</p><h2 id="ef-core-data-fetching-in-net-8-and-net-10">EF Core Data fetching in .NET 8 and .NET 10</h2><p>To benchmark the fetching performance, I will use a .NET 8 API and a .NET 10 API, each with the same PostgreSQL database below. To make the data size considerable, I have inserted 200000 records. Let's go through the steps for setting up the database.</p><p><strong>Step 1: Create a PostgreSQL database</strong></p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/12/image-12.png" class="kg-image" alt="PostgreSQL" loading="lazy" width="247" height="312"></figure><p>I have named the database <code>efbench_db</code>.</p><p><strong>Step 2: Create the Users table and insert data</strong></p><pre><code class="language-sql">CREATE TABLE "Users" (
    "Id" SERIAL PRIMARY KEY,
    "Name" TEXT NOT NULL,
    "OrganizationId" INT NOT NULL
);

INSERT INTO "Users" ("Name", "OrganizationId")
SELECT 
    'User ' || g,
    (g % 10) + 1
FROM generate_series(1, 200_000) g;
</code></pre><p>The new table can be inspected using a bit of SQL.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/12/image-13.png" class="kg-image" alt="Select count from users" loading="lazy" width="364" height="289"></figure><p>For the C# code, I will create a project structure looking like this:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/12/image-14.png" class="kg-image" alt="Project structure" loading="lazy" width="287" height="100"></figure><h3 id="ef-core-data-access-with-net-8-and-c12">EF Core data access with .NET 8 and C#12</h3><p>Let us start by creating the project for querying user data from .NET 8.</p><p><strong>Step 1: Create a Web Api project in .NET 8</strong></p><pre><code class="language-console">dotnet new webapi -n ApiNet8 -f net8.0
</code></pre><p><strong>Step 2: Install the necessary NuGet packages</strong></p><pre><code class="language-console">dotnet add package Microsoft.EntityFrameworkCore --version 8.*
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL --version 8.*
</code></pre><p>Here we specified package versions that will install the latest compatible version with .NET 8.</p><p><strong>Step 3: Define the model</strong></p><pre><code class="language-csharp">namespace ApiNet8.Models;

public class User
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public int OrganizationId { get; set; }
}
</code></pre><p><strong>Step 4: Configure AppDbContext</strong></p><pre><code class="language-csharp">using ApiNet8.Models;
using Microsoft.EntityFrameworkCore;

namespace ApiNet8.Data;

public class AppDbContext: DbContext
{
    public DbSet&lt;User&gt; Users =&gt; Set&lt;User&gt;();

    public AppDbContext(DbContextOptions&lt;AppDbContext&gt; options)
        : base(options) { }
}</code></pre><p><strong>Step 5: Add the connection string to appsettings.json</strong></p><pre><code class="language-json">{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "ConnectionStrings": {
    "DefaultConnection": "Host=localhost;Port=5432;Database=efbench_db;Username=postgres;Password=pass"
  },
  "AllowedHosts": "*"
}
</code></pre><p><strong>Step 6: Set up Program.cs</strong></p><p>I have injected the connection string and <code>AppDbContext</code> in <code>Program.cs</code>. So our final file looks like this:</p><pre><code class="language-csharp">using ApiNet8.Data;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");

builder.Services.AddDbContext&lt;AppDbContext&gt;(options =&gt;
{
    options.UseNpgsql(connectionString);
});

var app = builder.Build();

app.MapGet("/users/{orgId:int}", async (int orgId, AppDbContext db) =&gt;
{
    return await db.Users
        .AsNoTracking()
        .Where(u =&gt; u.OrganizationId == orgId)
        .Select(u =&gt; new { u.Id, u.Name })
        .ToListAsync();
});

app.Run();</code></pre><p>One important thing that the <code>Program.cs</code> contains a minimal API to fetch users filtered by <code>OrganizationId</code> <a href="https://blog.elmah.io/building-read-models-with-ef-core-projections/" rel="noreferrer">using a projection</a>.</p><h3 id="ef-core-data-access-with-net-10-and-c14">EF Core data access with .NET 10 and C#14</h3><p>It is time to introduce the same code but for .NET 10.</p><p><strong>Step 1: Create a .NET 10 API project</strong></p><pre><code class="language-console">dotnet new webapi -n ApiNet10 -f net10.0</code></pre><p><strong>Step 2: Add the required packages</strong></p><pre><code class="language-console">dotnet add package Microsoft.EntityFrameworkCore --version 10.*
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL --version 10.*</code></pre><p>Steps 3, 4, and 5 are the same as the preceding project.</p><p><strong>Step 6: Add configuration to Program.cs</strong></p><pre><code class="language-csharp">using ApiNet10.Data;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// EF Core 10 pipeline optimizations
builder.Services.ConfigureHttpJsonOptions(o =&gt;
{
    o.SerializerOptions.AllowOutOfOrderMetadataProperties = true;
});

// Read connection string from appsettings
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");

// Register DbContext
builder.Services.AddDbContext&lt;AppDbContext&gt;(options =&gt;
{
    options.UseNpgsql(connectionString);
});

var app = builder.Build();

app.MapGet("/users/{orgId:int}",
    static async (int orgId, AppDbContext db) =&gt;
        await db.Users
            .AsNoTracking()
            .Where(u =&gt; u.OrganizationId == orgId)
            .Select(u =&gt; new { u.Id, u.Name })
            .ToListAsync()
);

app.Run();</code></pre><p>A few noticeable differences here are allowing out-of-order metadata and static keyword in minimal API fetching, which allows skipping strict ordering checks in JSON deserialization. It is an optimisation step that reduces parsing branches in compilation and speeds up model binding. <code>static () =&gt;</code> Prevents closure allocation by restricting the lambda from capturing variables. In preceding versions, lambda saves the inner variable in <a href="https://blog.elmah.io/how-net-garbage-collector-works-and-when-you-should-care/" rel="noreferrer">Gen 0 memory</a>. While in the latest updates, static lambda results in 0 allocation and a faster, memory-efficient API.</p><h3 id="api-benchmark-project">API Benchmark project</h3><p>Finally, we can add our referee, the API benchmark project. </p><p><strong>Step 1: Add a Console project to the solution</strong></p><pre><code class="language-console">dotnet new console -n ApiBenchmarks
</code></pre><p><strong>Step 2: Install the required NuGet packages</strong></p><pre><code class="language-console">dotnet add package BenchmarkDotNet
dotnet add package Microsoft.Extensions.Http
</code></pre><p><strong>Step 3:  Add Benchmarking code</strong></p><p>I added the <code>ApiBenchmarks</code> class.</p><pre><code class="language-csharp">using BenchmarkDotNet.Attributes;
using System.Net.Http.Json;

namespace DefaultNamespace;

[MemoryDiagnoser]
public class ApiBenchmarks
{
    private readonly HttpClient _net8 =
        new() { BaseAddress = new Uri("http://localhost:5267") };

    private readonly HttpClient _net10 =
        new() { BaseAddress = new Uri("http://localhost:5186") };

    [Benchmark]
    public async Task Net8_EFCore8()
    {
        await _net8.GetFromJsonAsync&lt;object[]&gt;("/users/5");
    }

    [Benchmark]
    public async Task Net10_EFCore10()
    {
        await _net10.GetFromJsonAsync&lt;object[]&gt;("/users/5");
    }
}
</code></pre><p>Dedicated HTTP clients will call the endpoint of the respective project. While the <code>[Benchmark]</code> attribute evaluates performance on each method.</p><p><strong>Step 4: Set up Program.cs</strong></p><pre><code class="language-csharp">using BenchmarkDotNet.Running;

BenchmarkRunner.Run&lt;ApiBenchmarks&gt;();</code></pre><p>Here, I call the <code>BenchmarkRunner.Run</code> method to run the tests.</p><p><strong>Step 5: Run the projects</strong></p><p>Open 3 terminals, on the first one run</p><pre><code class="language-console">cd ApiNet8
dotnet run --project Api.Net8
</code></pre><p>On the second</p><pre><code class="language-console">cd ApiNet10
dotnet run --project Api.Net10</code></pre><p>On the final terminal</p><pre><code class="language-console">cd ApiBenchmarks
dotnet run -c Release</code></pre><p><strong>Output</strong></p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/12/image-11.png" class="kg-image" alt="Output" loading="lazy" width="899" height="122" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2025/12/image-11.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/12/image-11.png 899w" sizes="(min-width: 720px) 720px"></figure><p>.NET 10 performance is 25 to 50% better in the benchmark metrics. Although query and EF Core are the same, the impact is due to .NET 10's execution speedup. Some vital reasons behind this improvement are</p><ul><li>JIT inlines methods and optimizes escape analysis better. .NET 10 performs fast dictionary and span operations. They flattened the pipeline, eliminating unpredictable branches in hot paths. Hence EF Core does not dangle in these complexities. </li><li>.NET 10 brought faster <code>ExpressionVisitor</code>and caching of traversal results. Previously, EF Core had to walk expression trees multiple times and allocate helper objects. Now your code has a single-pass expression analysis and fewer allocations.</li><li>One big blow is in Row Materialization that involves EF Cores data reading from the source to mapping in the .NET object. EF Core goes through multiple abstraction layers, reads columns defensively to handle null values and conversion checks, traverses additional branches for entity tracking, and generic materializers. .NET 10's inlining straightened the materialization flow, applied stronger devirtualization, upgraded nullable checks so EF Core can skip redundant null checks safely. Faster generic specialization made value handling cheaper. </li><li>JIT optimization of the hot path helped EF Core run without regression, saving decisive time. </li></ul><h2 id="conclusion">Conclusion</h2><p>.NET 10 is released in November 2025 and is supported for three years as a long-term support (LTS) release. It brings the latest version of C# with many refinements. In the blog post for our .NET 10 and C# 14 series, I visualized with benchmarked the impact of .NET 10 improvements on EF Core data fetching. .NET 10's game-changer enhancements are its JIT inlining optimizations and request pipeline streamlining that massively impacted every feature. Same worked with EF Core's data fetching, along with other icebreakers that I discussed. </p><p>Code: <a href="https://github.com/elmahio-blog/EfCoreBenchmarkDemo.git">https://github.com/elmahio-blog/EfCoreBenchmarkDemo.git</a></p> ]]></content:encoded>
    </item>
    <item>
        <title><![CDATA[ New in .NET 10 and C# 14: Optimizations in log aggregation jobs ]]></title>
        <description><![CDATA[ .NET 10 is officially out, along with C# 14. Microsoft has released .NET 10 as Long-Term Support (LTS) as a successor to .NET 8. Like every version, it is not just an update but brings something new to the table. In this series, we will explore which aspects of software ]]></description>
        <link>https://blog.elmah.io/new-in-net-10-and-c-14-optimizations-in-log-aggregation-jobs/</link>
        <guid isPermaLink="false">693199c93ca5410001ec62c1</guid>
        <category><![CDATA[  ]]></category>
        <dc:creator><![CDATA[ Ali Hamza Ansari ]]></dc:creator>
        <pubDate>Tue, 16 Dec 2025 08:55:28 +0100</pubDate>
        <media:content url="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/12/new-in-dotnet-10-and-csharp-14-optimizations-in-log-aggregation-jobs-o.png" medium="image"/>
        <content:encoded><![CDATA[ <p>This blog post is originally published on <a href="https://blog.elmah.io/new-in-net-10-and-c-14-optimizations-in-log-aggregation-jobs/">https://blog.elmah.io/new-in-net-10-and-c-14-optimizations-in-log-aggregation-jobs/</a></p> 
<!--kg-card-begin: html-->
<div class="toc"></div>
<!--kg-card-end: html-->
<p>.NET 10 is officially out, along with C# 14. Microsoft has released .NET 10 as Long-Term Support (LTS) as a successor to .NET 8. Like every version, it is not just an update but brings something new to the table. In this <a href="https://blog.elmah.io/tag/whats-new-in-net-10/" rel="noreferrer">series</a>, we will explore which aspects of software can be upgraded with the latest release. Today, in the series of What is new in .NET 10 and C#14, we will see optimization in log aggregation jobs.</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/12/new-in-dotnet-10-and-csharp-14-optimizations-in-log-aggregation-jobs-o-1.png" class="kg-image" alt="New in .NET 10 and C# 14: Optimizations in log aggregation jobs" loading="lazy" width="1500" height="750" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2025/12/new-in-dotnet-10-and-csharp-14-optimizations-in-log-aggregation-jobs-o-1.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2025/12/new-in-dotnet-10-and-csharp-14-optimizations-in-log-aggregation-jobs-o-1.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/12/new-in-dotnet-10-and-csharp-14-optimizations-in-log-aggregation-jobs-o-1.png 1500w" sizes="(min-width: 1200px) 1200px"></figure><p>Logging is a pathway to find anomalies and debug issues in any application. The application keeps generating logs for operations such as receiving device data, calling an API, checking background services, and more. Processing of logs requires high throughput because some applications demand the generation of multiple logs for a single operation. That job adds up once you do aggregation, including normalizing, parsing to the desired format, and storing them. Notification services often analyze stored logs to generate notifications and alerts based on the criticality of the logs. So, log aggregation is a tedious job for the server and a silent soldier of the application. Mindful of these, .NET brings several refinements in its JSON and string operations. Besides memory allocation, changes will help you in log generation. I will shed light on how much the said task has been improved.</p><h2 id="log-aggregation-job-with-net-8-and-c12">Log Aggregation job with .NET 8 and C#12</h2><p>To observe a significant improvement in .NET 10, let's first create log aggregation with the prior version, .NET 8.</p><p><strong>Step 1: Create a project</strong></p><p>Run the command to create a log aggregation project</p><pre><code class="language-console">mkdir LogBenchmark.Net8
cd LogBenchmark.Net8</code></pre><p><strong>Step 2: Install Benchmark and Newtonsoft NuGet packages</strong></p><pre><code class="language-console">dotnet add package BenchmarkDotNet
dotnet add package Newtonsoft.Json</code></pre><p>We will use <a href="https://blog.elmah.io/how-to-monitor-your-apps-performance-with-net-benchmarking/" rel="noreferrer">BenchmarkDotNet</a> to measure the time taken for each method.</p><p><strong>Step 3: Write Logging jobs with benchmark</strong></p><pre><code class="language-csharp">using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using Newtonsoft.Json.Linq;
using System.Text.Json;

BenchmarkRunner.Run&lt;LogBenchmark&gt;();

[MemoryDiagnoser]
public class LogBenchmark
{
    private readonly byte[] _utf8LogBytes;
    private readonly string _logString;

    public LogBenchmark()
    {
        _logString = """
        {
            "level": "info",
            "deviceId": "A1B2C3D4E5F60788",
            "timestamp": "2025-12-01T10:22:00Z",
            "message": "Temperature reading received",
            "value": 22.5
        }
        """;

        _utf8LogBytes = System.Text.Encoding.UTF8.GetBytes(_logString);
    }

    // ------------------------------------------------------------
    // 1. Baseline: Newtonsoft.Json 
    // ------------------------------------------------------------
    [Benchmark(Baseline = true)]
    public string Newtonsoft_Parse()
    {
        var obj = JObject.Parse(_logString);
        return (string)obj["deviceId"]!;
    }

    // ------------------------------------------------------------
    // 2. System.Text.Json in UTF-16 
    // ------------------------------------------------------------
    [Benchmark]
    public string SystemTextJson_Parse()
    {
        using var doc = JsonDocument.Parse(_logString);
        return doc.RootElement.GetProperty("deviceId").GetString()!;
    }

    // ------------------------------------------------------------
    // 3. UTF8JsonReader + Span&lt;byte&gt; 
    // ------------------------------------------------------------
    [Benchmark]
    public string? Utf8JsonReader_Parse()
    {
        var reader = new Utf8JsonReader(_utf8LogBytes);

        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.PropertyName &amp;&amp;
                reader.ValueTextEquals("deviceId"))
            {
                reader.Read();
                return reader.GetString();
            }
        }

        return null;
    }
}</code></pre><p>I defined 3 methods for logging using NewtonSoft and <code>System.Text</code> and <code>Utf8Json </code>returning deviceId. Each of them is decorated with the Benchmark attribute. To test, I defined a log message in the constructor. </p><p><strong>Step 4: Run and test</strong></p><p><a href="https://blog.elmah.io/how-to-monitor-your-apps-performance-with-net-benchmarking/" rel="noreferrer">As we know</a>, we have to run a release in benchmark projects.</p><pre><code class="language-console">dotnet run -c Release</code></pre><table>
<thead>
<tr>
<th style="text-align:left">Method</th>
<th style="text-align:right">Mean</th>
<th style="text-align:right">Error</th>
<th style="text-align:right">StdDev</th>
<th style="text-align:right">Median</th>
<th style="text-align:right">Ratio</th>
<th style="text-align:right">RatioSD</th>
<th style="text-align:right">Gen0</th>
<th style="text-align:right">Allocated</th>
<th style="text-align:right">Alloc Ratio</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left">Newtonsoft_Parse</td>
<td style="text-align:right">3,735.4 ns</td>
<td style="text-align:right">187.27 ns</td>
<td style="text-align:right">531.25 ns</td>
<td style="text-align:right">3,562.1 ns</td>
<td style="text-align:right">1.02</td>
<td style="text-align:right">0.20</td>
<td style="text-align:right">3.0060</td>
<td style="text-align:right">4728 B</td>
<td style="text-align:right">1.000</td>
</tr>
<tr>
<td style="text-align:left">SystemTextJson_Parse</td>
<td style="text-align:right">952.0 ns</td>
<td style="text-align:right">30.03 ns</td>
<td style="text-align:right">86.65 ns</td>
<td style="text-align:right">917.8 ns</td>
<td style="text-align:right">0.26</td>
<td style="text-align:right">0.04</td>
<td style="text-align:right">0.0706</td>
<td style="text-align:right">112 B</td>
<td style="text-align:right">0.024</td>
</tr>
<tr>
<td style="text-align:left">Utf8JsonReader_Parse</td>
<td style="text-align:right">233.3 ns</td>
<td style="text-align:right">4.86 ns</td>
<td style="text-align:right">9.92 ns</td>
<td style="text-align:right">234.2 ns</td>
<td style="text-align:right">0.06</td>
<td style="text-align:right">0.01</td>
<td style="text-align:right">0.0248</td>
<td style="text-align:right">40 B</td>
<td style="text-align:right">0.008</td>
</tr>
</tbody>
</table>
<h2 id="log-aggregation-job-with-net-10-and-c14">Log Aggregation job with .NET 10 and C#14</h2><p>Now jump to our latest tool, the .NET 10. I will create the same project</p><p><strong>Step 1: Create a project</strong></p><pre><code class="language-console">mkdir LogBenchmark.Net10
cd LogBenchmark.Net10</code></pre><p><strong>Step 2: Install Benchmark and Newtonsoft NuGet packages</strong></p><pre><code class="language-console">dotnet add package BenchmarkDotNet
dotnet add package Newtonsoft.Json</code></pre><p><strong>Step 3: Write Logging jobs with benchmark</strong></p><pre><code class="language-csharp">using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using Newtonsoft.Json.Linq;
using System.Text.Json;

BenchmarkRunner.Run&lt;LogBenchmark&gt;();

[MemoryDiagnoser]
public class LogBenchmark
{
    private readonly byte[] _utf8LogBytes;
    private readonly string _logString;

    public LogBenchmark()
    {
        _logString = """
        {
            "level": "info",
            "deviceId": "A1B2C3D4E5F60788",
            "timestamp": "2025-12-01T10:22:00Z",
            "message": "Temperature reading received",
            "value": 22.5
        }
        """;

        _utf8LogBytes = System.Text.Encoding.UTF8.GetBytes(_logString);
    }

    // ------------------------------------------------------------
    // 1. Baseline: Newtonsoft.Json 
    // ------------------------------------------------------------
    [Benchmark(Baseline = true)]
    public string Newtonsoft_Parse()
    {
        var obj = JObject.Parse(_logString);
        return (string)obj["deviceId"]!;
    }

    // ------------------------------------------------------------
    // 2. System.Text.Json in UTF-16 
    // ------------------------------------------------------------
    [Benchmark]
    public string SystemTextJson_Parse()
    {
        using var doc = JsonDocument.Parse(_logString);
        return doc.RootElement.GetProperty("deviceId").GetString()!;
    }

    // ------------------------------------------------------------
    // 3. UTF8JsonReader + Span&lt;byte&gt; 
    // ------------------------------------------------------------
    [Benchmark]
    public string? Utf8JsonReader_Parse()
    {
        var reader = new Utf8JsonReader(_utf8LogBytes);

        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.PropertyName &amp;&amp;
                reader.ValueTextEquals("deviceId"))
            {
                reader.Read();
                return reader.GetString();
            }
        }

        return null;
    }
}</code></pre><p>So, to keep the scale equal, I have used the same code for all 3 methods with the same input message.</p><p><strong>Step 4: Run and test</strong></p><p>Running the project</p><pre><code class="language-console">dotnet run -c Release</code></pre><table>
<thead>
<tr>
<th style="text-align:left">Method</th>
<th style="text-align:right">Mean</th>
<th style="text-align:right">Error</th>
<th style="text-align:right">StdDev</th>
<th style="text-align:right">Median</th>
<th style="text-align:right">Ratio</th>
<th style="text-align:right">RatioSD</th>
<th style="text-align:right">Gen0</th>
<th style="text-align:right">Allocated</th>
<th style="text-align:right">Alloc Ratio</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left">Newtonsoft_Parse</td>
<td style="text-align:right">2,326.9 ns</td>
<td style="text-align:right">65.47 ns</td>
<td style="text-align:right">184.67 ns</td>
<td style="text-align:right">2,285.6 ns</td>
<td style="text-align:right">1.01</td>
<td style="text-align:right">0.11</td>
<td style="text-align:right">3.0098</td>
<td style="text-align:right">4728 B</td>
<td style="text-align:right">1.000</td>
</tr>
<tr>
<td style="text-align:left">SystemTextJson_Parse</td>
<td style="text-align:right">793.3 ns</td>
<td style="text-align:right">19.81 ns</td>
<td style="text-align:right">54.57 ns</td>
<td style="text-align:right">777.6 ns</td>
<td style="text-align:right">0.34</td>
<td style="text-align:right">0.03</td>
<td style="text-align:right">0.0706</td>
<td style="text-align:right">112 B</td>
<td style="text-align:right">0.024</td>
</tr>
<tr>
<td style="text-align:left">Utf8JsonReader_Parse</td>
<td style="text-align:right">202.5 ns</td>
<td style="text-align:right">7.74 ns</td>
<td style="text-align:right">22.32 ns</td>
<td style="text-align:right">195.8 ns</td>
<td style="text-align:right">0.09</td>
<td style="text-align:right">0.01</td>
<td style="text-align:right">0.0253</td>
<td style="text-align:right">40 B</td>
<td style="text-align:right">0.008</td>
</tr>
</tbody>
</table>
<p>We can observe the clear difference between the two versions. .NET 10 improved logging and string operations significantly. Our latest fellow reduced execution time up to 38%. Let's break down what made .NET 10 achieve the feat</p><ul><li>JIT optimizes Span&lt;T&gt; and Utf8JsonReader in the area of escape analysis. It reduced object allocation and reduced<a href="https://blog.elmah.io/how-net-garbage-collector-works-and-when-you-should-care/" rel="noreferrer"> Gen0 Garbage Collector (GC)</a> activity. JIT also inlines small methods, <code>Utf8JsonReader</code>resulting in fewer loops. </li><li>UTF-8 processing also saw improvements, including faster UTF-8 transcoding, reduced branching on common code paths, and faster byte scanning. </li><li>The JIT compiler can place the promoted members of struct arguments into shared registers directly, rather than on the stack, eliminating unnecessary memory operations. </li><li>.NET team is working to upgrade System. Text is Microsoft's own string manipulation library. <a href="https://blog.elmah.io/optimizing-json-serialization-in-net-newtonsoft-json-vs-system-text-json/" rel="noreferrer">System.Text that is native to .NET, as opposed to Newtonsoft, which is third-party.</a> They improved property lookup, UTF-8 parsing, and JSONDocument memory usage in the updates.</li><li>Although NewtonSoft is not managed by Microsoft, compiler enhancements affected the application's overall performance. We saw that in our results, too. Lower GC pauses, better dictionary lookups, faster JIT inlining, and improved string allocation helped Newtonsoft perform well. </li></ul><h2 id="conclusion">Conclusion</h2><p>.NET 10 is released in November 2025 and is supported for three years as a long-term support (LTS) release. It brings the latest version of C# with many refinements. In the blog post for our .NET 10 and C# 14 series, I highlighted improvements to the log aggregation jobs. Numerous changes to GC operations, JIT, and string manipulation have yielded significant results, as I demonstrated through benchmarking comparing predecessor .NET 8. </p><p>.NET 8 example: <a href="https://github.com/elmahio-blog/LogBenchmark.Net8">https://github.com/elmahio-blog/LogBenchmark.Net8</a></p><p>.NET 10 example: <a href="https://github.com/elmahio-blog/LogBenchmark.Net10">https://github.com/elmahio-blog/LogBenchmark.Net10</a></p> ]]></content:encoded>
    </item>
    <item>
        <title><![CDATA[ Using Strategy Pattern with Dependency Injection in ASP.NET Core ]]></title>
        <description><![CDATA[ Selection logic is a prominent part of many applications. Whether you add a simple environment toggle, a UI mode decision, or apply a discount, you have to rely on user input. Sometimes, simply using an intuitive if-else or a switch case can work. However, when conditions are growing or a ]]></description>
        <link>https://blog.elmah.io/using-strategy-pattern-with-dependency-injection-in-asp-net-core/</link>
        <guid isPermaLink="false">6925433163da690001befad9</guid>
        <category><![CDATA[  ]]></category>
        <dc:creator><![CDATA[ Ali Hamza Ansari ]]></dc:creator>
        <pubDate>Wed, 10 Dec 2025 09:24:40 +0100</pubDate>
        <media:content url="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/12/using-strategy-pattern-with-dependency-injection-in-asp.net-core-o.png" medium="image"/>
        <content:encoded><![CDATA[ <p>This blog post is originally published on <a href="https://blog.elmah.io/using-strategy-pattern-with-dependency-injection-in-asp-net-core/">https://blog.elmah.io/using-strategy-pattern-with-dependency-injection-in-asp-net-core/</a></p> 
<!--kg-card-begin: html-->
<div class="toc"></div>
<!--kg-card-end: html-->
<p>Selection logic is a prominent part of many applications. Whether you add a simple environment toggle, a UI mode decision, or apply a discount, you have to rely on user input. Sometimes, simply using an intuitive if-else or a switch case can work. However, when conditions are growing or a complex algorithm selection is required, simple conditional statements can't work. Your code becomes exhaustive and hard to maintain. The Strategy pattern rescues the situation, adheres to the open/closed principle, and keeps the logic maintainable. This article walks you through a practical, straightforward example of the strategy pattern: choosing between Regular, VIP, and Student discount strategies at runtime.</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/12/using-strategy-pattern-with-dependency-injection-in-asp.net-core-o-1.png" class="kg-image" alt="Using Strategy Pattern with Dependency Injection in ASP.NET Core" loading="lazy" width="1500" height="750" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2025/12/using-strategy-pattern-with-dependency-injection-in-asp.net-core-o-1.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2025/12/using-strategy-pattern-with-dependency-injection-in-asp.net-core-o-1.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/12/using-strategy-pattern-with-dependency-injection-in-asp.net-core-o-1.png 1500w" sizes="(min-width: 1200px) 1200px"></figure><h2 id="what-is-the-strategy-pattern">What is the Strategy pattern?</h2><p>The Strategy design pattern is a behavioral pattern used when you need to switch between different algorithms at runtime. The strategy pattern encapsulates algorithms and selects the right one when needed, usually based on an input. This pattern provides a flexible, maintainable solution to an algorithm-selection problem, keeping the code cleaner and easier to extend. If you need to add a new algorithm, just add another class instead of touching the existing logic, adhering to the open/closed principle.</p><h2 id="what-is-the-problem-without-the-strategy-pattern">What is the problem without the Strategy pattern?</h2><p>To understand the usability of the strategy pattern, we need to identify the problems we may face without it. Suppose we offer different discounts to different users based on their membership. A naive solution is to use an if-else statement or a switch case. Let's do it and evaluate the implementation.</p><p><strong>Step 1: Create a Console application</strong></p><pre><code class="language-console">dotnet new console -n StrategyPatternDemo
cd StrategyPatternDemo</code></pre><p><strong>Step 2: Create DiscountService class</strong></p><p>In the service, we will define discount calculation with a conditional statement.</p><pre><code class="language-csharp">public class DiscountService
{
    public decimal GetDiscount(string customerType, decimal amount)
    {
        if (customerType.ToLower() == "regular")
        {
            return amount * 0.05m;
        }
        else if (customerType.ToLower() == "vip")
        {
            return amount * 0.20m;
        }
        else
        {
            return 0;
        }
    }
}</code></pre><p><strong>Step 3: Use the service in the Strategy Pattern <code>Sword.cs</code></strong></p><pre><code class="language-csharp">using StrategyPatternDemo;

Console.Write("Enter customer type (regular/vip): ");
var type = Console.ReadLine();

Console.Write("Enter amount: ");
var amount = decimal.Parse(Console.ReadLine());

var service = new DiscountService();
var discount = service.GetDiscount(type, amount);
var final = amount - discount;

Console.WriteLine($"Discount: {discount}");
Console.WriteLine($"Final Price: {final}");</code></pre><p><strong>Step 4: Run and test</strong></p><p>Let's test it </p><pre><code class="language-console">dotnet run</code></pre><p>Output</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/11/image-17.png" class="kg-image" alt="Output" loading="lazy" width="520" height="150"></figure><p>It works as expected. But the code contains design and maintainability flaws.</p><ul><li>The solution violates the Open/Closed principle. Adding a new membership will require changes to the core method, such as adding an else-if block. </li><li>All the discount logic is tightly coupled in a single class and lacks separation of concerns or single responsibility.</li><li>Conjoined code makes testing harder. To ensure the functionality, you have to test the monster every time.</li><li>As the conditions grow, you can fall into a spiral of conditions. Imagine if you have 20 memberships, that will be a nightmare for maintainability.</li></ul><h2 id="implementing-the-strategy-pattern-in-a-console-application">Implementing the strategy pattern in a console application</h2><p>In our example, let's address the above issues using the Strategy Pattern.</p><p><strong>Step 1: Define Strategy Interface</strong></p><p>Adding the discount strategy interface</p><pre><code class="language-csharp">public interface IDiscountStrategy
{
    decimal ApplyDiscount(decimal amount);
}</code></pre><p><strong>Step 2: Add concrete strategies</strong></p><p>Adding separate implementations of each algorithm</p><pre><code class="language-csharp">public class RegularDiscount : IDiscountStrategy
{
    public decimal ApplyDiscount(decimal amount) =&gt; amount * 0.05m;
}</code></pre><p>For Vip</p><pre><code class="language-csharp">public class VipDiscount : IDiscountStrategy
{
    public decimal ApplyDiscount(decimal amount) =&gt; amount * 0.20m;
}</code></pre><p>Notice that none of the strategies implement validation or error handling. In real-world code, you would probably want to look into that. This part has been left out of this post since the focus is around splitting the business logic out in strategies.</p><p><strong>Step 3: Define context class</strong></p><pre><code class="language-csharp">public class DiscountService
{
    private readonly IDiscountStrategy _strategy;

    public DiscountService(IDiscountStrategy strategy)
    {
        _strategy = strategy;
    }

    public decimal GetDiscount(decimal amount) =&gt; _strategy.ApplyDiscount(amount);
}</code></pre><p>The Context class in the strategy pattern holds a reference to a strategy interface (<code>IDiscountStrategy</code> in our case). It receives a strategy from outside. It does not implement logic itself, instead, it delegates work to the strategy, while the concrete classes define their logic. </p><p><strong>Step 4: Use the strategy in the <code>Program.cs</code></strong></p><pre><code class="language-csharp">
Console.WriteLine("Enter customer type (regular/vip): ");
string type = Console.ReadLine()?.ToLower();

IDiscountStrategy strategy;

// Manually picking strategy — no switch needed, but you *can* if you want.
if (type == "vip")
    strategy = new VipDiscount();
else
    strategy = new RegularDiscount();

var service = new DiscountService(strategy);

Console.Write("Enter amount: ");
decimal amount = decimal.Parse(Console.ReadLine());

var discount = service.GetDiscount(amount);
var finalPrice = amount - discount;

Console.WriteLine($"Discount applied: {discount}");
Console.WriteLine($"Final price: {finalPrice}");</code></pre><p><strong>Output</strong></p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/11/image-18.png" class="kg-image" alt="Output" loading="lazy" width="480" height="165"></figure><p>We understand basic principles of the strategy pattern. We can proceed with our primary target: implementing the strategy pattern in ASP.NET Core.</p><h2 id="implementing-the-strategy-pattern-in-an-aspnet-core-api">Implementing the strategy pattern in an ASP.NET Core API</h2><p><strong>Step 1: Create a .NET Core api </strong></p><p>Run the following command in the terminal</p><pre><code class="language-console">dotnet new webapi -n StrategyPatternApi
cd StrategyPatternApi
</code></pre><p><strong>Step 2: Add concrete strategies</strong></p><p>Adding separate implementations of each algorithm</p><pre><code class="language-csharp">public class RegularDiscount : IDiscountStrategy
{
    public decimal ApplyDiscount(decimal amount) =&gt; amount * 0.05m;
}</code></pre><p>For Vip</p><pre><code class="language-csharp">public class VipDiscount : IDiscountStrategy
{
    public decimal ApplyDiscount(decimal amount) =&gt; amount * 0.20m;
}</code></pre><p><strong>Step 3: Define context class</strong></p><pre><code class="language-csharp">public class DiscountService
{
    private readonly Func&lt;string, IDiscountStrategy&gt; _strategyFactory;

    public DiscountService(Func&lt;string, IDiscountStrategy&gt; strategyFactory)
    {
        _strategyFactory = strategyFactory;
    }

    // public API: ask for a discount by customer type
    public decimal GetDiscount(string customerType, decimal amount)
    {
        var strategy = _strategyFactory(customerType);
        return strategy.ApplyDiscount(amount);
    }
}</code></pre><p><code>DiscountService</code> plays the context role in the strategy pattern. <code>DiscountService</code> has a property <code>Func&lt;string, IDiscountStrategy&gt; _strategyFactory</code> that holds a factory delegate. The <code>Func</code> delegate returns an appropriate implementation of <code>IDiscountStrategy</code> based on the given type. <code>Func</code> allows the service to request a strategy at runtime by name/key without knowing the DI container internals or concrete types.</p><p><strong>Step 4: Add a controller with the endpoint</strong></p><pre><code class="language-csharp">[ApiController]
[Route("api/[controller]")]
public class PricingController : ControllerBase
{
    private readonly DiscountService _pricingService;

    public PricingController(DiscountService pricingService)
    {
        _pricingService = pricingService;
    }

    [HttpGet]
    public IActionResult Get([FromQuery] string type, [FromQuery] decimal amount)
    {
        var discount = _pricingService.GetDiscount(type, amount);
        var final = amount - discount;
        return Ok(new { type = type ?? "regular", amount, discount, final });
    }
}</code></pre><p><strong>Step 5: Configure <code>Program.cs</code></strong></p><p>Add the concrete services in dependency injection (DI) in the <code>Program.cs</code> file </p><pre><code class="language-csharp">services.AddTransient&lt;RegularDiscount&gt;();
services.AddTransient&lt;VipDiscount&gt;();</code></pre><p>They are transient because discount strategies are stateless, so creating a new instance each time is fine. Note that I haven't injected them with <code>IDiscountStrategy</code> any implementing service because ASP.NET Core decides this automatically. Hence, the final code will look like this:</p><pre><code class="language-csharp">using StrategyPatternApi;

var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// Register concrete strategy types so they can be resolved by the factory
services.AddTransient&lt;RegularDiscount&gt;();
services.AddTransient&lt;VipDiscount&gt;();

services.AddSingleton&lt;Func&lt;string, IDiscountStrategy&gt;&gt;(sp =&gt; key =&gt;
{
    var k = (key ?? "").Trim().ToLowerInvariant();
    return k switch
    {
        "vip" =&gt; sp.GetRequiredService&lt;VipDiscount&gt;(),
        // add more cases if you add more strategies
        _ =&gt; sp.GetRequiredService&lt;RegularDiscount&gt;()
    };
});

// Register the service that uses the factory
services.AddScoped&lt;DiscountService&gt;();

// Add controllers (or leave for minimal endpoints)
services.AddControllers();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
app.MapControllers();
app.Run();</code></pre><p>In DI, the decisive part is:</p><pre><code class="language-csharp">services.AddSingleton&lt;Func&lt;string, IDiscountStrategy&gt;&gt;(sp =&gt; key =&gt;
{
    var k = (key ?? "").Trim().ToLowerInvariant();
    return k switch
    {
        "vip" =&gt; sp.GetRequiredService&lt;VipDiscount&gt;(),
        // add more cases if you add more strategies
        _ =&gt; sp.GetRequiredService&lt;RegularDiscount&gt;()
    };
});</code></pre><p>As explicitly stated, the switch condition resolves the appropriate concrete strategy via DI based on the type value. If any condition does not match, I made a default choice to get <code>RegularService</code>.</p><p><strong>Step 6: Run and test</strong></p><pre><code class="language-console">dotnet run</code></pre><p>Now running the project</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/12/image.png" class="kg-image" alt="/api/Pricing endpoint" loading="lazy" width="863" height="505" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2025/12/image.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/12/image.png 863w" sizes="(min-width: 720px) 720px"></figure><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/12/image-1.png" class="kg-image" alt="Response" loading="lazy" width="671" height="224" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2025/12/image-1.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/12/image-1.png 671w"></figure><h2 id="extension-of-algorithms-in-the-aspnet-core-strategy-pattern">Extension of algorithms in the ASP.NET Core strategy pattern</h2><p>The Open/Close principle is one of the benefits of the Strategy Pattern. Let's continue with our example of how we can add a new discount within the bounds of the Open/Close principle.</p><p><strong>Step 1: Add the Student discount's concrete strategy</strong></p><pre><code class="language-csharp">public class StudentDiscount : IDiscountStrategy
{
    public decimal ApplyDiscount(decimal amount) =&gt; amount * 0.10m;
}
</code></pre><p><strong>Step 2: Register a new service</strong></p><pre><code class="language-csharp">services.AddTransient&lt;StudentDiscount&gt;();
</code></pre><p><strong>Step 3: Update factory switch</strong></p><pre><code class="language-csharp">services.AddSingleton&lt;Func&lt;string, IDiscountStrategy&gt;&gt;(sp =&gt; key =&gt;
{
    var k = (key ?? "").Trim().ToLowerInvariant();
    return k switch
    {
        "vip" =&gt; sp.GetRequiredService&lt;VipDiscount&gt;(),
        "student" =&gt; sp.GetRequiredService&lt;StudentDiscount&gt;(),   
        _ =&gt; sp.GetRequiredService&lt;RegularDiscount&gt;()
    };
});
</code></pre><p>To add a new strategy implementation, we simply need to add the strategy code and inject it via dynamic DI.</p><p><strong>Step 4: Run and test</strong></p><pre><code class="language-console">dotnet run</code></pre><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/12/image-2.png" class="kg-image" alt="/api/Pricing endpoint" loading="lazy" width="776" height="495" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2025/12/image-2.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/12/image-2.png 776w" sizes="(min-width: 720px) 720px"></figure><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/12/image-3.png" class="kg-image" alt="Response" loading="lazy" width="424" height="231"></figure><p>By default value</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/12/image-4.png" class="kg-image" alt="Default value" loading="lazy" width="693" height="484" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2025/12/image-4.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/12/image-4.png 693w"></figure><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/12/image-5.png" class="kg-image" alt="Response" loading="lazy" width="425" height="225"></figure><h2 id="conclusion">Conclusion</h2><p>Writing long if-else or cases is tiring. Every time you need to add a condition, you have to dive into the well and add one condition. The same happens while debugging. The strategy pattern provides a modular solution that keeps the code intact while dynamically allowing you to extend conditions. In this blog post, I highlighted the need for a strategy pattern and showed how to implement it in an ASP.NET Core API.</p><p>Example 1: <a href="https://github.com/elmahio-blog/StrategyPatternDemo">https://github.com/elmahio-blog/StrategyPatternDemo</a></p><p>Example 2:  <a href="https://github.com/elmahio-blog/StrategyPatternApi">https://github.com/elmahio-blog/StrategyPatternApi</a></p> ]]></content:encoded>
    </item>
    <item>
        <title><![CDATA[ Mastering owned entities in EF Core: Cleaner complex types ]]></title>
        <description><![CDATA[ Not all data in your application should live as a standalone table with its own ID and lifecycle. Sometimes you need a tightly coupled dependent object that exists alongside its parent, like a movie&#39;s budget, a survey&#39;s questions, or a customer&#39;s address. If you ]]></description>
        <link>https://blog.elmah.io/mastering-owned-entities-in-ef-core-cleaner-complex-types/</link>
        <guid isPermaLink="false">691f5824fb01eb0001fd248b</guid>
        <category><![CDATA[  ]]></category>
        <dc:creator><![CDATA[ Ali Hamza Ansari ]]></dc:creator>
        <pubDate>Tue, 02 Dec 2025 09:12:25 +0100</pubDate>
        <media:content url="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/11/mastering-owned-entities-in-ef-core-cleaner-complex-types-o.png" medium="image"/>
        <content:encoded><![CDATA[ <p>This blog post is originally published on <a href="https://blog.elmah.io/mastering-owned-entities-in-ef-core-cleaner-complex-types/">https://blog.elmah.io/mastering-owned-entities-in-ef-core-cleaner-complex-types/</a></p> 
<!--kg-card-begin: html-->
<div class="toc"></div>
<!--kg-card-end: html-->
<p>Not all data in your application should live as a standalone table with its own ID and lifecycle. Sometimes you need a tightly coupled dependent object that exists alongside its parent, like a movie's budget, a survey's questions, or a customer's address. If you had the magic to handle this data differently, you could save tons of lines and reduce Entity Framework Core (EF Core) overhead. The good news is that EF Core offers owned entities that do this. It reduces the complexities of your domain model. In this post, I will show you how to use owned entity types to maintain dependent data.</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/11/mastering-owned-entities-in-ef-core-cleaner-complex-types-o-1.png" class="kg-image" alt="Mastering owned entities in EF Core: Cleaner complex types" loading="lazy" width="1500" height="750" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2025/11/mastering-owned-entities-in-ef-core-cleaner-complex-types-o-1.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2025/11/mastering-owned-entities-in-ef-core-cleaner-complex-types-o-1.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/11/mastering-owned-entities-in-ef-core-cleaner-complex-types-o-1.png 1500w" sizes="(min-width: 1200px) 1200px"></figure><h2 id="what-is-an-owned-entity">What is an owned entity?</h2><p>In EF Core, an owned entity is a special type of entity that exists only within another entity and cannot stand alone in the database. Unlike other EF models, owned entities do not have their own identity, such as a primary key. Owned entities inherit the identity of their parent table. The lifecycle of owned entities also depends on the parent entity. When the owning entity is created, owned properties are made, when it is deleted, all of its owned properties are also deleted. </p><h2 id="key-characteristics-of-ef-cores-owned-properties">Key characteristics of EF Core's owned properties </h2><p>EF Core owned entities possess the following characteristics:</p><ul><li>Owned entity types do not have a separate identity. Instead, they depend on their parents' identity.</li><li>An owned entity's lifecycle is tied to its owner entity and cannot exist independently of it. It is created, updated, and deleted with its parent entity.</li><li>They are stored in the same table as their owner by default. Although we create a separate class for them, they do not go to a separate table and are saved with their class prefix, like <code>Address_Street</code>.</li><li>An owned entity does not have a separate <code>DbSets</code> and cannot be queried independently.</li><li>An owned entity always has a one-to-one relation with its owner entity</li><li>You cannot reference an owned entity with another entity via a foreign key relation.</li><li>EF change tracker tracks owned entities along with their owners, not as separate entities.</li><li>Can encapsulate complex data types such as coordinates, addresses, and contact details.</li></ul><h2 id="how-to-use-an-owned-entity-in-ef-core">How to use an owned entity in EF Core</h2><p>I am creating a console application to see how we can implement an owned entity in an EF Core-based project.</p><p><strong>Step 1: Create a console application</strong></p><p>Run the following in the CLI</p><pre><code class="language-console">dotnet new console -o MovieProjectionDemo
</code></pre><p><strong>Step 2: Install all required packages</strong></p><p>For the project, we need to run the command to install the required NuGet packages.</p><pre><code class="language-console">dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.Json</code></pre><p>I will be using a Postgres database, so installing <code>Npgsql.EntityFrameworkCore.PostgreSQL</code> You can install the NuGet package accordingly, as per your preferred database.</p><p><strong>Step 3: Add models</strong></p><pre><code class="language-csharp">namespace OwnedEntityDemo.Models;

public class Movie
{
    public int Id { get; set; }
    public string Title { get; set; } = string.Empty;
    public int ReleaseYear { get; set; }
    public string Genre { get; set; } = string.Empty;
    
    public Budget Budget { get; set; } = null!;
}

public class Budget
{
    public decimal ProductionCost { get; set; }
    public decimal MarketingCost { get; set; }
    public decimal DistributionCost { get; set; }
    public string Currency { get; set; } = string.Empty;
}</code></pre><p>Here, the <code>Budget</code> object will be part of the <code>Movie</code> entity. Note, I did not create any ID in the budget as it relies on its owner, the <code>Movie</code> entity.</p><p><strong>Step 4: Set up the DbContext</strong></p><pre><code class="language-csharp">using Microsoft.Extensions.Configuration;
using OwnedEntityDemo.Models;
using Microsoft.EntityFrameworkCore;

namespace OwnedEntityDemo;

public class AppDbContext: DbContext
{
    public DbSet&lt;Movie&gt; Movies =&gt; Set&lt;Movie&gt;();

    private readonly string _connectionString;

    public AppDbContext()
    {
        // Simple reading from appsettings.json
        var config = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("appsettings.json")
            .Build();

        _connectionString = config.GetConnectionString("PostgresConnection");
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseNpgsql(_connectionString);
        
    }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity&lt;Movie&gt;()
            .OwnsOne(c =&gt; c.Budget, bugdet =&gt;
            {
                bugdet.Property(a =&gt; a.ProductionCost);
                bugdet.Property(a =&gt; a.DistributionCost);
                bugdet.Property(a =&gt; a.MarketingCost);
                bugdet.Property(a =&gt; a.Currency);
            });
    }
}</code></pre><p>One <code>DbSet</code> is registered. Also, you need to configure the <code>Budget</code> entity as an owned entity with <code>OwnsOne</code> in the <code>OnModelCreating</code> method.</p><p><strong>Step 5: Create <code>appsettings.json</code></strong></p><p>The console app does not contain an appsettings file by default. So I have created one with the connection string:</p><pre><code class="language-json">{
  "ConnectionStrings": {
    "PostgresConnection": "Host=localhost;Port=5432;Database=tvDb;Username=postgres;Password=4567"
  }
}
</code></pre><p><strong>Step 6: Configure appsettings in csproj</strong></p><p>By default, a console app will expect the file in the bin directory. To read newly added appsettings from the root directory, add the following inside the&nbsp;<code>&lt;Project&gt;</code>&nbsp;tag of the application's project file:</p><pre><code class="language-xml">&lt;ItemGroup&gt;
  &lt;None Update="appsettings.json"&gt;
    &lt;CopyToOutputDirectory&gt;PreserveNewest&lt;/CopyToOutputDirectory&gt;
  &lt;/None&gt;
&lt;/ItemGroup&gt;</code></pre><p><strong>Step 7: Prepare the database</strong></p><p>To create the table in the database, create and run the migration:</p><pre><code class="language-console">dotnet ef migrations add InitialCreate
</code></pre><pre><code class="language-console">dotnet ef database update</code></pre><p>Our database is now ready</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/11/image-11.png" class="kg-image" alt="Database" loading="lazy" width="237" height="217"></figure><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/11/image-13.png" class="kg-image" alt="Movies table" loading="lazy" width="242" height="246"></figure><p>Notice how columns of owned entities are named as a prefix of the owned class name.</p><p><strong>Step 8: Prepare <code>Program.cs</code> to write the data </strong></p><p>We will seed the data by initializing it in the <code>Program.cs</code> file:</p><pre><code class="language-csharp">using Microsoft.EntityFrameworkCore;
using OwnedEntityDemo;
using OwnedEntityDemo.Models;

using var context = new AppDbContext();

// Make sure database exists
await context.Database.EnsureCreatedAsync();

// Add a movie with budget
var movie1 = new Movie
{
    Title = "Inception",
    Genre = "Sci-Fi",
    ReleaseYear = 2010,
    Budget = new Budget
    {
        ProductionCost = 160_000_000,
        MarketingCost = 100_000_000,
        DistributionCost = 50_000_000,
        Currency = "USD"
    }
};
var movie2 = new Movie
{
    Title = "Memento",
    Genre = "Thriller",
    ReleaseYear = 2000,
    Budget = new Budget
    {
        ProductionCost = 90_000_000,
        MarketingCost = 120_000_000,
        DistributionCost = 2_000_000,
        Currency = "USD"
    }
};

context.Movies.Add(movie1);
context.Movies.Add(movie2);
await context.SaveChangesAsync();
Console.WriteLine("Movie saved!");

// Query movie
var storedMovies = await context.Movies.ToListAsync();
foreach (var movie in storedMovies)
{
    Console.WriteLine($"Movie: {movie.Title}, Total Budget: {movie.Budget.ProductionCost + movie.Budget.MarketingCost}");
}</code></pre><p>In the first part, we are populating the <code>Movie</code> table with some data. While the latter part gets and prints them. I focused on implementing the owned entity. You can <a href="https://blog.elmah.io/building-read-models-with-ef-core-projections/" rel="noreferrer">query the records better with a projection</a>.</p><p><strong>Step 9: Run the project</strong></p><p>Let's run it:</p><pre><code class="language-console">dotnet run</code></pre><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/11/image-12.png" class="kg-image" alt="Output" loading="lazy" width="464" height="107"></figure><p>The above approach can be more customized. You can rename the column in the configuration as you want.</p><pre><code class="language-csharp">modelBuilder.Entity&lt;Movie&gt;(entity =&gt;
{
    entity.OwnsOne(m =&gt; m.Budget, budget =&gt;
    {
        // Optional: Configure column names
        budget.Property(b =&gt; b.ProductionCost).HasColumnName("ProductionCost");
        budget.Property(b =&gt; b.MarketingCost).HasColumnName("MarketingCost");
        budget.Property(b =&gt; b.DistributionCost).HasColumnName("DistributionCost");
        budget.Property(b =&gt; b.Currency).HasColumnName("Currency");
    });
});</code></pre><p>That way, the columns will be like <code>PodcutionCost</code> and <code>MarketingCost</code> instead of <code>Budget_PodcutionCost</code> and <code>Budget_MarketingCost</code>.</p><h2 id="how-to-add-nested-owned-entities-in-ef-core">How to add nested owned entities in EF Core</h2><p>An entity can have more than one owned entity. Also, you can define a nested owned entity, an owned entity inside another owned entity. Let's have a look at how we can do that with the same example.</p><p><strong>Step 1: Add another owned entity</strong></p><p>Create the class <code>CostBreakdown</code>:</p><pre><code class="language-csharp">public class Budget
{
    public decimal ProductionCost { get; set; }
    public decimal MarketingCost { get; set; }
    public decimal DistributionCost { get; set; }
    public string Currency { get; set; } = string.Empty;
    public CostBreakdown Breakdown { get; set; } = null!;
}

public class CostBreakdown
{
    public decimal CastSalaries { get; set; }
    public decimal CrewSalaries { get; set; }
    public decimal Equipment { get; set; }
    public decimal CGI { get; set; }
}</code></pre><p><strong>Step 2: Configure a new owned type</strong></p><pre><code class="language-csharp">modelBuilder.Entity&lt;Movie&gt;(movie =&gt;
{
    movie.OwnsOne(m =&gt; m.Budget, budget =&gt;
    {
        budget.Property(b =&gt; b.ProductionCost);
        budget.Property(b =&gt; b.MarketingCost);
        budget.Property(b =&gt; b.DistributionCost);
        budget.Property(b =&gt; b.Currency);

        // Nested Owned Entity
        budget.OwnsOne(b =&gt; b.Breakdown, breakdown =&gt;
        {
            breakdown.Property(p =&gt; p.CastSalaries);
            breakdown.Property(p =&gt; p.CrewSalaries);
            breakdown.Property(p =&gt; p.Equipment);
            breakdown.Property(p =&gt; p.CGI);
        });
    });
});</code></pre><p>As in the<strong> </strong>example, you can use the fluent API to add nested owned types to relations in your <code>Movies</code> table.</p><p><strong>Step 3: Add migration</strong></p><p>Add a new migration by running the command:</p><pre><code class="language-console">dotnet ef migrations add CostBreakdownAdded</code></pre><p>and apply it:</p><pre><code class="language-console">dotnet ef database update</code></pre><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/11/image-14.png" class="kg-image" alt="Updated Movies table" loading="lazy" width="280" height="336"></figure><h2 id="how-to-add-a-collection-owned-entity-in-ef-core">How to add a Collection Owned Entity in EF Core</h2><p>Owned types can be a collection as well, where multiple objects of the same type depend on a single owner entity. In that case, EF Core maintains a separate table to save the dependent data. Let's see how we can do in our example.</p><p><strong>Step 1: Create models</strong></p><p>Let's create a new <code>Questionnaire</code> model. All the questions will be collected from the entity in question. <code>Question</code>.</p><pre><code class="language-csharp ">public class Questionnaire
{
    public int Id { get; set; }
    public string Title { get; set; } = string.Empty;

    public List&lt;Question&gt; Questions { get; set; } = new();
}

public class Question
{
    public string Text { get; set; } = string.Empty;
    public string Type { get; set; } = string.Empty;  // e.g. Text, MCQ, Rating
    public bool IsRequired { get; set; }
}</code></pre><p><strong>Step 2: Perform configurations in the DbContext</strong></p><p>Add a new <code>DbSet</code>:</p><pre><code class="language-csharp">public DbSet&lt;Questionnaire&gt; Questionnaires =&gt; Set&lt;Questionnaire&gt;();
</code></pre><p>and use the fluent API to set up the entity:</p><pre><code class="language-csharp">modelBuilder.Entity&lt;Questionnaire&gt;(q =&gt;
{
    q.OwnsMany(x =&gt; x.Questions, questions =&gt;
    {
        questions.WithOwner().HasForeignKey("QuestionnaireId");

        questions.Property&lt;int&gt;("Id");           // shadow key
        questions.HasKey("Id");

        questions.Property(x =&gt; x.Text).HasColumnName("Text");
        questions.Property(x =&gt; x.Type).HasColumnName("Type");
        questions.Property(x =&gt; x.IsRequired).HasColumnName("IsRequired");

        questions.ToTable("QuestionnaireQuestions"); // optional
    });
});</code></pre><p><code>OwnsMany</code> configures that each entity of <code>Questionnaire</code> will hold a collection of <code>Questions</code> as an owned type. <code>questions.WithOwner().HasForeignKey("QuestionnaireId");</code> defines that each <code>Question</code> belongs to one <code>Questionnaire</code> with a foreign key <code>QuestionnaireId</code>. EF Core will translate it to:</p><pre><code class="language-SQL">QuestionnaireId INTEGER NOT NULL
</code></pre><p>Although <code>Question</code>s have a primary and foreign key, they are not queryable and cannot exist independently.</p><p><strong>Step 3: Run migration</strong></p><p>Make the updates with a new migration:</p><pre><code class="language-console">dotnet ef migrations add QuestionaireAdded</code></pre><p>And run the migration:</p><pre><code class="language-console">dotnet ef database update</code></pre><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/11/image-15.png" class="kg-image" alt="Updated database" loading="lazy" width="218" height="507"></figure><p><strong>Step 4: Set up Program.cs to insert data</strong></p><pre><code class="language-csharp">var survey = new Questionnaire
{
    Title = "Customer Satisfaction Survey",
    Questions = new List&lt;Question&gt;
    {
        new Question { Text = "How satisfied are you?", Type = "Rating", IsRequired = true },
        new Question { Text = "What can we improve?", Type = "Text", IsRequired = false },
        new Question { Text = "Would you recommend us?", Type = "YesNo", IsRequired = true }
    }
};
context.Questionnaires.Add(survey);
await context.SaveChangesAsync();
Console.WriteLine("Data saved!");

var data = await context.Questionnaires.FirstAsync();
Console.WriteLine($"Questionaire: {data.Title}");
foreach (var item in data.Questions)
{
    Console.WriteLine($"Text: {item.Text}, Type: {item.Type}");
}</code></pre><p><strong>Step 5: Test the project</strong></p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2025/11/image-16.png" class="kg-image" alt="Output" loading="lazy" width="525" height="178"></figure><h2 id="advantages-of-an-owned-entity">Advantages of an owned entity</h2><ul><li>An owned entity is a value object defined solely by its values, without an identity. The nature of owned types allows your domain to model real-world concepts while remaining clean and expressive.</li><li>As owned entities are dependent, they cascade automatically with the owner. Hence, you do not require involvement in additional management for dependent fields, such as the questions in a questionnaire.</li><li>In the model, you can encapsulate related fields in separate classes, while the database does not need to map them to the tables. That way, you can keep code flexible without cluttering up extra tables or DbSets.</li><li>As EF Core stores the properties of owned entities in the same table, no additional joins are performed when querying the data. Hence, owning property helps improve the overall performance of the application.e</li><li>Owned handles parent-child relations cleanly with shadow keys. You don't need standalone entities even for one-to-many parent-child relationships. </li><li>Owned entities eliminate unnecessary IDs and primary keys by directly attaching them to their parent entities.</li><li>It keeps data consistent because the lifecycle of owned entities is tied to the owner, deleting or updating the owner changes the child entities.</li><li>The dependent entities reduce EF Core's overhead by allowing it to track only the parent entity.</li></ul><h2 id="conclusion">Conclusion</h2><p>Owned entity types provide a clean way to save complex values in the database. They are dependent entities that do not hold any identity or table of their own but exist alongside their parent. We delved into this remarkable feature and saw how you can encapsulate complex data such as addresses, costs, and order items. Owned entities keep the domain models clean by clustering related fields into dependent types.</p> ]]></content:encoded>
    </item>

</channel>
</rss>