<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>Thu, 21 May 2026 17:19:10 +0200</lastBuildDate>
<atom:link href="https://blog.elmah.io" rel="self" type="application/rss+xml"/>
<ttl>60</ttl>

    <item>
        <title><![CDATA[ How to upload files in an ASP.NET Core Web API ]]></title>
        <description><![CDATA[ Files are an integrated part of an application. From a social app to an ERP, some form of media exists in the ecosystem. .NET Core APIs provide built-in support for uploading and fetching documents. In today&#39;s post, I will show you how to cleanly and efficiently use .NET ]]></description>
        <link>https://blog.elmah.io/how-to-upload-files-in-an-asp-net-core-web-api/</link>
        <guid isPermaLink="false">69f6e14fdda1b400015f476d</guid>
        <category><![CDATA[  ]]></category>
        <dc:creator><![CDATA[ Ali Hamza Ansari ]]></dc:creator>
        <pubDate>Wed, 20 May 2026 09:47:16 +0200</pubDate>
        <media:content url="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/05/how-to-upload-files-in-an-asp.net-core-web-api-o.png" medium="image"/>
        <content:encoded><![CDATA[ <p>This blog post is originally published on <a href="https://blog.elmah.io/how-to-upload-files-in-an-asp-net-core-web-api/">https://blog.elmah.io/how-to-upload-files-in-an-asp-net-core-web-api/</a></p> 
<!--kg-card-begin: html-->
<div class="toc"></div>
<!--kg-card-end: html-->
<p>Files are an integrated part of an application. From a social app to an ERP, some form of media exists in the ecosystem. .NET Core APIs provide built-in support for uploading and fetching documents. In today's post, I will show you how to cleanly and efficiently use .NET Core's file system classes to receive file uploads.</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/05/how-to-upload-files-in-an-asp.net-core-web-api-o-1.png" class="kg-image" alt="How to upload files in an ASP.NET Core Web API" loading="lazy" width="1500" height="750" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/05/how-to-upload-files-in-an-asp.net-core-web-api-o-1.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/05/how-to-upload-files-in-an-asp.net-core-web-api-o-1.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/05/how-to-upload-files-in-an-asp.net-core-web-api-o-1.png 1500w" sizes="(min-width: 1200px) 1200px"></figure><h2 id="uploading-files-in-aspnet-core-api">Uploading files in ASP.NET Core API</h2><p>I am creating an API with .NET 10 to accomplish our goal.</p><p><strong>Step 1: Create a project</strong></p><pre><code class="language-console">dotnet new webapi -n fileUploadDemo
cd fileUploadDemo</code></pre><p><strong>Step 2: Create a controller</strong></p><pre><code class="language-csharp">using Microsoft.AspNetCore.Mvc;

namespace fileUploadDemo.Controllers;

[ApiController]
[Route("api/[controller]")]
public class FileController: ControllerBase
{
    [HttpPost("upload")]
    public async Task&lt;IActionResult&gt; Upload(IFormFile file)
    {
        if (file == null || file.Length == 0)
            return BadRequest("No file uploaded.");
    
        var uploadsFolder = Path.Combine(Directory.GetCurrentDirectory(), "Uploads");
    
        if (!Directory.Exists(uploadsFolder))
            Directory.CreateDirectory(uploadsFolder);
    
        var filePath = Path.Combine(uploadsFolder, file.FileName);
    
        using (var stream = new FileStream(filePath, FileMode.Create))
        {
            await file.CopyToAsync(stream);
        }
    
        return Ok(new { file.FileName, file.Length });
    }
}</code></pre><p>The endpoint upload will save the file to the "Uploads" directory and return the filename and length. </p><p><strong>Step 3: Configure Program.cs</strong></p><pre><code class="language-csharp">var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();

var app = builder.Build();

app.UseStaticFiles();
app.MapControllers();

app.UseHttpsRedirection();

app.Run();</code></pre><p><code>app.UseStaticFiles();</code> enables the app to serve static files directly over HTTP</p><p>Make sure your <code>Uploads</code> folder is accessible.</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/2026/05/image.png" class="kg-image" alt="Test" loading="lazy" width="957" height="357" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/05/image.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/05/image.png 957w" sizes="(min-width: 720px) 720px"></figure><p>Hence, our file is saved:</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/05/image-1.png" class="kg-image" alt="Solution explorer" loading="lazy" width="317" height="194"></figure><p>So far, we have seen a naive, minimal approach to saving a file in an ASP.NET Core API. However, in real applications, you may split it up into layers, services, or however you prefer to structure applications.</p><h2 id="adding-openapiswagger-documentation">Adding OpenApi/Swagger documentation </h2><p>.NET 10 does not create OpenAPI (formerly Swagger) documentation like previous versions. So we have to add them. First, install the NuGet package:</p><pre><code class="language-console">dotnet add package Swashbuckle.AspNetCore</code></pre><p>Next, change <code>Program.cs</code>:</p><pre><code class="language-csharp">using Microsoft.AspNetCore.Http.Features;

var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();

app.UseSwagger();
app.UseSwaggerUI(c =&gt;
{
    c.SwaggerEndpoint("/swagger/v1/swagger.json", "Upload V1");
});

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.UseStaticFiles();
app.MapControllers();
app.UseHttpsRedirection();

app.Run();</code></pre><p>The <code>AddOpenApi()</code> method registers with the OpenAPI document generation services. Then <code>AddEndpointsApiExplorer()</code> adds an API explorer service that discovers endpoints and provides metadata about routes, parameters, and return types. Without this, Swagger would show no APIs. <code>AddSwaggerGen()</code> configures Swashbuckle (Swagger for .NET) and generates a Swagger document. Besides, it adds schema generation for your DTOs. <code>UseSwagger()</code> registers middleware to serve the Swagger JSON document. You must register middleware before using SwaggerUI. The <code>UseSwaggerUI</code> configures the Swagger UI web interface and navigates the UI to the JSON document. It basically adds UI elements, such as the page and <em>Try out</em> buttons.</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/2026/05/image-2.png" class="kg-image" alt="Swagger" loading="lazy" width="1344" height="228" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/05/image-2.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/05/image-2.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/05/image-2.png 1344w" 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/05/image-3.png" class="kg-image" alt="Swagger" loading="lazy" width="1316" height="470" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/05/image-3.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/05/image-3.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/05/image-3.png 1316w" 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/05/image-4.png" class="kg-image" alt="Result" loading="lazy" width="1281" height="313" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/05/image-4.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/05/image-4.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/05/image-4.png 1281w" sizes="(min-width: 720px) 720px"></figure><h2 id="restrict-the-file-size-limit">Restrict the file size limit</h2><p>It is recommended to set a maximum file size limit for uploaded files:</p><pre><code class="language-csharp">var builder = WebApplication.CreateBuilder(args);

builder.Services.Configure&lt;FormOptions&gt;(options =&gt;
{
    options.MultipartBodyLengthLimit = 104857600; 
});</code></pre><p>With the property <code>MultipartBodyLengthLimit</code>, I set the maximum size of the multipart body to 100 MBs.</p><h2 id="allowing-formats-for-different-file-types">Allowing formats for different file types</h2><p>Each document type has a set of extensions: images are usually in JPG or PNG, and documents are usually in PDF or docx. For different purposes, we should validate the input. Like for a profile picture, users may not upload a docx or a PDF, while for a report, they should avoid JPG. To enforce it in our API, I will separate the image and document endpoints.</p><p><code>UploadImage</code> specified for uploading image files:</p><pre><code class="language-csharp">[HttpPost("UploadImage")]
public async Task&lt;IActionResult&gt; UploadImageAsync(IFormFile file)
{
    if (file == null || file.Length == 0)
        return BadRequest("No file uploaded.");

    var allowedExtensions = new[] { ".jpg", ".png" };
    var extension = Path.GetExtension(file.FileName);
    
    if (!allowedExtensions.Contains(extension))
    {
        var extensionsWithoutDots = allowedExtensions.Select(ext =&gt; ext.TrimStart('.'));
        return BadRequest($"Invalid file type. Allowed types are: {string.Join(", ", extensionsWithoutDots)}");
    }
    
    var uploadsFolder = Path.Combine(Directory.GetCurrentDirectory(), "Uploads");

    if (!Directory.Exists(uploadsFolder))
        Directory.CreateDirectory(uploadsFolder);

    var filePath = Path.Combine(uploadsFolder, file.FileName);

    using (var stream = new FileStream(filePath, FileMode.Create))
    {
        await file.CopyToAsync(stream);
    }

    return Ok(new { file.FileName, file.Length });
}</code></pre><p><code>UploadDocument</code> method for uploading document files:</p><pre><code class="language-csharp">
    [HttpPost("UploadDocument")]
    public async Task&lt;IActionResult&gt; UploadDocumentAsync(IFormFile file)
    {
        if (file == null || file.Length == 0)
            return BadRequest("No file uploaded.");

        var allowedExtensions = new[] { ".pdf", ".txt" };
        var extension = Path.GetExtension(file.FileName);
        
        if (!allowedExtensions.Contains(extension))
        {
            var extensionsWithoutDots = allowedExtensions.Select(ext =&gt; ext.TrimStart('.'));
            return BadRequest($"Invalid file type. Allowed types are: {string.Join(", ", extensionsWithoutDots)}");
        }
        
        var uploadsFolder = Path.Combine(Directory.GetCurrentDirectory(), "Uploads");
    
        if (!Directory.Exists(uploadsFolder))
            Directory.CreateDirectory(uploadsFolder);
    
        var filePath = Path.Combine(uploadsFolder, file.FileName);
    
        using (var stream = new FileStream(filePath, FileMode.Create))
        {
            await file.CopyToAsync(stream);
        }
    
        return Ok(new { file.FileName, file.Length });
    }</code></pre><p>The prior method only supports JPG and PNG, while the latter supports PDF and TXT. Lets test it from SwaggerUI:</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/05/image-5.png" class="kg-image" alt="API" loading="lazy" width="1312" height="550" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/05/image-5.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/05/image-5.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/05/image-5.png 1312w" 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/05/image-6.png" class="kg-image" alt="API" loading="lazy" width="1297" height="346" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/05/image-6.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/05/image-6.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/05/image-6.png 1297w" sizes="(min-width: 720px) 720px"></figure><p>On a correct input:</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/05/image-7.png" class="kg-image" alt="Upload png file" loading="lazy" width="1332" height="550" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/05/image-7.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/05/image-7.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/05/image-7.png 1332w" 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/05/image-8.png" class="kg-image" alt="Result" loading="lazy" width="1275" height="436" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/05/image-8.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/05/image-8.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/05/image-8.png 1275w" sizes="(min-width: 720px) 720px"></figure><p>If we inspect the Uploads folder, we can see the uploaded file:</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/05/image-9.png" class="kg-image" alt="Result" loading="lazy" width="311" height="52"></figure><p>While the other method:</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/05/image-10.png" class="kg-image" alt="Upload invalid file" loading="lazy" width="1335" height="547" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/05/image-10.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/05/image-10.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/05/image-10.png 1335w" 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/05/image-11.png" class="kg-image" alt="Bad request" loading="lazy" width="1288" height="344" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/05/image-11.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/05/image-11.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/05/image-11.png 1288w" sizes="(min-width: 720px) 720px"></figure><p>And for the allowed document:</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/05/image-12.png" class="kg-image" alt="Upload valid document" loading="lazy" width="1320" height="551" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/05/image-12.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/05/image-12.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/05/image-12.png 1320w" 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/05/image-13.png" class="kg-image" alt="Upload result" loading="lazy" width="1282" height="434" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/05/image-13.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/05/image-13.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/05/image-13.png 1282w" 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/05/image-14.png" class="kg-image" alt="Result" loading="lazy" width="259" height="72"></figure><h2 id="renaming-the-file-to-standard-naming">Renaming the file to standard naming</h2><p>We have now successfully uploaded files. However, there is a problem: you may notice that file names are arbitrary and random.</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/05/image-15.png" class="kg-image" alt="Uploads" loading="lazy" width="295" height="130"></figure><p>For real applications, we often want to control the names of the files to avoid someone uploading a Snapchat image of themselves with a weird-looking filename. Let's fix that.</p><p><strong>Step 1: Add model</strong></p><p>First, introduce a model to input images. It will contain a type specifying what the file is for.</p><p>Add File type enum:</p><pre><code class="language-csharp">namespace fileUploadDemo.Models.Enums;

public enum FileTypeEnum
{
    ProfilePicture = 1,
    PostImage = 2,
    Resume = 3
}</code></pre><p>File input:</p><pre><code class="language-csharp">using fileUploadDemo.Models.Enums;

namespace fileUploadDemo.Models.Dtos;

public class FileDtoInp
{
    public FileTypeEnum Type { get; set; }
    public IFormFile File { get; set; }
}</code></pre><p>To return, we will use a model instead of an anonymous object:</p><pre><code class="language-csharp">namespace fileUploadDemo.Models.Dtos;

public class FileDto
{
    public string FileName { get; set; } = string.Empty;
    public long Length { get; set; } 
}</code></pre><p><strong>Step 2: Add service layer</strong></p><p>So far, I have added everything to the controller, which is a <a href="https://blog.elmah.io/designing-business-rules-that-dont-leak-into-controllers/" rel="noreferrer">bad practice</a>. I'll add a service layer, but this can be implemented in whatever way you prefer.</p><p>Add a <code>IFileService</code> interface:</p><pre><code class="language-csharp">using fileUploadDemo.Models.Dtos;

namespace fileUploadDemo.Services.IServices;

public interface IFileService
{
    Task&lt;FileDto&gt; UploadImageAsync(FileDtoInp input);
    Task&lt;FileDto&gt; UploadDocumentAsync(FileDtoInp input);
}</code></pre><p>And a <code>FileService</code> implementation:</p><pre><code class="language-csharp">using fileUploadDemo.Models.Dtos;
using fileUploadDemo.Models.Enums;
using fileUploadDemo.Services.IServices;

namespace fileUploadDemo.Services;

public class FileService: IFileService
{
    private readonly string _uploadsFolder;

    public FileService(IWebHostEnvironment env)
    {
        _uploadsFolder = Path.Combine(env.ContentRootPath, "Uploads");
    }
    public async Task&lt;FileDto&gt; UploadImageAsync(FileDtoInp input)
    {
        if (input.File == null || input.File.Length == 0)
            throw new Exception("No file uploaded.");
        
        var allowedExtensions = new[] { ".jpg", ".png" };
        var extension = Path.GetExtension(input.File.FileName);
        
        if (!allowedExtensions.Contains(extension))
        {
            var extensionsWithoutDots = allowedExtensions.Select(ext =&gt; ext.TrimStart('.'));
            throw new Exception($"Invalid file type. Allowed types are: {string.Join(", ", extensionsWithoutDots)}");
        }
        
        if (!Directory.Exists(_uploadsFolder))
            Directory.CreateDirectory(_uploadsFolder);

        var fileName = $"{ GetFileName(input.Type) }{extension}";
        
        var filePath = Path.Combine(_uploadsFolder, fileName);
    
        using (var stream = new FileStream(filePath, FileMode.Create))
        {
            await input.File.CopyToAsync(stream);
        }

        return new 
            FileDto()
            {
                FileName = fileName, 
                Length = input.File.Length
            };
    }

    public async Task&lt;FileDto&gt; UploadDocumentAsync(FileDtoInp input)
    {
        if (input.File == null || input.File.Length == 0)
            throw new Exception("No file uploaded.");

        var allowedExtensions = new[] { ".pdf", ".txt" };
        var extension = Path.GetExtension(input.File.FileName);
        
        if (!allowedExtensions.Contains(extension))
        {
            var extensionsWithoutDots = allowedExtensions.Select(ext =&gt; ext.TrimStart('.'));
            throw new Exception($"Invalid file type. Allowed types are: {string.Join(", ", extensionsWithoutDots)}");
        }
        
        if (!Directory.Exists(_uploadsFolder))
            Directory.CreateDirectory(_uploadsFolder);
    
        var fileName = $"{ GetFileName(input.Type) }{extension}";
        var filePath = Path.Combine(_uploadsFolder, fileName);
    
        using (var stream = new FileStream(filePath, FileMode.Create))
        {
            await input.File.CopyToAsync(stream);
        }

        return new 
            FileDto()
            {
                FileName = fileName, 
                Length = input.File.Length
            };
    }
    
    private string GetFileName(FileTypeEnum input)
        =&gt; input switch
        {
            FileTypeEnum.ProfilePicture =&gt; $"PRF-{DateTime.UtcNow:yyMMddHHmmss}",
            FileTypeEnum.PostImage =&gt; $"PST-{DateTime.UtcNow:yyMMddHHmmss}",
            FileTypeEnum.Resume =&gt; $"RSM-{DateTime.UtcNow:yyMMddHHmmss}",
            _ =&gt; throw new ArgumentOutOfRangeException(nameof(input), input, null)
        };
}</code></pre><p>Let's understand what is happening here. We have methods for uploading images and documents, which first validate the file types. They actually abstracted the logic from the earlier upgrade of our application from controllers to services, <a href="https://blog.elmah.io/designing-business-rules-that-dont-leak-into-controllers/" rel="noreferrer">which is always recommended</a>. The main update here is the <code>GetFileName</code> method that returns a standardized name for each type. To make it collision-proof, I concatenated the current datetime into the name. Besides, the upload directory address is initialized in the constructor and used throughout the service. An even better way is to use it in appsettings.json and use the option pattern. Lets keep it simple for now. At last, I returned the FileDto model as output. You may also want to switch to not using exceptions for input validation, but I will leave that part up to you.</p><p><strong>Step 3: Register the new service</strong></p><pre><code class="language-csharp">builder.Services.AddScoped&lt;IFileService, FileService&gt;();</code></pre><p>In <code>Program.cs</code>I have registered the service dependency.</p><p><strong>Step 4: Update controller</strong></p><p>Now, the controller will inject and use the <code>IFileService</code> we just created: </p><pre><code class="language-csharp">using fileUploadDemo.Models.Dtos;
using fileUploadDemo.Services.IServices;
using Microsoft.AspNetCore.Mvc;

namespace fileUploadDemo.Controllers;

[ApiController]
[Route("api/[controller]")]
public class FileController: ControllerBase
{
    private readonly IFileService _fileService;

    public FileController(IFileService fileService)
    {
        _fileService = fileService;
    }
    
    [HttpPost("UploadImage")]
    public async Task&lt;IActionResult&gt; UploadImageAsync([FromForm] FileDtoInp input)
    {
        var result = await _fileService.UploadImageAsync(input);
        return Ok(result);
    }
    
    [HttpPost("UploadDocument")]
    public async Task&lt;IActionResult&gt; UploadDocumentAsync([FromForm] FileDtoInp input)
    {
        var result = await _fileService.UploadDocumentAsync(input);
        return Ok(result);
    }
}</code></pre><p><code>[FromForm]</code> specifies the input as form-data, not a JSON body, ensuring the endpoint receives files.</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/2026/05/image-23.png" class="kg-image" alt="API" loading="lazy" width="1334" height="516" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/05/image-23.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/05/image-23.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/05/image-23.png 1334w" 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/05/image-21.png" class="kg-image" alt="Result" loading="lazy" width="1272" height="310" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/05/image-21.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/05/image-21.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/05/image-21.png 1272w" sizes="(min-width: 720px) 720px"></figure><p>Ensure the file is named according to our standard, regardless of its original name. </p><p>The same happens with the documents:</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/05/image-24.png" class="kg-image" alt="API" loading="lazy" width="1320" height="463" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/05/image-24.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/05/image-24.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/05/image-24.png 1320w" 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/05/image-20.png" class="kg-image" alt="Result" loading="lazy" width="1290" height="434" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/05/image-20.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/05/image-20.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/05/image-20.png 1290w" 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/05/image-22.png" class="kg-image" alt="Uploaded files" loading="lazy" width="248" height="126"></figure><p>Apart from naming files with date and time, using a unique GUID or the username is also a common practice. Consider the following ways:</p><pre><code class="language-csharp">var fileName = $"{ Guid.NewGuid().ToString() }{extension}";</code></pre><p>or:</p><pre><code class="language-csharp">var fileName = $"{ _userContext.Username }{extension}";</code></pre><h2 id="creating-a-get-endpoint-to-fetch-a-file">Creating a GET endpoint to fetch a file</h2><p>We have covered several ways to upload a file in an ASP.NET Core API. Next requirement naturally arises: how to fetch the saved file? Let's design an endpoint for it.</p><p>A new model to return the file from the service to the controller could be implemented like this:</p><pre><code class="language-csharp">namespace fileUploadDemo.Models.Dtos;

public class FileResultDto
{
    public byte[] Content { get; set; } = default!;
    public string ContentType { get; set; } = string.Empty;
    public string FileName { get; set; } = string.Empty;
}</code></pre><p>Our service implementation should include a new method for fetching a file:</p><pre><code class="language-csharp">public async Task&lt;FileResultDto?&gt; GetFileAsync(string key)
{
    if (string.IsNullOrWhiteSpace(key) || key.Contains(".."))
        return null;

    var filePath = Path.Combine(_uploadsFolder, key);

    if (!System.IO.File.Exists(filePath))
        return null;

    var extension = Path.GetExtension(filePath).ToLower();

    var contentType = extension switch
    {
        ".jpg" or ".jpeg" =&gt; "image/jpeg",
        ".png" =&gt; "image/png",
        ".pdf" =&gt; "application/pdf",
        _ =&gt; "application/octet-stream"
    };

    var bytes = await System.IO.File.ReadAllBytesAsync(filePath);

    return new FileResultDto
    {
        Content = bytes,
        ContentType = contentType,
        FileName = key
    };
}</code></pre><p>After some initial string checks and combining the filename with the directory, we check whether the file exists. In the latter part, we convert the file into a MIME type based on the extension. <code>var bytes = await System.IO.File.ReadAllBytesAsync(filePath);</code> loads the entire file into memory and returns <code>byte[]</code>. If large files are uploaded, consider streaming instead. Finally, it packs the content and metadata into the response model and returns it.</p><p>Use it in the controller:</p><pre><code class="language-csharp">[HttpGet("GetFile")]
public async Task&lt;IActionResult&gt; GetFileAsync([FromQuery] string key)
{
    var result = await _fileService.GetFileAsync(key);

    if (result == null)
        return NotFound("File not found");

    return File(result.Content, result.ContentType, result.FileName);
}</code></pre><p><code>[FromQuery]</code> will prompt for the filename as a key param and pass it to the service. Once the data is received, the endpoint will return the actual file.</p><p><strong>Test</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/05/image-25.png" class="kg-image" alt="API" loading="lazy" width="1321" height="409" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/05/image-25.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/05/image-25.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/05/image-25.png 1321w" 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/05/image-26.png" class="kg-image" alt="Result with a Download file link" loading="lazy" width="599" height="230"></figure><p>Simply, the user can download the file.</p><h2 id="tips-for-using-file-uploading-in-aspnet-core">Tips for using file uploading in ASP.NET Core</h2><p>For production environments and scalable systems, consider the following aspects.</p><ul><li>For the production environment, use a cloud service such as Azure Blob or AWS S3 to save files for better reliability and speed.</li><li>Use the path in appsettings for security and good design.</li><li>Organize upload paths into separate directories for document- and module-wise uploads. Even separating directories for users is also a common practice, such as "images/Profile", "images/Posts", "documents/Resumes" or "images/john/".</li><li>Store metadata in a database for referencing and persistence. Creating an image-and-document table with a Type key is a great way to reference files to corresponding entities, such as User and Building, via ImageId and DocumentId. The tables can store other important information, such as file size, creation date, uploader user, file name, etc.</li><li>If your system requires a very large file upload, use chunk uploading.</li><li>Use file streaming for large file instead of buffer that can result in high RAM usage.</li><li>Enable progress tracking via SignalR.</li></ul><h2 id="conclusion">Conclusion</h2><p>The long story comes to an end, and we learned how to handle files in ASP.NET Core APIs. We started with minimal code and applied best practices when using images and documents. By following these tips, you can design a file structure securely and efficiently in a production-grade system.</p> ]]></content:encoded>
    </item>
    <item>
        <title><![CDATA[ The complete guide to mastering Dapper micro-ORM in .NET ]]></title>
        <description><![CDATA[ For developers who want to taste ORM but don&#39;t want to leave SQL either, Dapper is a perfect choice. Dapper runs SQL queries like ADO.NET but returns results as C# objects, like Entity Framework Core. Apart from its abstracting nature, you leverage high-speed data access. The feature ]]></description>
        <link>https://blog.elmah.io/the-complete-guide-to-mastering-dapper-micro-orm-in-net/</link>
        <guid isPermaLink="false">69e4b06542b0770001f2aad6</guid>
        <category><![CDATA[  ]]></category>
        <dc:creator><![CDATA[ Ali Hamza Ansari ]]></dc:creator>
        <pubDate>Wed, 13 May 2026 08:34:36 +0200</pubDate>
        <media:content url="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/05/the-complete-guide-to-mastering-dapper-micro-orm-in-dot.net-o-2.png" medium="image"/>
        <content:encoded><![CDATA[ <p>This blog post is originally published on <a href="https://blog.elmah.io/the-complete-guide-to-mastering-dapper-micro-orm-in-net/">https://blog.elmah.io/the-complete-guide-to-mastering-dapper-micro-orm-in-net/</a></p> 
<!--kg-card-begin: html-->
<div class="toc"></div>
<!--kg-card-end: html-->
<p>For developers who want to taste ORM but don't want to leave SQL either, Dapper is a perfect choice. Dapper runs SQL queries like ADO.NET but returns results as C# objects, like Entity Framework Core. Apart from its abstracting nature, you leverage high-speed data access. The feature set of Dapper is quite large and covers key areas of database work, such as JOINs, aggregate functions, database procedures and functions, transactions, etc. In today's post, I will walk through everything needed to get you started with Dapper.</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/05/the-complete-guide-to-mastering-dapper-micro-orm-in-dot.net-o-3.png" class="kg-image" alt="The complete guide to mastering Dapper micro-ORM in .NET" loading="lazy" width="1500" height="750" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/05/the-complete-guide-to-mastering-dapper-micro-orm-in-dot.net-o-3.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/05/the-complete-guide-to-mastering-dapper-micro-orm-in-dot.net-o-3.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/05/the-complete-guide-to-mastering-dapper-micro-orm-in-dot.net-o-3.png 1500w" sizes="(min-width: 1200px) 1200px"></figure><h2 id="what-is-dapper">What is Dapper?</h2><p>Dapper is a micro-ORM (Object Relational Mapper) for .NET. Developed by Stack Overflow, Dapper offers higher performance database operations faster than full ORMs like Entity Framework Core (<a href="https://blog.elmah.io/new-in-net-10-and-c-14-ef-core-10s-faster-production-queries/" rel="noreferrer">EF Core</a>). It suits developers who work with ADO.NET because of its query commands, unlike EF Core, which operates on objects rather than SQL. SQL Queries are directly mapped to strongly typed objects like any ORM.</p><h2 id="why-use-dapper">Why Use Dapper?</h2><p>As Dapper is a micro-ORM that offers a thin abstraction between the database and applications, it introduces very little overhead. For these reasons, it is an optimal choice for high-performing scenarios. Similar to ADO .NET, you can get full control over SQL queries. You can think of it as the middle ground between ADO.NET and an ORM like EF Core, which uses data as strongly typed C# objects. One can <a href="https://blog.elmah.io/visualizing-linq-queries-with-linqpad-boost-your-ef-core-debugging/" rel="noreferrer">view the SQL generated by EF Core</a>, but can't granularly control it. Dapper uses minimal dependencies, keeping the architecture lightweight. That is one of the reasons Dapper best suits microservices, financial systems, web APIs, and modular APIs. </p><h2 id="commonly-used-dapper-methods">Commonly used Dapper methods</h2><p>To summarize the most commonly used Dapper methods, let's go ahead and create a new project.</p><p><strong>Step 1: Create a project</strong></p><pre><code class="language-console">dotnet new console -n UserDapperDemo
cd UserDapperDemo</code></pre><p><strong>Step 2: Install the required packages</strong></p><pre><code class="language-console">dotnet add package Dapper
dotnet add package Npgsql
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.Json</code></pre><p><code>Npgsql</code> provides a <code>NpgsqlConnection</code> class for connecting to PostgreSQL. The Configuration* packages are useful when we create and include <code>appsettings.json</code> in the project.</p><p><strong>Step 3: Create models</strong></p><pre><code class="language-csharp">namespace UserDapperDemo.Models;

public class Movie
{
    public int Id { get; set; }
    public string Title { get; set; } = string.Empty;
    public int DirectorId { get; set; }
    public double Rating { get; set; }
}</code></pre><pre><code class="language-csharp">namespace UserDapperDemo.Models;

public class Director
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
}</code></pre><p><strong>Step 4: Add <code>appsettings.json</code> and its configuration</strong></p><pre><code class="language-json">{
  "ConnectionStrings": {
    "PostgresConnection": "Host=localhost;Port=5432;Database=movieDb;Username=postgres;Password=1234"
  }
}</code></pre><p>In the <code>.proj</code> file add the following item group:</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 5: Configure connection factory</strong></p><pre><code class="language-csharp">using System.Data;
using Microsoft.Extensions.Configuration;
using Npgsql;

namespace UserDapperDemo.Data;

public class DbConnectionFactory
{
    
    private readonly string _connectionString;

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

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

    public IDbConnection CreateConnection()
        =&gt; new NpgsqlConnection(_connectionString);
}</code></pre><p>It makes a centralized database connection point.</p><p><strong>Step 6: Prepare the database</strong></p><p>Run the following query for the database</p><pre><code class="language-SQL">CREATE TABLE IF NOT EXISTS public."Director"
(
    "Id" integer NOT NULL DEFAULT nextval('"Director_Id_seq"'::regclass),
    "Name" text COLLATE pg_catalog."default" NOT NULL,
    CONSTRAINT "Director_pkey" PRIMARY KEY ("Id")
)
CREATE TABLE IF NOT EXISTS public."Movie"
(
    "Id" integer NOT NULL DEFAULT nextval('"Movie_Id_seq"'::regclass),
    "Title" text COLLATE pg_catalog."default" NOT NULL,
    "DirectorId" integer,
    "Rating" numeric,
    CONSTRAINT "Movie_pkey" PRIMARY KEY ("Id"),
    CONSTRAINT "Movie_DirectorId_fkey" FOREIGN KEY ("DirectorId")
        REFERENCES public."Director" ("Id") MATCH SIMPLE
        ON UPDATE NO ACTION
        ON DELETE NO ACTION
)
</code></pre><p>So, our database tables 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/04/image-12.png" class="kg-image" alt="Tables" loading="lazy" width="185" height="72"></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/04/image-14.png" class="kg-image" alt="Database" loading="lazy" width="433" height="265"></figure><p><strong>Step 7: Implement Dapper methods</strong></p><p>I will follow a repository pattern to test some of Dapper's most common methods. This is just for the sake of demoing common methods. Whether or not you want to implement repositories in your code is entirely up to you. Some people love repositories while others absolutely hate them. </p><p>The movie repository will be:</p><pre><code class="language-csharp">public class MovieRepo
{
    private readonly DbConnectionFactory _factory;
    public MovieRepo(DbConnectionFactory factory)
    {
        _factory = factory;
    }
    
    //methods
}
    </code></pre><h3 id="queryasynct">QueryAsync&lt;T&gt;()</h3><pre><code class="language-csharp">public async Task&lt;IEnumerable&lt;Movie&gt;&gt; GetAllMoviesAsync()
{
    using var connection = _factory.CreateConnection();

    return await connection.QueryAsync&lt;Movie&gt;(
        "SELECT * FROM \"Movie\""
    );</code></pre><p><code>QueryAsync</code> returns multiple rows of data asynchronously. It is used when we need to fetch multiple rows of data. Its synchronous counterpart is <code>Query&lt;&gt;()</code>.</p><h3 id="queryfirstasynct">QueryFirstAsync&lt;T&gt;()</h3><pre><code class="language-csharp">public async Task&lt;Movie&gt; GetFirstMovie()
{
    using var connection = _factory.CreateConnection();

    return await connection.QueryFirstAsync&lt;Movie&gt;(
        "SELECT * FROM \"Movie\" ORDER BY \"Id\" LIMIT 1"
    );
}
</code></pre><p>A single row-returning operation that returns the first row that matches the given condition. It expects the record to exist, otherwise, it throws an exception. <code>QueryFirst&lt;T&gt;()</code> is for synchronous operation. </p><h3 id="queryfirstordefaultasynct">QueryFirstOrDefaultAsync&lt;T&gt;()</h3><pre><code class="language-csharp">public async Task&lt;Movie?&gt; GetMovieByTitle(string title)
{
    using var connection = _factory.CreateConnection();

    return await connection.QueryFirstOrDefaultAsync&lt;Movie&gt;(
        "SELECT * FROM \"Movie\" WHERE \"Title\" = @Title",
        new { Title = title }
    );
}</code></pre><p>As the name suggests, it returns the first row that matches the condition, or otherwise returns the default value. A similar sync method is <code>QueryFirstOrDefault</code>.</p><h3 id="querysingleasynct">QuerySingleAsync&lt;T&gt;()</h3><pre><code class="language-csharp">public  async Task&lt;Movie&gt; GetMovieById(int id)
{
    using var connection = _factory.CreateConnection();

    return await connection.QuerySingleAsync&lt;Movie&gt;(
        "SELECT * FROM \"Movie\" WHERE \"Id\" = @Id",
        new { Id = id }
    );
}</code></pre><p><code>QuerySingleAsync</code> or <code>QuerySingle</code> (sync version) expects exactly one row to match the condition. If fewer or more are found, it throws an exception. <code>QuerySingle</code> is ideal when filtering a record by ID, as ID does not duplicate.</p><h3 id="querysingleordefaultasynct">QuerySingleOrDefaultAsync&lt;T&gt;()</h3><pre><code class="language-csharp">public  async Task&lt;Movie?&gt; GetMovieSafe(int id)
{
    using var connection = _factory.CreateConnection();

    return await connection.QuerySingleOrDefaultAsync&lt;Movie&gt;(
        "SELECT * FROM \"Movie\" WHERE \"Id\" = @Id",
        new { Id = id }
    );
}</code></pre><p>Returns one record matched or null. Use <code>QuerySingleOrDefault</code> if you want non-async. </p><h3 id="executeasync">ExecuteAsync</h3><pre><code class="language-csharp">public  async Task&lt;int&gt; InsertMovie(Movie movie)
{
    using var connection = _factory.CreateConnection();

    return await connection.ExecuteAsync(
        @"INSERT INTO ""Movie"" (""Title"", ""director_id"", ""rating"")
  VALUES (@Title, @DirectorId, @Rating)",
        movie
    );
}</code></pre><pre><code class="language-csharp">public async Task&lt;int&gt; UpdateMovieAsync(Movie movie)
{
    using var connection = _factory.CreateConnection();

    return await connection.ExecuteAsync(
        @"UPDATE ""Movie"" 
      SET ""Title"" = @Title, ""Rating"" = @Rating
      WHERE ""Id"" = @Id",
        movie
    );
}</code></pre><p><code>ExecuteAsync</code> runs the command and returns the number of rows affected. Specifically, this and its Execute methods are used for Insert, Update, and Delete operations. </p><h3 id="executescalarasync">ExecuteScalarAsync</h3><pre><code class="language-csharp">public async Task&lt;int&gt; GetMovieCount()
{
    using var connection = _factory.CreateConnection();

    return await connection.ExecuteScalarAsync&lt;int&gt;(
        "SELECT COUNT(*) FROM \"Movie\""
    );
}</code></pre><p><code>ExecuteScalarAsync</code> or <code>ExecuteScalar</code> returns a single value as a result of SQL aggregation functions such as <code>COUNT()</code>, <code>AVG()</code>, and <code>SUM()</code>. </p><h3 id="querymultipleasync">QueryMultipleAsync</h3><pre><code class="language-csharp">public async Task&lt;(IEnumerable&lt;Movie&gt;, IEnumerable&lt;Director&gt;)&gt; GetDashboard()
{
    using var connection = _factory.CreateConnection();

    var sql = @"
        SELECT * FROM ""Movie"";
        SELECT * FROM ""Director"";
    ";

    using var multi = await connection.QueryMultipleAsync(sql);

    var movies = multi.Read&lt;Movie&gt;();
    var directors = multi.Read&lt;Director&gt;();

    return (movies, directors);
}</code></pre><p>The method is complex, running multiple commands from a single command. <code>QueryMultiple</code> is the sync version of it. Instead of hitting the database multiple times, <code>QueryMultiple</code> lets you fetch multiple datasets in a single round-trip.</p><p><strong>Step 8: Set up <code>Program.cs</code></strong></p><pre><code class="language-csharp">using UserDapperDemo.Models;
using UserDapperDemo.Data;
using UserDapperDemo.Data.Repos;

var factory = new DbConnectionFactory();

var repo = new MovieRepo(factory);

// INSERT
repo.InsertMovie(new Movie
{
    Title = "Inception",
    DirectorId = 1,
    Rating = 9.0
});

Console.WriteLine("=== ALL MOVIES ===");
// QUERY
var movies = await repo.GetAllMoviesAsync();

foreach (var m in movies)
{
    Console.WriteLine($"Id: {m.Id}, Title: {m.Title}, Rating: {m.Rating}");
}

Console.WriteLine("\n=== SINGLE MOVIE (QueryFirst) ===");
// SINGLE
var movie1 = await repo.GetFirstMovie();

Console.WriteLine($"Id: {movie1.Id}, Title: {movie1.Title}, Rating: {movie1.Rating}");

Console.WriteLine("\n=== SINGLE MOVIE (QueryFirstOrDefault) ===");
// SINGLE
var movie2 = await repo.GetMovieByTitle("Memento");

Console.WriteLine($"Id: {movie2.Id}, Title: {movie2.Title}, Rating: {movie2.Rating}");

Console.WriteLine("\n=== SINGLE MOVIE (QuerySingle) ===");
// SINGLE
var movie3 = await repo.GetMovieById(1);

Console.WriteLine($"Id: {movie3.Id}, Title: {movie3.Title}, Rating: {movie3.Rating}");

Console.WriteLine("\n=== SINGLE MOVIE (QuerySingleOrDefault) ===");
// SINGLE
var movie4 = await repo.GetMovieSafe(1);

Console.WriteLine($"Id: {movie4.Id}, Title: {movie4.Title}, Rating: {movie4.Rating}");

Console.WriteLine("\n=== MOVIE COUNT (ExecuteScalar) ===");
// COUNT
var count = await repo.GetMovieCount();
Console.WriteLine($"Total Movies: {count}");

Console.WriteLine("\n=== DASHBOARD (QueryMultiple) ===");
// MULTIPLE
var (allMovies, directors) = await repo.GetDashboard();
foreach (var m in allMovies)
{
    Console.WriteLine($"Id: {m.Id}, Title: {m.Title}, Rating: {m.Rating}");
}

foreach (var d in directors)
{
    Console.WriteLine($"Id: {d.Id}, Name: {d.Name}");
}</code></pre><p>Here we are using <code>movieRepo</code> to call all of its methods.</p><p><strong>Step 9: 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/2026/04/image-15.png" class="kg-image" alt="Result" loading="lazy" width="584" height="322"></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/04/image-16.png" class="kg-image" alt="Result" loading="lazy" width="594" height="502"></figure><h2 id="advanced-dapper-features">Advanced Dapper Features</h2><p>We have seen some of the most common dapper methods till now. However, Dapper offers ways to tackle them as well for complex scenarios.</p><h3 id="working-with-transactions">Working with transactions</h3><p>When inserting or updating data, an exception in between the operation can leave the database inconsistent. <a href="https://blog.elmah.io/3-essential-techniques-for-managing-transactions-in-ef-core/" rel="noreferrer">Transactions </a>rescue and atomize the data manipulation operations as one, either the whole block executes or fails and rolls back. Let's see how we can do it in Dapper.</p><pre><code class="language-csharp">public async Task CreateMovieWithDirector(Movie movie, string directorName)
{
    using var connection = _factory.CreateConnection();
    connection.Open();

    using var transaction = connection.BeginTransaction();

    try
    {
        var directorId = await connection.ExecuteScalarAsync&lt;int&gt;(
            @"INSERT INTO ""Director"" (""Name"")
          VALUES (@Name)
          RETURNING ""Id"";",
            new { Name = directorName },
            transaction
        );

        movie.DirectorId = directorId;

        await connection.ExecuteAsync(
            @"INSERT INTO ""Movie"" (""Title"", ""DirectorId"", ""Rating"")
          VALUES (@Title, @DirectorId, @Rating)",
            movie,
            transaction
        );

        transaction.Commit();
    }
    catch
    {
        transaction.Rollback();
        throw;
    }
}
</code></pre><p>I opened a transaction on the connection, then we first created the director before the movie, as the movie depends on the <code>directorId</code>. At last <code>transaction.Commit()</code> commits the transaction to write everything in the database, while the catch block rolls back the transaction using <code>transaction.Rollback()</code>. Note that I am using a parameterized query to keep the database safe from SQL injection attacks and unwanted data. </p><p>In <code>Program.cs</code>:</p><pre><code class="language-csharp">Console.WriteLine("\n=== Transaction ===");
await repo.CreateMovieWithDirector(
    new Movie()
    {
        Title = null,
        Rating = 9,
    },
    "Martin Scorsese"
);
</code></pre><p>I intentionally kept the title null, as per the repo method, the director will be added successfully, but during Movie creation, there is an error. The transaction will roll back the director insertion when the second half fails as well.</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/04/image-18-1.png" class="kg-image" alt="Exception" loading="lazy" width="807" height="404" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/04/image-18-1.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/04/image-18-1.png 807w" sizes="(min-width: 720px) 720px"></figure><p>Upon correct values it will be done:</p><pre><code class="language-csharp">new Movie()
    {
        Title = "The Departed",
        Rating = 9
    },</code></pre><h3 id="using-stored-proceduresfunctions">Using Stored Procedures/Functions</h3><p>Stored procedures and functions encapsulate complex logic and provide reusable calls. For such detailed operations, you should not write a raw query spanning dozens of lines in the code that can be difficult to maintain and organize. The best way is to abstract into functions or procedures. Let's consider a function</p><pre><code class="language-SQL">CREATE FUNCTION public.get_movies_by_director(
	p_director_id integer)
    RETURNS TABLE("Id" integer, "Title" text, "Rating" numeric) 
    LANGUAGE 'plpgsql'
    COST 100
    VOLATILE PARALLEL UNSAFE
    ROWS 1000

AS $BODY$
BEGIN
    RETURN QUERY
    SELECT m."Id", m."Title", m."Rating"
    FROM "Movie" m
    WHERE m."DirectorId" = p_director_id;
END;
$BODY$;</code></pre><p>repo method that calls the function</p><pre><code class="language-csharp">public async Task&lt;IEnumerable&lt;Movie&gt;&gt; GetMoviesByDirector(int directorId)
{
    using var connection = _factory.CreateConnection();

    return await connection.QueryAsync&lt;Movie&gt;(
        @"SELECT * FROM get_movies_by_director(@DirectorId)",
        new { DirectorId = directorId }
    );
}</code></pre><pre><code class="language-csharp">var moviesByDirector = await repo.GetMoviesByDirector(1);

foreach (var m in moviesByDirector)
{
    Console.WriteLine($"{m.Id} - {m.Title} - {m.Rating}");
}</code></pre><p> 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/04/image-19.png" class="kg-image" alt="Result" loading="lazy" width="221" height="38"></figure><h3 id="bulk-operations-with-dapper">Bulk Operations with Dapper</h3><p>Bulk operations are important features supported by Dapper. When inserting a large amount of data, you do not want to hit the database separately for each record, nor should you. SQL allows bulk insertion or update at once, and you can run the query with <code>ExecuteAsync</code>.</p><pre><code class="language-csharp">public async Task BulkInsertMovies(IEnumerable&lt;Movie&gt; movies)
{
    using var connection = _factory.CreateConnection();

    await connection.ExecuteAsync(
        @"INSERT INTO ""Movie"" (""Title"", ""DirectorId"", ""Rating"")
          VALUES (@Title, @DirectorId, @Rating)",
        movies
    );
}</code></pre><p><code>Program.cs</code> call</p><pre><code class="language-csharp">await repo.BulkInsertMovies(new List&lt;Movie&gt;
{
    new Movie { Title = "Batman Begins", DirectorId = 1, Rating = 8.2 },
    new Movie { Title = "The Dark Knight", DirectorId = 1, Rating = 9.0 }
});</code></pre><h3 id="opt-between-buffered-and-unbuffered-query">Opt between Buffered and Unbuffered Query </h3><p>By default, Dapper loads everything into the buffer memory. The behavior can be problematic with large datasets, so choose unbuffered queries that stream results rather than storing them in memory. </p><pre><code class="language-csharp">var movies = await connection.QueryAsync&lt;Movie&gt;(
    sql,
    buffered: false
);</code></pre><h3 id="multi-mapping-handling-join-queries">Multi-Mapping (Handling JOIN Queries)</h3><p>JOINs are a common way to get data from multiple tables in SQL. That common feature is workable using Dapper as well. With the same <code>QueryAsync</code> you can run queries with JOIN commands. </p><pre><code class="language-csharp">public async Task&lt;IEnumerable&lt;MovieWithDirector&gt;&gt; GetMoviesWithDirectors()
{
    using var connection = _factory.CreateConnection();

    var sql = @"
    SELECT 
        m.""Id"", 
        m.""Title"", 
        m.""Rating"",
        d.""Name"" AS ""DirectorName""
    FROM ""Movie"" m
    JOIN ""Director"" d ON m.""DirectorId"" = d.""Id"";
";

    var result = await connection.QueryAsync&lt;MovieWithDirector&gt;(sql);

    return result;
}</code></pre><p>We need a model to get the results</p><pre><code class="language-csharp">namespace UserDapperDemo.Models;

public class MovieWithDirector
{
    public int Id { get; set; }
    public string Title { get; set; } = string.Empty;
    public double Rating { get; set; }
    public string DirectorName { get; set; } = string.Empty;
}</code></pre><p>Simply printing them all after calling the repo method</p><pre><code class="language-csharp">var moviesWithDirectors = await repo.GetMoviesWithDirectors();

foreach (var m in moviesWithDirectors)
{
    Console.WriteLine($"{m.Id} - {m.Title} - {m.Rating} by {m.DirectorName}");
}</code></pre><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/2026/04/image-20.png" class="kg-image" alt="Result" loading="lazy" width="590" height="184"></figure><h2 id="best-practices-when-using-dapper">Best Practices When Using Dapper</h2><p>Following some practices can make Dapper work better than usual. These ways make the application fast, maintainable, and secure.</p><h3 id="use-parameterized-queries">Use Parameterized Queries</h3><p>That is general advice to always use parameters to prevent SQL injection attacks. Apart from security, some special characters, such as "," and")", can cause errors when inserting or updating string values. </p><h3 id="prefer-async-methods">Prefer Async Methods</h3><p>To avoid thread block and support asynchronous, go with async versions like <code>QueryAsync</code> and <code>ExecuteAsync</code>. Applications usually run different threads in parallel, preferring async methods in Dapper. </p><h3 id="organize-sql-queries">Organize SQL Queries </h3><p>Good architecture makes a project maintainable. You should handle data access as well when using Dapper. <a href="https://blog.elmah.io/the-repository-pattern-is-simple-yet-misunderstood/" rel="noreferrer">Repository pattern</a> is a preferable choice however choose <a href="https://blog.elmah.io/repository-pattern-vs-specification-pattern-which-is-more-maintainable/" rel="noreferrer">as per your requirement</a>. Keeping long queries in files will also save mess in the code.</p><h3 id="manage-connections-efficiently">Manage Connections Efficiently</h3><p>Database connections are like portals to the database. Open and close them the right way with&nbsp;<code>using</code>&nbsp;statements or connection factories to ensure that connections open only once and are properly closed after each query.</p><h3 id="avoid-business-logic-in-the-sql-queries">Avoid Business logic in the SQL queries</h3><p>Validate the inputs and other business rules before passing them to SQL queries. This avoids unnecessary checks on SQL and keeps the single responsibility in the data access layer.on SQL and keep the single responsibility on the data access layer.</p><p>Avoid checks like </p><pre><code class="language-csharp">CASE WHEN rating &gt; 5 THEN 'Good' ELSE 'Bad'</code></pre><p>It is generally good to keep the repository or other data access separate from business rules.</p><h3 id="cache-frequently-accessed-data">Cache frequently-accessed data</h3><p>Database operations are always costly, try to reduce them as much as possible. Caching is one of the best ways to reduce database processing and delays. First, identify which data are most requested or don't change frequently. <a href="https://blog.elmah.io/caching-strategies-in-asp-net-core/" rel="noreferrer">Set up caching</a> and save them to provide quick access to users, rather than requesting the same data from the dataset every time.</p><p>You can follow some <a href="https://blog.elmah.io/how-net-handles-exceptions-internally-and-why-theyre-expensive/" rel="noreferrer">generic recommendations</a> for the application apart from Dapper.</p><h2 id="conclusion">Conclusion</h2><p>We rediscovered Dapper from its basic methods to its advanced features. Later, we reviewed some recommendations for using Dapper. Like any other feature, Dapper has its own world and definitions. I defined its commonly used methods and some of the advanced features. I shared a real example of how those methods and features address most database requirements. By following the tips in the article, you can scale up the use of the high-performance library.</p><p>Code: <a href="https://github.com/elmahio-blog/MovieDapperDemo.git">https://github.com/elmahio-blog/MovieDapperDemo.git</a></p> ]]></content:encoded>
    </item>
    <item>
        <title><![CDATA[ Pattern matching in C#: Advanced scenarios you didn&#x27;t know ]]></title>
        <description><![CDATA[ table of contents



Pattern matching is not just condition checking. It reflects how you think as a developer. Matching and validation can be achieved in a naive, descriptive way. However, a cleaner approach stands out in terms of readability and sometimes performance. Pattern matching combines patterns to express complex logic ]]></description>
        <link>https://blog.elmah.io/pattern-matching-in-c-advanced-scenarios-you-didnt-know/</link>
        <guid isPermaLink="false">69db60464515690001427afc</guid>
        <category><![CDATA[  ]]></category>
        <dc:creator><![CDATA[ Ali Hamza Ansari ]]></dc:creator>
        <pubDate>Tue, 05 May 2026 10:19:19 +0200</pubDate>
        <media:content url="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/04/pattern-matching-in-csharp-advanced-scenarios-you-didnt-know-o.png" medium="image"/>
        <content:encoded><![CDATA[ <p>This blog post is originally published on <a href="https://blog.elmah.io/pattern-matching-in-c-advanced-scenarios-you-didnt-know/">https://blog.elmah.io/pattern-matching-in-c-advanced-scenarios-you-didnt-know/</a></p> 
<!--kg-card-begin: html-->
<div class="toc">table of contents</div>
<!--kg-card-end: html-->
<p>Pattern matching is not just condition checking. It reflects how you think as a developer. Matching and validation can be achieved in a naive, descriptive way. However, a cleaner approach stands out in terms of readability and sometimes performance. Pattern matching combines patterns to express complex logic in a single, readable line. In today's post, I will cover some advanced pattern-matching solutions that developers often miss.</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/04/pattern-matching-in-csharp-advanced-scenarios-you-didnt-know-o-1.png" class="kg-image" alt="Pattern matching in C#: Advanced scenarios you didn’t know" loading="lazy" width="1500" height="750" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/04/pattern-matching-in-csharp-advanced-scenarios-you-didnt-know-o-1.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/04/pattern-matching-in-csharp-advanced-scenarios-you-didnt-know-o-1.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/04/pattern-matching-in-csharp-advanced-scenarios-you-didnt-know-o-1.png 1500w" sizes="(min-width: 1200px) 1200px"></figure><h2 id="implementation-of-pattern-matching-scenarios">Implementation of pattern-matching scenarios</h2><p>To show pattern matching in code, I will start by creating a console application to test out different examples.</p><p><strong>Step 1: Create the project</strong></p><pre><code class="language-console">dotnet new console -n PatternMatchingDemo
cd PatternMatchingDemo</code></pre><p><strong>Step 2: Create records</strong></p><p>For models, I will use records <a href="https://blog.elmah.io/exploring-c-records-and-their-use-cases/" rel="noreferrer">because we need value types</a> here. </p><pre><code class="language-csharp">namespace PatternMatchingDemo.Records;
public record Address(string City, string Country);
public record User(string Name, int Age, Address Address, List&lt;string&gt; Roles);
public record Request(string Source, int Priority);
public record Point(int X, int Y);</code></pre><p><strong>Step 3: Define collections</strong></p><p>To use pattern matching, we need a few collections. Defining an in-memory collection, while it will mimic any real data.</p><pre><code class="language-csharp">var users = new List&lt;User&gt;
{
    new("Ali", 25, new Address("Karachi", "Pakistan"), new List&lt;string&gt; { "Admin", "User" }),
    new("Sara", 17, new Address("Chicago", "United States"), new List&lt;string&gt; { "User" }),
    new("Kennedy", 65, new Address("London", "UK"), new List&lt;string&gt; { "Guest" })
};

var requests = new List&lt;Request&gt;
{
    new("System", 10),
    new("User", 3),
    new("System", 2)
};</code></pre><p><strong>Step 4: Use pattern matching for different requirements</strong></p><p>That's it for the setup. In the next sections, I will showcase pattern matching for different requirements and purposes.</p><h3 id="property-pattern-nested-matching">Property pattern (nested matching)</h3><pre><code class="language-csharp">foreach (var user in users)
{
    if (user is { Address.City: "Karachi" })
    {
        Console.WriteLine($"{user.Name} is from Karachi");
    }
}</code></pre><p>Property pattern provides a clean way to match an object's properties, even when they are nested. It helps in JSON matching and DTO validation without verbosity. A traditional way without a property pattern would be:</p><pre><code class="language-csharp">if (user != null &amp;&amp; user.Address != null &amp;&amp; user.Address.City == "Karachi" &amp;&amp; user.Age &gt; 18)</code></pre><h3 id="pattern-matching-with-not">Pattern matching with <code>not</code></h3><pre><code class="language-csharp">foreach (var user in users)
{
    if (user is not { Address.City: "Karachi" })
    {
        Console.WriteLine($"{user.Name} is NOT from Karachi");
    }
}</code></pre><p>This one is just the opposite of the previous pattern. It simply excludes the given condition and fetches all other records.</p><h3 id="matching-multiple-cases-in-one-pattern">Matching multiple cases in one pattern</h3><pre><code class="language-csharp">foreach (var user in users)
{
    if (user is { Address.City: "Karachi" or "Lahore" })
    {
        Console.WriteLine($"{user.Name} is from a major city");
    }
}</code></pre><p>The same property matching can be extended to multiple cases. Well, the pattern is very much descriptive itself, referring to what it actually does.</p><h3 id="pattern-matching-inside-linq">Pattern matching inside LINQ</h3><pre><code class="language-csharp">var adultsFromPakistan = users
    .Where(u =&gt; u is { Age: &gt; 18, Address.Country: "Pakistan" })
    .ToList();

foreach (var user in adultsFromPakistan)
{
    Console.WriteLine($"{user.Name} is adult from Pakistan");
}</code></pre><p>One of the most usable scenarios is pattern matching inside LINQ. It filters collections based on object shape and conditions.</p><h3 id="matching-partial-objects">Matching partial objects</h3><pre><code class="language-csharp">foreach (var user in users)
{
    if (user is { Name: "Ali" })
    {
        Console.WriteLine("Found Ali");
    }
}</code></pre><p>To match a condition, you don't even need to know the structure completely. As the example shows, you can check on a field as well.</p><h3 id="relational-logical-patterns">Relational + logical patterns</h3><pre><code class="language-csharp">foreach (var user in users)
{
    if (user.Age is &gt; 18 and &lt; 60)
    {
        Console.WriteLine($"{user.Name} is Adult");
    }
    else if (user.Age is &lt; 18 or &gt; 60)
    {
        Console.WriteLine($"{user.Name} is Special Age Group");
    }
}</code></pre><p>The relational pattern has made the comparison easier to read. Rather than just mathematical logical operators, you can use readable keywords. Apart from readability, it offers applications like comparing ages, checking a threshold, or checking a range.</p><h3 id="switch-expression">Switch expression</h3><pre><code class="language-csharp">foreach (var user in users)
{
    var category = user.Age switch
    {
        &lt; 13 =&gt; "Child",
        &lt; 20 =&gt; "Teen",
        &lt; 60 =&gt; "Adult",
        _ =&gt; "Senior"
    };

    Console.WriteLine($"{user.Name} =&gt; {category}");
}</code></pre><p> A switch case is a well-known way to compare multiple conditions. It is an ideal way of handling multiple cases instead of using a cluster of <code>else-if</code>. If you have some complex code to execute, then the following version is workable:</p><pre><code class="language-csharp">foreach (var user in users)
{
    string category;
    
    // Traditional switch statement with cases
    switch (user.Age)
    {
        case &lt; 13:
            category = "Child";
            break;
        case &lt; 20:
            category = "Teen";
            break;
        case &lt; 60:
            category = "Adult";
            break;
        default:
            category = "Senior";
            break;
    }
    
    Console.WriteLine($"{user.Name} =&gt; {category}");
}</code></pre><h3 id="type-condition-pattern"> Type + condition pattern</h3><pre><code class="language-csharp">object value = 150;

if (value is int number &amp;&amp; number &gt; 100)
{
    Console.WriteLine("Large number (old way)");
}

if (value is int and &gt; 100)
{
    Console.WriteLine("Large number (pattern matching way)");
}
</code></pre><p>The pattern checks type and condition simultaneously, liberating you from separate casting and checking logic. It is useful for object or dynamic data.</p><h3 id="list-pattern">List pattern</h3><pre><code class="language-csharp">int[] nums = { 1, 2, 3 };

if (nums is [1, 2, 3])
{
    Console.WriteLine("Exact match");
}

if (nums is [1, .., 3])
{
    Console.WriteLine("Starts with 1 and ends with 3");
}
</code></pre><p>List matching is available in C# 11 and later versions. You can match the array/list structure and content and validate without a loop. List patterns can be handy for validating sequences, checking API payload lists, and detecting start/end patterns.</p><h3 id="positional-pattern">Positional pattern</h3><pre><code class="language-csharp">var point = new Point(10, 20);

if (point is (10, 20))
{
    Console.WriteLine("Point matched (10,20)");
}
</code></pre><p>Another value comparison pattern matches objects based on their constructor/deconstructed values. Positional patterns are ideal for value types, such as records, because their comparisons are lightweight.</p><h3 id="combined-pattern">Combined pattern</h3><pre><code class="language-csharp">foreach (var user in users)
{
    if (user is
        {
            Age: &gt; 18,
            Address.City: "Lahore",
            Roles: ["User", ..]
        })
    {
        Console.WriteLine($"{user.Name} is eligible Lahore user");
    }
}</code></pre><p>This code combines different patterns. Actually, this is one of the most realistic scenarios in which complex objects and requirements are combined to implement different patterns.</p><h3 id="null-pattern">Null pattern</h3><pre><code class="language-csharp">User? maybeUser = null;

if (maybeUser is not null)
{
    Console.WriteLine("User exists");
}
else
{
    Console.WriteLine("User is null");
}
</code></pre><p>Null pattern is a cleaner and more readable alternative to the traditional <code>!= null</code>. Its usage spans large applications, input checks, and condition matching.</p><h3 id="guard-clause">Guard clause</h3><pre><code class="language-csharp">number = 7;

var result = number switch
{
    int n when n % 2 == 0 =&gt; "Even",
    int n when n % 2 != 0 =&gt; "Odd",
    _ =&gt; "Unknown"
};

Console.WriteLine(result);</code></pre><p>A guard clause allows an additional condition inside a switch case. It tackles complex branching logic and mathematical conditions, giving flexibility when patterns alone aren't enough.</p><h3 id="request-handling">Request handling</h3><pre><code class="language-csharp">foreach (var request in requests)
{
    var response = request switch
    {
        { Source: "System", Priority: &gt; 5 } =&gt; "Critical System Request",
        { Source: "User", Priority: &lt;= 5 } =&gt; "Normal User Request",
        _ =&gt; "Fallback"
    };

    Console.WriteLine($"{request.Source} ({request.Priority}) =&gt; {response}");
}</code></pre><p>Request handling is a remarkable way to implement business logic while keeping it readable within a switch statement. It has a ton of use cases, like event processing, request routing, and validating business rules. </p><p><strong>Step 5: 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/2026/04/image-8.png" class="kg-image" alt="Result" loading="lazy" width="618" height="519" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/04/image-8.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/04/image-8.png 618w"></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/04/image-9.png" class="kg-image" alt="Result" loading="lazy" width="399" height="155"></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/04/image-4.png" class="kg-image" alt="Result" loading="lazy" width="463" height="430"></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/04/image-5.png" class="kg-image" alt="Result" loading="lazy" width="512" height="524"></figure><p>The results are expected and correct. However, we did the job more cleanly by combining patterns as needed.</p><h2 id="conclusion">Conclusion</h2><p>Pattern matching has tons of usage. From validation to condition check, it is prominent. Using the right pattern at the right place can save a lot of code. In today's post, I shared some scenarios and their pattern solutions. In real scenarios, you have to know the patterns and use a combination when needed. This is real art.</p><p>Code: <a href="https://github.com/elmahio-blog/PatternMatchingDemo.git">https://github.com/elmahio-blog/PatternMatchingDemo.git</a></p> ]]></content:encoded>
    </item>
    <item>
        <title><![CDATA[ 12 practices for optimizing PostgreSQL queries for large datasets ]]></title>
        <description><![CDATA[ A database is the root of most applications. Anything displayed or any operations performed often rely on the database. So, to build a loyal client base, you have to think in terms of a database. In today&#39;s post, I will go through some key strategies you can apply ]]></description>
        <link>https://blog.elmah.io/12-practices-for-optimizing-postgresql-queries-for-large-datasets/</link>
        <guid isPermaLink="false">69b25d33db602000012ce9d0</guid>
        <category><![CDATA[  ]]></category>
        <dc:creator><![CDATA[ Ali Hamza Ansari ]]></dc:creator>
        <pubDate>Tue, 21 Apr 2026 09:21:23 +0200</pubDate>
        <media:content url="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/12-practices-for-optimizing-postgresql-queries-for-large-datasets-o.png" medium="image"/>
        <content:encoded><![CDATA[ <p>This blog post is originally published on <a href="https://blog.elmah.io/12-practices-for-optimizing-postgresql-queries-for-large-datasets/">https://blog.elmah.io/12-practices-for-optimizing-postgresql-queries-for-large-datasets/</a></p> 
<!--kg-card-begin: html-->
<div class="toc"></div>
<!--kg-card-end: html-->
<p>A database is the root of most applications. Anything displayed or any operations performed often rely on the database. So, to build a loyal client base, you have to think in terms of a database. In today's post, I will go through some key strategies you can apply to optimize your PostgreSQL database and win the performance war.</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/12-practices-for-optimizing-postgresql-queries-for-large-datasets-o-1.png" class="kg-image" alt="12 practices for optimizing PostgreSQL queries for large datasets" 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/12-practices-for-optimizing-postgresql-queries-for-large-datasets-o-1.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w1000/2026/03/12-practices-for-optimizing-postgresql-queries-for-large-datasets-o-1.png 1000w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/12-practices-for-optimizing-postgresql-queries-for-large-datasets-o-1.png 1500w" sizes="(min-width: 1200px) 1200px"></figure><h2 id="create-indexes-on-frequently-queried-columns">Create indexes on frequently queried columns</h2><p>Index data structures allow a row to be looked up quickly without scanning the entire table.</p><p>Without Indexes:</p><pre><code class="language-SQL">EXPLAIN ANALYZE
SELECT *
FROM orders
WHERE customer_id = 500;
</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-17.png" class="kg-image" alt="Query results" loading="lazy" width="697" height="441" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/03/image-17.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-17.png 697w"></figure><p>So, the execution time is 240 milliseconds.</p><p>After Index:</p><pre><code class="language-SQL">CREATE INDEX idx_orders_customer_id
ON orders(customer_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/03/image-18.png" class="kg-image" alt="Query results" loading="lazy" width="767" height="404" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/03/image-18.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-18.png 767w" sizes="(min-width: 720px) 720px"></figure><h2 id="normalize-the-database-strategically">Normalize the database strategically</h2><p>Normalization is a crucial step in database design to reduce anomalies and redundancy. Simply break your table into maintainable tables with logically coherent columns.</p><p>Instead of keeping role information in the user table, introduce a separate <code>Role</code> table and map it to <code>User</code> to keep data integral and non-redundant.</p><p>Keep in mind that over-normalization can also lead to performance penalties. Joining tables is more expensive than querying a single table, so you should analyze which columns should be split into new, normalized tables.</p><h2 id="avoid-select">Avoid SELECT *</h2><p>Fetch only the required columns from the query.</p><p>Bad query with SELECT *:</p><pre><code class="language-SQL">EXPLAIN ANALYZE
SELECT *
FROM orders
WHERE status = 'Completed';</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-20.png" class="kg-image" alt="Query results" loading="lazy" width="750" height="378" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/03/image-20.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-20.png 750w" sizes="(min-width: 720px) 720px"></figure><p>Now, with selective columns, let's say I only need <code>id</code> and <code>amount</code>, so I'm fetching only those columns:</p><pre><code class="language-SQL">EXPLAIN ANALYZE
SELECT id, amount
FROM orders
WHERE status = 'Completed';</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-21.png" class="kg-image" alt="Query results" loading="lazy" width="790" height="361" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/03/image-21.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-21.png 790w" sizes="(min-width: 720px) 720px"></figure><p>In this example, the execution time is identical, but it reduces I/O and memory usage, especially as the database schema becomes more complex. If the columns you select are all part of an index, PostgreSQL can return the data directly from the index without touching the heap (the actual table file). In that case, this can improve performance as well as I/O.</p><h2 id="order-joins-properly">Order JOINs properly</h2><p>Ordering JOINs properly can also affect performance. Modern PostgreSQL uses a cost-based optimizer. It analyzes table statistics to determine the most efficient join order and method regardless of the order you write them in the query. But when dealing with older versions or if settings like <code>join_collapse_limit</code> is set manually, this still applies.</p><p>Consider this query:</p><pre><code class="language-SQL">EXPLAIN ANALYZE
SELECT *
FROM orders o
JOIN customers c
ON o.customer_id = c.id
WHERE c.country = 'USA';
</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-36.png" class="kg-image" alt="Query results" loading="lazy" width="737" height="307" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/03/image-36.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-36.png 737w" sizes="(min-width: 720px) 720px"></figure><p>Now calling the <code>customers</code> table before:</p><pre><code class="language-SQL">EXPLAIN ANALYZE
SELECT *
FROM customers c
JOIN orders o
ON c.id = o.customer_id
WHERE c.country = 'USA';
</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-35.png" class="kg-image" alt="Query results" loading="lazy" width="713" height="297" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/03/image-35.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-35.png 713w"></figure><p>The <code>orders</code> table has 1 million rows, while the clusters have 100k. When we fetch orders before then, we have a larger set to JOIN with customers. If we use customers first, then join with orders, fewer rows are filtered in the first stage. Filtering earlier reduces the rows participating in the join.</p><h2 id="use-limit-when-exploring-data">Use LIMIT When Exploring Data</h2><p>In most cases, you will not use thousands of data points at once. Usually, you fetch a chunk of data to display at once, which is practical for UI display. Using that information, only fetch the required amount of data:</p><pre><code class="language-SQL">EXPLAIN ANALYZE
SELECT *
FROM orders
ORDER BY created_at DESC;</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-24.png" class="kg-image" alt="Query results" loading="lazy" width="744" height="201" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/03/image-24.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-24.png 744w" sizes="(min-width: 720px) 720px"></figure><p>While the efficient way:</p><pre><code class="language-SQL">EXPLAIN ANALYZE
SELECT *
FROM orders
ORDER BY created_at DESC
LIMIT 50;</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-25.png" class="kg-image" alt="Query results" loading="lazy" width="767" height="339" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/03/image-25.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-25.png 767w" sizes="(min-width: 720px) 720px"></figure><p>Hence, fetching limited rows saves you in all ways.</p><h2 id="use-partial-indexes">Use partial indexes</h2><p>A naive way for the query would be:</p><pre><code class="language-SQL">EXPLAIN ANALYZE
SELECT *
FROM orders
WHERE status = 'Completed';</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-26.png" class="kg-image" alt="Query results" loading="lazy" width="721" height="172" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/03/image-26.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-26.png 721w" sizes="(min-width: 720px) 720px"></figure><p>Creating a partial index:</p><pre><code class="language-SQL">CREATE INDEX idx_completed_orders
ON orders(customer_id)
WHERE status = 'Completed';</code></pre><p>Now the same query gives:</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-27.png" class="kg-image" alt="Query results" loading="lazy" width="841" height="350" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/03/image-27.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-27.png 841w" sizes="(min-width: 720px) 720px"></figure><p>Hence, indexing reduced the execution time to half that of the prior query. Simply create an index for frequently filtered values to speed up.</p><h2 id="use-proper-data-types">Use Proper Data Types</h2><p>Each data type has its own storage size and comparison time. Columns like primary keys should be handled with care due to these differences. Ints are faster than text, so use int as the primary key column for faster comparisons.</p><p>Good advice is to use the smallest sufficient data type. For example, use <code>bigint</code> instead of <code>int</code> if you expect more than 2.1 billion rows, as changing a PK type later is a high-risk operation.</p><h2 id="avoid-functions-on-indexed-columns">Avoid Functions on Indexed Columns</h2><p>Functions on indexed columns prevent index usage:</p><pre><code class="language-SQL ">EXPLAIN ANALYZE
SELECT *
FROM customers
WHERE LOWER(name) = 'customer 100';</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-28.png" class="kg-image" alt="Query results" loading="lazy" width="727" height="204" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/03/image-28.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-28.png 727w" sizes="(min-width: 720px) 720px"></figure><p>Creating an index :</p><pre><code class="language-SQL">CREATE INDEX idx_customers_lower_name
ON customers(LOWER(name));</code></pre><p>Now, the same query gives:</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-29.png" class="kg-image" alt="Query results" loading="lazy" width="796" height="229" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/03/image-29.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-29.png 796w" sizes="(min-width: 720px) 720px"></figure><p>We included the function in the index. Index can now support the function.</p><h2 id="partition-large-tables">Partition large tables </h2><p>Partitioning is another healthy way to handle large data sets. It divides a table into smaller ones:</p><pre><code class="language-SQL">EXPLAIN ANALYZE
SELECT *
FROM orders
WHERE created_at BETWEEN '2025-01-01' AND '2025-06-30';</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-31.png" class="kg-image" alt="Query result" loading="lazy" width="872" height="189" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/03/image-31.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-31.png 872w" sizes="(min-width: 720px) 720px"></figure><p>Create a copy table where we will use partitioning:</p><pre><code class="language-SQL">CREATE TABLE public.orders_partitioned(
    id          serial NOT NULL ,
    customer_id integer,
    amount      numeric,
    status      text,
    created_at  timestamp without time zone NOT NULL,
    CONSTRAINT orders_partitioned_pkey PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);</code></pre><p>Create yearly partitions:</p><pre><code class="language-SQL">CREATE TABLE public.orders_2024
    PARTITION OF public.orders_partitioned
    FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');

CREATE TABLE public.orders_2025
    PARTITION OF public.orders_partitioned
    FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');

CREATE TABLE public.orders_2026
    PARTITION OF public.orders_partitioned
    FOR VALUES FROM ('2026-01-01') TO ('2027-01-01');
    
-- Default partition — catches ANY row that doesn't fit the ranges above
CREATE TABLE public.orders_default
    PARTITION OF public.orders_partitioned
    DEFAULT;
</code></pre><p>Copy the data into our partitioned table:</p><pre><code class="language-SQL">INSERT INTO public.orders_partitioned SELECT * FROM public.orders;</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-32.png" class="kg-image" alt="Data inserted" loading="lazy" width="381" height="83"></figure><p>Just to check if everything is well:</p><pre><code class="language-SQL">SELECT
    inhrelid::regclass AS partition
FROM pg_inherits
WHERE inhparent = 'orders_partitioned'::regclass;</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-33.png" class="kg-image" alt="Partitions" loading="lazy" width="157" height="142"></figure><p>We can see 4 partitions created. Now the same query after partition:</p><pre><code class="language-SQL">EXPLAIN ANALYZE
SELECT *
FROM orders_partitioned
WHERE created_at BETWEEN '2025-01-01' AND '2025-06-30';</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-34.png" class="kg-image" alt="Query result" loading="lazy" width="876" height="171" srcset="https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/size/w600/2026/03/image-34.png 600w, https://storage.ghost.io/c/a8/df/a8df410f-f385-46fe-9464-e894f7952067/content/images/2026/03/image-34.png 876w" sizes="(min-width: 720px) 720px"></figure><h2 id="use-a-transaction-for-bulk-operations">Use a transaction for bulk operations.</h2><p>Operations like inserting or updating multiple rows at once use transactions. Transaction groups multiple SQL operations into a single unit of work. Instead of committing after every statement, PostgreSQL commits only once at the end of the transaction:</p><pre><code class="language-SQL ">INSERT INTO orders(customer_id, amount, status)
VALUES (1,100,'Completed');

INSERT INTO orders(customer_id, amount, status)
VALUES (2,200,'Completed');

INSERT INTO orders(customer_id, amount, status)
VALUES (3,300,'Completed');</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-37.png" class="kg-image" alt="Transaction result" loading="lazy" width="341" height="85"></figure><p>The above one writes each row one by one. Each insertion triggers a WAL write, a disk sync, and a commit operation:</p><pre><code class="language-SQL ">BEGIN;

INSERT INTO orders(customer_id, amount, status)
VALUES (1,100,'Completed');

INSERT INTO orders(customer_id, amount, status)
VALUES (2,200,'Completed');

INSERT INTO orders(customer_id, amount, status)
VALUES (3,300,'Completed');

COMMIT;</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-38.png" class="kg-image" alt="Transaction result" loading="lazy" width="319" height="75"></figure><p>The transaction was completed in less than half the time. That is the only difference, with only 3 inserts. Once you scale up to more rows, this difference becomes more pronounced. transaction commits only once, awakening the expensive operations once for all the rows. </p><h2 id="avoid-long-running-transactions">Avoid Long-Running Transactions</h2><p>Long-running transactions keep old row versions alive underp PostgreSQL's MVCC. This prevents VACUUM from cleaning dead rows, causing table bloat:</p><pre><code class="language-SQL">BEGIN;

SELECT *
FROM orders;

COMMIT;</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-39.png" class="kg-image" alt="Transaction result" loading="lazy" width="391" height="87"></figure><pre><code class="language-SQL">BEGIN;

UPDATE orders
SET status = 'Completed'
WHERE id = 100;

COMMIT;</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-40.png" class="kg-image" alt="transaction result" loading="lazy" width="326" height="92"></figure><p>Short, focused transactions don't last as long as the first one in our example did. Besides, VACUUM can reclaim space and shorter transactions lower lock contention.</p><h2 id="clean-the-table-and-index-bloats">Clean the table and index bloats</h2><p>Postgres <strong>table bloat</strong> occurs when a table contains dead rows that are no longer visible to any transaction but still occupy disk space. Postgres <code>DELETE</code> or <code>UPDATE</code> Operations create dead rows in the tables they modify. For example, if we run the following on an order row with a 400 amount:</p><pre><code class="language-SQL">UPDATE orders
SET amount = 1000
WHERE id = 55;</code></pre><p>Postgres keeps the older row but marks it as dead, and adds a new one. That means the deleted row will be there along with the new one. Such dead rows pile up, overwhelming the storage. PostgreSQL provides VACUUM to reclaim or reuse&nbsp;the storage.</p><p><strong>Index bloat</strong> occurs when indexes contain references to dead rows that still occupy space. Every update also updates indexes, so PostgreSQL keeps dead row indexes.</p><p>Bloated tables and indexes can significantly slow down queries and unnecessarily consume additional disk space, pressuring I/O.</p><p>We can simply fix table bloat manually:</p><pre><code class="language-SQL">VACUUM orders;</code></pre><p>This reclaims storage occupied by dead rows and ensures that the space can be reused:</p><pre><code class="language-SQL">VACUUM FULL orders;</code></pre><p>The above is an intensive counterpart that reclaims all storage occupied by dead rows, including the space at the end of the table. Be aware that a full vacuum will block all reads and writes to that table until it finishes. For large datasets, this can cause hours of downtime.</p><p>Usually, an AUTOVACUUM is by default enabled, which we can check </p><pre><code class="language-SQL">SHOW autovacuum;</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-41.png" class="kg-image" alt="autovacuum" loading="lazy" width="151" height="68"></figure><p>You can configure the auto vacuum. Make sure it remains on, otherwise, leaving dead rows can eat up space, which is dangerous for the production database. You can simply do it by running:</p><pre><code class="language-SQL">ALTER SYSTEM SET autovacuum = 'on';
SELECT pg_reload_conf();</code></pre><p>How often should it run:</p><pre><code class="language-SQL">-- Set the naptime to 30 seconds for more aggressive cleanup
ALTER SYSTEM SET autovacuum_naptime = '30s';
-- Reload the configuration to apply changes without restarting the service
SELECT pg_reload_conf();</code></pre><p>When vacuum triggers:</p><pre><code class="language-SQL">autovacuum_vacuum_threshold = 50
autovacuum_vacuum_scale_factor = 0.2</code></pre><p>The above configuration means running the vacuum when the number of dead rows exceeds 50 and when 20% of the table size is reached.</p><p>Apply changes by running:</p><pre><code class="language-SQL">SELECT pg_reload_conf();</code></pre><p>For a table level:</p><pre><code class="language-SQL">ALTER TABLE orders
SET (
    autovacuum_vacuum_threshold = 50
    autovacuum_vacuum_scale_factor = 0.2
);</code></pre><p>To view how much storage your tables are taking, run:</p><pre><code class="language-SQL">SELECT
    relname,
    pg_size_pretty(pg_total_relation_size(relid)) AS total_size
FROM pg_catalog.pg_statio_user_tables
ORDER BY pg_total_relation_size(relid) DESC;</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-42.png" class="kg-image" alt="tables storage" loading="lazy" width="233" height="203"></figure><h2 id="conclusion">Conclusion</h2><p>As a popular database choice for scalable applications, PostgreSQL requires several performance optimization techniques to keep the system fast. In this post, I presented the 12 most insightful ways to speed up your database. From indexing and partitioning to projection and handling bloat. I shared them in detail and explained how to implement them.</p> ]]></content:encoded>
    </item>
    <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>

</channel>
</rss>