<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	xmlns:media="http://search.yahoo.com/mrss/" >

<channel>
	<title>AspDotnetHelp.com</title>
	<atom:link href="https://aspdotnethelp.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://aspdotnethelp.com</link>
	<description>Asp.Net and C#.Net Tutorials</description>
	<lastBuildDate>Wed, 11 Nov 2026 13:30:40 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1.4</generator>

<image>
	<url>https://aspdotnethelp.com/wp-content/uploads/2023/08/aspdotnethelp-favicon-150x150.png</url>
	<title>AspDotnetHelp.com</title>
	<link>https://aspdotnethelp.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Minimal APIs vs Controllers in ASP.NET Core (.NET 10): Tested Side by Side</title>
		<link>https://aspdotnethelp.com/minimal-api-vs-controllers-aspnet-core/</link>
		
		<dc:creator><![CDATA[Bijay Kumar]]></dc:creator>
		<pubDate>Wed, 11 Nov 2026 21:20:00 +0000</pubDate>
				<category><![CDATA[ASP.NET Core]]></category>
		<guid isPermaLink="false">https://aspdotnethelp.com/?p=2793</guid>

					<description><![CDATA[If your minimal API sent back an empty 404 while the controller next to it returned a full ProblemDetails body, you&#8217;ve found the real ASP.NET Core minimal API vs controller difference: controllers hand you MVC&#8217;s conventions, minimal APIs make you opt in. Use minimal APIs for new .NET 10 APIs, and keep controllers when you ... <a title="Minimal APIs vs Controllers in ASP.NET Core (.NET 10): Tested Side by Side" class="read-more" href="https://aspdotnethelp.com/minimal-api-vs-controllers-aspnet-core/" aria-label="Read more about Minimal APIs vs Controllers in ASP.NET Core (.NET 10): Tested Side by Side">Read more &#62;&#62;</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">If your minimal API sent back an empty 404 while the controller next to it returned a full ProblemDetails body, you&#8217;ve found the real <strong>ASP.NET Core minimal API vs controller</strong> difference: controllers hand you MVC&#8217;s conventions, minimal APIs make you opt in.</p>



<p class="wp-block-paragraph">Use minimal APIs for new .NET 10 APIs, and keep controllers when you need MVC-only features like custom model binders or OData. In my local benchmarks of one orders API, the minimal endpoint was faster in 5 of 6 runs, by up to 1.57x.</p>



<h2 class="wp-block-heading">Minimal APIs vs Controllers at a Glance</h2>



<p class="wp-block-paragraph">Every row comes from the same project, tested on .NET 10 (SDK 10.0.402) further down:</p>



<figure class="wp-block-table"><table><thead><tr><th>Feature</th><th>Minimal APIs</th><th>Controllers</th></tr></thead><tbody><tr><td>Define an endpoint</td><td><code>app.MapGet(...)</code> lambda or static method</td><td>Action method on a <code>ControllerBase</code> class</td></tr><tr><td>Group routes</td><td><code>MapGroup(&quot;/minimal/orders&quot;)</code></td><td><code>[Route(&quot;controllers/orders&quot;)]</code> on the class</td></tr><tr><td>Body binding</td><td>Inferred for complex types</td><td>Inferred by <code>[ApiController]</code> (<code>[FromBody]</code> optional)</td></tr><tr><td>Validation</td><td><code>AddValidation()</code> (new in .NET 10)</td><td>Automatic with <code>[ApiController]</code></td></tr><tr><td>Default 404 body</td><td>Empty</td><td>ProblemDetails JSON</td></tr><tr><td>Filters</td><td><code>IEndpointFilter</code></td><td>Action, resource, result, exception filters</td></tr><tr><td>OpenAPI 404 documented</td><td>Yes, from <code>Results&lt;Ok, NotFound&gt;</code></td><td>Only with <code>[ProducesResponseType]</code></td></tr><tr><td>Local throughput (GET by id, one run)</td><td>63,179 req/s</td><td>40,363 req/s</td></tr><tr><td>Native AOT</td><td>Supported (partial)</td><td>Not supported</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Microsoft&#8217;s own guidance now says to start new projects with minimal APIs. If you&#8217;re still on .NET 8, <a href="https://aspdotnethelp.com/upgrade-dotnet-8-to-dotnet-10/">upgrade to .NET 10</a> first: the validation support below needs it.</p>



<h2 class="wp-block-heading">The Sample: One US Orders API Built Both Ways</h2>



<p class="wp-block-paragraph">I put both styles in one project, UsOrdersApi, so they share the same store, middleware and server. <code>/minimal/orders</code> is the minimal API and <code>/controllers/orders</code> is the controller. The only package is Microsoft.AspNetCore.OpenApi 10.0.14:</p>



<pre class="wp-block-code"><code>&lt;Project Sdk=&quot;Microsoft.NET.Sdk.Web&quot;&gt;

  &lt;PropertyGroup&gt;
    &lt;TargetFramework&gt;net10.0&lt;/TargetFramework&gt;
    &lt;Nullable&gt;enable&lt;/Nullable&gt;
    &lt;ImplicitUsings&gt;enable&lt;/ImplicitUsings&gt;
  &lt;/PropertyGroup&gt;

  &lt;ItemGroup&gt;
    &lt;PackageReference Include=&quot;Microsoft.AspNetCore.OpenApi&quot; Version=&quot;10.0.14&quot; /&gt;
  &lt;/ItemGroup&gt;

&lt;/Project&gt;</code></pre>



<p class="wp-block-paragraph">The models are positional records (here&#8217;s <a href="https://aspdotnethelp.com/csharp-record-vs-class-vs-struct/">when a record beats a class</a>), and <code>CreateOrder</code> carries the validation attributes. Three Texas and California orders seed the store:</p>



<pre class="wp-block-code"><code>using System.ComponentModel.DataAnnotations;

namespace UsOrdersApi;

public record Order(int Id, string Customer, string State, decimal Total);

public record CreateOrder(
    [Required] string Customer,
    [Required, StringLength(2, MinimumLength = 2)] string State,
    [Range(1, 10_000)] decimal Total);

// One in-memory store, shared by both styles (registered as a singleton)
public class OrderStore
{
    readonly List&lt;Order&gt; orders =
    [
        new(1, &quot;Jenna Morales&quot;, &quot;TX&quot;, 249.99m),
        new(2, &quot;Tyler Brooks&quot;, &quot;CA&quot;, 89.50m),
        new(3, &quot;Rachel Kim&quot;, &quot;TX&quot;, 1200.00m),
    ];

    public IEnumerable&lt;Order&gt; All(string? state) =&gt;
        state is null ? orders : orders.Where(o =&gt; o.State == state);

    public Order? Find(int id) =&gt; orders.FirstOrDefault(o =&gt; o.Id == id);

    public Order Add(CreateOrder input)
    {
        lock (orders)
        {
            var order = new Order(orders.Count + 1, input.Customer, input.State, input.Total);
            orders.Add(order);
            return order;
        }
    }
}</code></pre>



<p class="wp-block-paragraph"><code>Program.cs</code> registers both styles side by side. Yes, one app can serve minimal endpoints and controllers at the same time:</p>



<pre class="wp-block-code"><code>using UsOrdersApi;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton&lt;OrderStore&gt;();
builder.Services.AddControllers();   // controllers
builder.Services.AddValidation();    // .NET 10 validation for minimal APIs
builder.Services.AddOpenApi();

var app = builder.Build();

app.MapOpenApi();
app.MapControllers();                // OrdersController
app.MapOrderEndpoints();             // OrderEndpoints (MapGroup)

app.Run();</code></pre>



<p class="wp-block-paragraph">The launch profile pins port 5793, and <code>appsettings.json</code> is the template&#8217;s logging config:</p>



<pre class="wp-block-code"><code>{
  &quot;$schema&quot;: &quot;https://json.schemastore.org/launchsettings.json&quot;,
  &quot;profiles&quot;: {
    &quot;http&quot;: {
      &quot;commandName&quot;: &quot;Project&quot;,
      &quot;dotnetRunMessages&quot;: true,
      &quot;launchBrowser&quot;: false,
      &quot;applicationUrl&quot;: &quot;http://localhost:5793&quot;,
      &quot;environmentVariables&quot;: {
        &quot;ASPNETCORE_ENVIRONMENT&quot;: &quot;Development&quot;
      }
    }
  }
}</code></pre>



<pre class="wp-block-code"><code>{
  &quot;Logging&quot;: {
    &quot;LogLevel&quot;: {
      &quot;Default&quot;: &quot;Information&quot;,
      &quot;Microsoft.AspNetCore&quot;: &quot;Warning&quot;
    }
  }
}</code></pre>



<h2 class="wp-block-heading">Routing: Minimal API MapGroup vs Controller [Route]</h2>



<p class="wp-block-paragraph">The minimal side lives in <code>OrderEndpoints.cs</code>. <code>MapGroup</code> sets the prefix, the OpenAPI tag and the filter once for every endpoint in the group:</p>



<pre class="wp-block-code"><code>using Microsoft.AspNetCore.Http.HttpResults;

namespace UsOrdersApi;

public static class OrderEndpoints
{
    public static RouteGroupBuilder MapOrderEndpoints(this IEndpointRouteBuilder app)
    {
        var group = app.MapGroup(&quot;/minimal/orders&quot;)
            .WithTags(&quot;Minimal&quot;)
            .AddEndpointFilter&lt;StyleHeaderFilter&gt;();

        group.MapGet(&quot;/&quot;, (string? state, OrderStore store) =&gt; store.All(state));
        group.MapGet(&quot;/{id:int}&quot;, GetById);
        group.MapPost(&quot;/&quot;, Create);
        return group;
    }

    public static Results&lt;Ok&lt;Order&gt;, NotFound&gt; GetById(int id, OrderStore store) =&gt;
        store.Find(id) is { } order ? TypedResults.Ok(order) : TypedResults.NotFound();

    public static Created&lt;Order&gt; Create(CreateOrder input, OrderStore store)
    {
        var order = store.Add(input);
        return TypedResults.Created($&quot;/minimal/orders/{order.Id}&quot;, order);
    }
}</code></pre>



<p class="wp-block-paragraph">The controller gets the same thing from attributes: <code>[Route]</code> on the class, <code>[HttpGet]</code> and <code>[HttpPost]</code> on the actions:</p>



<pre class="wp-block-code"><code>using Microsoft.AspNetCore.Mvc;

namespace UsOrdersApi.Controllers;

[ApiController]
[Route(&quot;controllers/orders&quot;)]
[StyleHeader]
public class OrdersController(OrderStore store) : ControllerBase
{
    [HttpGet]
    public IEnumerable&lt;Order&gt; GetAll(string? state) =&gt; store.All(state);

    [HttpGet(&quot;{id:int}&quot;)]
    public ActionResult&lt;Order&gt; GetById(int id) =&gt;
        store.Find(id) is { } order ? order : NotFound();

    [HttpPost]
    public ActionResult&lt;Order&gt; Create([FromBody] CreateOrder input)
    {
        var order = store.Add(input);
        return CreatedAtAction(nameof(GetById), new { id = order.Id }, order);
    }
}</code></pre>



<p class="wp-block-paragraph">Same store, same query. Here&#8217;s Texas orders from both:</p>



<pre class="wp-block-code"><code>curl.exe -i &quot;http://localhost:5793/minimal/orders?state=TX&quot;
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
X-Api-Style: minimal (endpoint filter)
[{&quot;id&quot;:1,&quot;customer&quot;:&quot;Jenna Morales&quot;,&quot;state&quot;:&quot;TX&quot;,&quot;total&quot;:249.99},{&quot;id&quot;:3,&quot;customer&quot;:&quot;Rachel Kim&quot;,&quot;state&quot;:&quot;TX&quot;,&quot;total&quot;:1200.00}]

curl.exe -i &quot;http://localhost:5793/controllers/orders?state=TX&quot;
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
X-Api-Style: controller (action filter)
[{&quot;id&quot;:1,&quot;customer&quot;:&quot;Jenna Morales&quot;,&quot;state&quot;:&quot;TX&quot;,&quot;total&quot;:249.99},{&quot;id&quot;:3,&quot;customer&quot;:&quot;Rachel Kim&quot;,&quot;state&quot;:&quot;TX&quot;,&quot;total&quot;:1200.00}]</code></pre>



<p class="wp-block-paragraph">Both returned the same two orders, Jenna Morales ($249.99) and Rachel Kim ($1,200.00). The bodies match byte for byte; only my style header differs.</p>



<p class="wp-block-paragraph">Route constraints like <code>{id:int}</code> work the same in both. Under the hood, both use endpoint routing, so <a href="https://aspdotnethelp.com/aspnet-core-middleware/">ASP.NET Core middleware</a> runs the same way for either style.</p>



<h2 class="wp-block-heading">Model Binding: Do Minimal APIs Need [FromBody]?</h2>



<p class="wp-block-paragraph">No. A minimal API binds a complex type like <code>CreateOrder</code> from the JSON body on its own, and simple types like <code>string? state</code> from the route or query string. <code>[ApiController]</code> applies the same rules to controllers, so <code>[FromBody]</code> there is just documentation.</p>



<p class="wp-block-paragraph">Posting a valid Florida order to both:</p>



<pre class="wp-block-code"><code>curl.exe -i -X POST http://localhost:5793/minimal/orders -H &quot;Content-Type: application/json&quot; -d &#x27;{&quot;customer&quot;:&quot;Marcus Reed&quot;,&quot;state&quot;:&quot;FL&quot;,&quot;total&quot;:74.25}&#x27;
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Location: /minimal/orders/4
X-Api-Style: minimal (endpoint filter)
{&quot;id&quot;:4,&quot;customer&quot;:&quot;Marcus Reed&quot;,&quot;state&quot;:&quot;FL&quot;,&quot;total&quot;:74.25}

curl.exe -i -X POST http://localhost:5793/controllers/orders -H &quot;Content-Type: application/json&quot; -d &#x27;{&quot;customer&quot;:&quot;Marcus Reed&quot;,&quot;state&quot;:&quot;FL&quot;,&quot;total&quot;:74.25}&#x27;
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Location: http://localhost:5793/controllers/orders/5
X-Api-Style: controller (action filter)
{&quot;id&quot;:5,&quot;customer&quot;:&quot;Marcus Reed&quot;,&quot;state&quot;:&quot;FL&quot;,&quot;total&quot;:74.25}</code></pre>



<p class="wp-block-paragraph">Both returned 201 Created. The <code>Location</code> header differs, though: <code>TypedResults.Created</code> sent my relative <code>/minimal/orders/4</code>, while <code>CreatedAtAction</code> built the absolute <code>http://localhost:5793/controllers/orders/5</code> from the route.</p>



<h2 class="wp-block-heading">Validation: AddValidation in .NET 10 vs [ApiController] ModelState</h2>



<p class="wp-block-paragraph">Controllers have validated automatically for years: <code>[ApiController]</code> checks <code>ModelState</code> and returns a 400 before the action runs. Minimal APIs got the same thing in .NET 10 with <code>builder.Services.AddValidation()</code>, which uses a source generator to find the endpoint parameter types.</p>



<p class="wp-block-paragraph">Same bad order to both (blank customer, &#8220;Texas&#8221; as the state, a $0 total):</p>



<pre class="wp-block-code"><code>curl.exe -i -X POST http://localhost:5793/minimal/orders -H &quot;Content-Type: application/json&quot; -d &#x27;{&quot;customer&quot;:&quot;&quot;,&quot;state&quot;:&quot;Texas&quot;,&quot;total&quot;:0}&#x27;
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
{&quot;title&quot;:&quot;One or more validation errors occurred.&quot;,&quot;errors&quot;:{&quot;Customer&quot;:[&quot;The Customer field is required.&quot;],&quot;State&quot;:[&quot;The field State must be a string with a minimum length of 2 and a maximum length of 2.&quot;],&quot;Total&quot;:[&quot;The field Total must be between 1 and 10000.&quot;]}}

curl.exe -i -X POST http://localhost:5793/controllers/orders -H &quot;Content-Type: application/json&quot; -d &#x27;{&quot;customer&quot;:&quot;&quot;,&quot;state&quot;:&quot;Texas&quot;,&quot;total&quot;:0}&#x27;
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json; charset=utf-8
{&quot;type&quot;:&quot;https://tools.ietf.org/html/rfc9110#section-15.5.1&quot;,&quot;title&quot;:&quot;One or more validation errors occurred.&quot;,&quot;status&quot;:400,&quot;errors&quot;:{&quot;State&quot;:[&quot;The field State must be a string with a minimum length of 2 and a maximum length of 2.&quot;],&quot;Total&quot;:[&quot;The field Total must be between 1 and 10000.&quot;],&quot;Customer&quot;:[&quot;The Customer field is required.&quot;]},&quot;traceId&quot;:&quot;00-…-00&quot;}</code></pre>



<p class="wp-block-paragraph">Both caught all three errors with the same messages. The shape differs: the minimal API sent plain <code>application/json</code> with only <code>title</code> and <code>errors</code>, while the controller sent <code>application/problem+json</code> with <code>type</code>, <code>status</code> and <code>traceId</code>.</p>



<p class="wp-block-paragraph">To get the controller&#8217;s shape, I added <code>builder.Services.AddProblemDetails();</code> and posted the same order again:</p>



<pre class="wp-block-code"><code>HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
{&quot;type&quot;:&quot;https://tools.ietf.org/html/rfc9110#section-15.5.1&quot;,&quot;title&quot;:&quot;One or more validation errors occurred.&quot;,&quot;status&quot;:400,&quot;errors&quot;:{&quot;Customer&quot;:[&quot;The Customer field is required.&quot;],&quot;State&quot;:[&quot;The field State must be a string with a minimum length of 2 and a maximum length of 2.&quot;],&quot;Total&quot;:[&quot;The field Total must be between 1 and 10000.&quot;]},&quot;traceId&quot;:&quot;00-…-00&quot;}</code></pre>



<p class="wp-block-paragraph"><strong>Note:</strong> skip <code>AddValidation()</code> and nothing fails loudly. Invalid orders just reach your handler. On .NET 10, nullable value type parameters (like <code>int?</code>) also aren&#8217;t validated yet.</p>



<h2 class="wp-block-heading">Filters: Minimal API IEndpointFilter vs Controller Action Filters</h2>



<p class="wp-block-paragraph">Both styles let you run code around the handler. Minimal APIs use <code>IEndpointFilter</code>; controllers use MVC&#8217;s filter pipeline. Mine both set an <code>X-Api-Style</code> header:</p>



<pre class="wp-block-code"><code>using Microsoft.AspNetCore.Mvc.Filters;

namespace UsOrdersApi;

// Minimal APIs: endpoint filter, runs around the handler
public class StyleHeaderFilter : IEndpointFilter
{
    public async ValueTask&lt;object?&gt; InvokeAsync(EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        context.HttpContext.Response.Headers[&quot;X-Api-Style&quot;] = &quot;minimal (endpoint filter)&quot;;
        return await next(context);
    }
}

// Controllers: action filter, runs around the action method
public class StyleHeaderAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context) =&gt;
        context.HttpContext.Response.Headers[&quot;X-Api-Style&quot;] = &quot;controller (action filter)&quot;;
}</code></pre>



<p class="wp-block-paragraph">You saw them work in the curl output: every 200 and 201 carried the header. The 400s didn&#8217;t, in either style, because validation short-circuited the request before my filter ran.</p>



<p class="wp-block-paragraph">Controllers still have more filter types (resource, result, exception, authorization). For minimal APIs, I put cross-cutting work like error handling and auth in middleware or on the group with <code>RequireAuthorization()</code>, which pairs well with <a href="https://aspdotnethelp.com/jwt-authentication-aspnet-core/">JWT bearer authentication</a>.</p>



<h2 class="wp-block-heading">Dependency Injection in Minimal API Handlers vs Controllers</h2>



<p class="wp-block-paragraph">Look back at the two files. The minimal handler takes <code>OrderStore store</code> as a parameter, and ASP.NET Core sees it&#8217;s a registered service. The controller takes it through a primary <a href="https://aspdotnethelp.com/constructor-in-csharp/">constructor</a>, and a new controller is created for every request.</p>



<p class="wp-block-paragraph">The lifetimes work the same either way, so <code>OrderStore</code> is one singleton shared by both styles. My guide to <a href="https://aspdotnethelp.com/dependency-injection-lifetimes-aspnet-core/">transient, scoped and singleton lifetimes</a> covers which one to pick.</p>



<p class="wp-block-paragraph">One practical difference: a controller with ten actions builds every dependency for every request, even ones only one action needs. A minimal handler asks only for what it uses.</p>



<h2 class="wp-block-heading">OpenAPI Support for Minimal APIs and Controllers</h2>



<p class="wp-block-paragraph">Both styles feed the built-in <code>AddOpenApi()</code> document. This file-based app reads <code>/openapi/v1.json</code> and lists each operation with its tag and response codes:</p>



<pre class="wp-block-code"><code>using System.Text.Json;

var http = new HttpClient();
var url = &quot;http://localhost:5793/openapi/v1.json&quot;;
using var doc = JsonDocument.Parse(await http.GetStringAsync(url));

foreach (var path in doc.RootElement.GetProperty(&quot;paths&quot;).EnumerateObject())
    foreach (var op in path.Value.EnumerateObject())
    {
        var tag = op.Value.GetProperty(&quot;tags&quot;)[0].GetString();
        var codes = string.Join(&quot;,&quot;, op.Value.GetProperty(&quot;responses&quot;)
            .EnumerateObject().Select(r =&gt; r.Name));
        Console.WriteLine(
            $&quot;{op.Name.ToUpper(),-5} {path.Name,-26} {tag,-8} {codes}&quot;);
    }</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>GET   /minimal/orders            Minimal  200
POST  /minimal/orders            Minimal  201
GET   /minimal/orders/{id}       Minimal  200,404
GET   /controllers/orders        Orders   200
POST  /controllers/orders        Orders   200
GET   /controllers/orders/{id}   Orders   200</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img fetchpriority="high" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/minimal-api-vs-controller-openapi-document-paths-tags-response-codes.png" alt="ASP.NET Core minimal API vs controller OpenAPI document in VS Code: CheckOpenApi.cs lists the minimal GET /minimal/orders/{id} with 200,404 and the controller GET /controllers/orders/{id} with only 200" class="wp-image-2826" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/minimal-api-vs-controller-openapi-document-paths-tags-response-codes.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/minimal-api-vs-controller-openapi-document-paths-tags-response-codes-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/minimal-api-vs-controller-openapi-document-paths-tags-response-codes-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/minimal-api-vs-controller-openapi-document-paths-tags-response-codes-768x674.png 768w" sizes="(max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">The minimal endpoint documents its 404; the controller action doesn&#8217;t</figcaption></figure>
</div>


<p class="wp-block-paragraph">The minimal <code>GetById</code> returns <code>Results&lt;Ok&lt;Order&gt;, NotFound&gt;</code>, so OpenAPI knows both codes. The controller&#8217;s <code>ActionResult&lt;Order&gt;</code> only documents 200 until you add <code>[ProducesResponseType(StatusCodes.Status404NotFound)]</code>.</p>



<p class="wp-block-paragraph">The POST rows tell the same story: the minimal <code>Created&lt;Order&gt;</code> documents 201, while the controller claims 200 even though it really returns 201. Neither side lists the validation 400 on its own.</p>



<p class="wp-block-paragraph">To browse that document in a UI, follow my <a href="https://aspdotnethelp.com/openapi-scalar-aspnet-core/">OpenAPI with Scalar setup</a>; it works the same for both styles.</p>



<h2 class="wp-block-heading">Testing Minimal API Handlers vs Controller Actions</h2>



<p class="wp-block-paragraph">Unit tests are where named handlers pay off. This <a href="https://aspdotnethelp.com/dotnet-run-file-based-apps/">file-based app</a> references the project with <code>#:project</code> and calls both styles directly, no server involved:</p>



<pre class="wp-block-code"><code>#:project UsOrdersApi/UsOrdersApi.csproj
using Microsoft.AspNetCore.Http.HttpResults;
using UsOrdersApi;
using UsOrdersApi.Controllers;

var store = new OrderStore();

// Minimal API: the handler is a plain static method
var found = OrderEndpoints.GetById(1, store).Result;
var missing = OrderEndpoints.GetById(99, store).Result;
if (found is Ok&lt;Order&gt; ok) Console.WriteLine($&quot;Minimal 1:     Ok, {ok.Value!.Customer}&quot;);
if (missing is NotFound) Console.WriteLine(&quot;Minimal 99:    NotFound&quot;);

// Controller: create it with its dependencies, then call the action
var ctrl = new OrdersController(store);
Console.WriteLine($&quot;Controller 1:  {ctrl.GetById(1).Value!.Customer}&quot;);
var notFound = ctrl.GetById(99).Result!;
Console.WriteLine($&quot;Controller 99: {notFound.GetType().Name}&quot;);</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>Minimal 1:     Ok, Jenna Morales
Minimal 99:    NotFound
Controller 1:  Jenna Morales
Controller 99: NotFoundResult</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/unit-test-minimal-api-handler-vs-controller-action-typedresults-actionresult.png" alt="Unit test a minimal API handler vs a controller action in .NET 10: UnitCheck.cs calls OrderEndpoints.GetById and OrdersController.GetById directly and prints Ok, NotFound and NotFoundResult" class="wp-image-2830" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/unit-test-minimal-api-handler-vs-controller-action-typedresults-actionresult.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/unit-test-minimal-api-handler-vs-controller-action-typedresults-actionresult-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/unit-test-minimal-api-handler-vs-controller-action-typedresults-actionresult-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/unit-test-minimal-api-handler-vs-controller-action-typedresults-actionresult-768x674.png 768w" sizes="(max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">Both are easy to call; the minimal handler returns a typed result you can pattern-match</figcaption></figure>
</div>


<p class="wp-block-paragraph">The minimal handler is a static method, so the test passes in the store and pattern-matches <code>Ok&lt;Order&gt;</code> or <code>NotFound</code>. The controller works too, but you build it first and inspect <code>Value</code> or <code>Result</code> (<code>NotFoundResult</code>).</p>



<p class="wp-block-paragraph">For integration tests, <code>WebApplicationFactory</code> from Microsoft.AspNetCore.Mvc.Testing hosts the whole app in memory and works the same for both. Your test client is just an <a href="https://aspdotnethelp.com/httpclient-in-csharp/">HttpClient</a>.</p>



<h2 class="wp-block-heading">Minimal API Performance: Is It Faster Than Controllers?</h2>



<p class="wp-block-paragraph">Usually, yes. I ran this benchmark against the Release build of UsOrdersApi: 2,000 warm-up calls each, then three rounds of 20,000 <code>GET /orders/1</code> requests at 32 concurrent, keeping each style&#8217;s best round:</p>



<pre class="wp-block-code"><code>using System.Diagnostics;
using System.Globalization;

CultureInfo.CurrentCulture = new CultureInfo(&quot;en-US&quot;);
var http = new HttpClient();
var api = &quot;http://localhost:5793&quot;;
string[] urls = [$&quot;{api}/minimal/orders/1&quot;, $&quot;{api}/controllers/orders/1&quot;];
foreach (var url in urls) await Run(url, 2_000);       // warm-up
var best = new double[2];
for (int round = 0; round &lt; 3; round++)                // best of 3
    for (int i = 0; i &lt; 2; i++)
        best[i] = Math.Max(best[i], await Run(urls[i], 20_000));
string[] lines = [$&quot;Minimal API  {best[0],8:N0} req/s&quot;,
                  $&quot;Controller   {best[1],8:N0} req/s&quot;,
                  $&quot;Ratio        {best[0] / best[1],8:F2}x&quot;];
foreach (var line in lines) Console.WriteLine(line);
File.WriteAllLines(&quot;bench-results.txt&quot;, lines);

async Task&lt;double&gt; Run(string url, int count)
{
    var sw = Stopwatch.StartNew();
    var opts = new ParallelOptions { MaxDegreeOfParallelism = 32 };
    await Parallel.ForAsync(0, count, opts, async (_, ct) =&gt;
        (await http.GetAsync(url, ct)).EnsureSuccessStatusCode());
    return count / sw.Elapsed.TotalSeconds;
}</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>Minimal API    63,179 req/s
Controller     40,363 req/s
Ratio            1.57x</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/minimal-api-vs-controller-performance-benchmark-requests-per-second-dotnet-10.png" alt="Minimal API performance vs controllers on .NET 10: Bench.cs in VS Code shows 63,179 req/s for the minimal endpoint and 40,363 req/s for the controller, 1.57x" class="wp-image-2827" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/minimal-api-vs-controller-performance-benchmark-requests-per-second-dotnet-10.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/minimal-api-vs-controller-performance-benchmark-requests-per-second-dotnet-10-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/minimal-api-vs-controller-performance-benchmark-requests-per-second-dotnet-10-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/minimal-api-vs-controller-performance-benchmark-requests-per-second-dotnet-10-768x674.png 768w" sizes="(max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">Same app, same store, same filter: only the endpoint style changes</figcaption></figure>
</div>


<p class="wp-block-paragraph">In the run you see, the minimal endpoint served 63,179 requests per second and the controller 40,363, a ratio of 1.57x. The client uses <a href="https://aspdotnethelp.com/async-await-in-csharp/">async and await</a> with <code>Parallel.ForAsync</code> to keep 32 requests in flight.</p>



<p class="wp-block-paragraph">Raw numbers swung a lot. Across 6 runs of this benchmark, minimal APIs came out ahead 5 times (1.29x to 1.57x), and the other run was within 2% either way, a tie.</p>



<p class="wp-block-paragraph"><strong>Important:</strong> this is one laptop, with the client and server sharing CPU, on a tiny in-memory endpoint. A database call or real JSON payload costs far more than the framework gap, so don&#8217;t migrate a working API for speed alone.</p>



<h2 class="wp-block-heading">Native AOT: Minimal APIs Only</h2>



<p class="wp-block-paragraph">Native AOT compiles the app to one self-contained executable with faster startup and less memory. Microsoft&#8217;s compatibility table lists minimal APIs as partially supported and MVC as not supported. I turned on <code>PublishAot</code> for UsOrdersApi to see what the analyzer says:</p>



<pre class="wp-block-code"><code>dotnet build --no-incremental -p:PublishAot=true
Program.cs(6,1): warning IL2026: Using member &#x27;Microsoft.Extensions.DependencyInjection.MvcServiceCollectionExtensions.AddControllers(IServiceCollection)&#x27; which has &#x27;RequiresUnreferencedCodeAttribute&#x27; can break functionality when trimming application code. MVC does not currently support trimming or native AOT. https://aka.ms/aspnet/trimming</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/publishaot-addcontrollers-il2026-warning-mvc-does-not-support-native-aot.png" alt="PublishAot build of the ASP.NET Core orders API in VS Code: warning IL2026 on AddControllers says MVC does not currently support trimming or native AOT, while the minimal API endpoints raise no warning" class="wp-image-2829" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/publishaot-addcontrollers-il2026-warning-mvc-does-not-support-native-aot.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/publishaot-addcontrollers-il2026-warning-mvc-does-not-support-native-aot-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/publishaot-addcontrollers-il2026-warning-mvc-does-not-support-native-aot-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/publishaot-addcontrollers-il2026-warning-mvc-does-not-support-native-aot-768x674.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">One warning, and it&#8217;s on AddControllers: the minimal endpoints are AOT-clean</figcaption></figure>
</div>


<p class="wp-block-paragraph">The only warning landed on <code>AddControllers</code>. The minimal endpoints passed. For a real AOT app you&#8217;d also start from <code>dotnet new webapiaot</code>, use <code>CreateSlimBuilder</code> and register a <code>JsonSerializerContext</code>.</p>



<h2 class="wp-block-heading">Organizing Minimal APIs at Scale</h2>



<p class="wp-block-paragraph">The fair complaint about minimal APIs is a 900-line <code>Program.cs</code>. That&#8217;s a habit, not a limit. My <code>OrderEndpoints.cs</code> already shows the pattern I use on bigger projects:</p>



<ul class="wp-block-list">
<li>One static class per resource with a <code>Map*Endpoints</code> extension method, so <code>Program.cs</code> stays at one line per feature.</li>

<li>One <code>MapGroup</code> per resource for the prefix, tags, filters and <code>RequireAuthorization()</code>.</li>

<li>Named static handlers instead of long lambdas, so they&#8217;re testable and show up in stack traces.</li>
</ul>



<p class="wp-block-paragraph">You can see the same layout on a full EF Core app in my <a href="https://aspdotnethelp.com/aspnet-core-web-api-crud-ef-core/">ASP.NET Core Web API CRUD tutorial</a>. Controllers give you this folder structure by convention, which is why large teams that already know MVC often stay with them.</p>



<h2 class="wp-block-heading">When to Use Controllers Instead of Minimal APIs</h2>



<p class="wp-block-paragraph">Here&#8217;s how I decide for a new endpoint today:</p>



<ul class="wp-block-list">
<li>New API on .NET 10, microservice or AOT target: <strong>minimal APIs</strong>.</li>

<li>You need custom <code>IModelBinder</code> providers, <code>IModelValidator</code>, application parts or OData: <strong>controllers</strong>.</li>

<li>Big existing MVC codebase with a team that knows it: <strong>keep controllers</strong>, and add new endpoints as minimal APIs if you like.</li>

<li>Mixed needs: <strong>both in one app</strong>, exactly like this sample.</li>
</ul>



<p class="wp-block-paragraph">So the deciding differences are small: validation and OpenAPI now work in both, minimal APIs usually edge ahead on speed and win on testing and AOT, and controllers keep the MVC extensibility points. Check <a href="https://aspdotnethelp.com/whats-new-in-dotnet-10/">what changed in .NET 10</a> before you plan a migration.</p>



<p class="wp-block-paragraph">A few more ASP.NET Core and C# guides from the site:</p>



<ul class="wp-block-list">
<li><a href="https://aspdotnethelp.com/asp-net-core-tutorials/">All ASP.NET Core tutorials on AspDotnetHelp</a></li>

<li><a href="https://aspdotnethelp.com/interface-in-csharp/">Interfaces in C# (IEndpointFilter is one)</a></li>

<li><a href="https://aspdotnethelp.com/linq-in-csharp/">LINQ in C# for filtering results like the state query</a></li>

<li><a href="https://aspdotnethelp.com/dot-net-core-interview-questions-and-answers/">.NET Core interview questions and answers</a></li>
</ul>



<h2 class="wp-block-heading">Frequently Asked Questions</h2>


<div id="rank-math-faq" class="rank-math-block">
<div class="rank-math-list ">
<div id="faq-question-mvc0000001" class="rank-math-list-item">
<h3 class="rank-math-question ">Can I use minimal APIs and controllers in the same project?</h3>
<div class="rank-math-answer ">

<p>Yes. Call <code>AddControllers()</code> and <code>MapControllers()</code> next to your <code>Map*</code> calls. The sample in this post serves both from one app and one service container.</p>

</div>
</div>
<div id="faq-question-mvc0000002" class="rank-math-list-item">
<h3 class="rank-math-question ">Does dotnet new webapi create minimal APIs or controllers?</h3>
<div class="rank-math-answer ">

<p>Minimal APIs. Add <code>--use-controllers</code> (or <code>-controllers</code>) to get a controller-based project instead.</p>

</div>
</div>
<div id="faq-question-mvc0000003" class="rank-math-list-item">
<h3 class="rank-math-question ">Why does my minimal API return a 404 with no body?</h3>
<div class="rank-math-answer ">

<p><code>TypedResults.NotFound()</code> writes no body, and it stayed empty in my test even after <code>AddProblemDetails()</code>. Return <code>TypedResults.Problem(statusCode: 404)</code> when clients need a ProblemDetails body.</p>

</div>
</div>
<div id="faq-question-mvc0000004" class="rank-math-list-item">
<h3 class="rank-math-question ">Do minimal APIs support authorization like [Authorize]?</h3>
<div class="rank-math-answer ">

<p>Yes. Put <code>[Authorize]</code> on the handler or call <code>RequireAuthorization()</code> on an endpoint or a whole <code>MapGroup</code>.</p>

</div>
</div>
<div id="faq-question-mvc0000005" class="rank-math-list-item">
<h3 class="rank-math-question ">Are minimal APIs only for small projects?</h3>
<div class="rank-math-answer ">

<p>No. With one endpoint class and one MapGroup per resource they scale fine, and Microsoft recommends them for new projects. See <a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/apis?view=aspnetcore-10.0" target="_blank" rel="noopener">APIs overview on Microsoft Learn</a>.</p>

</div>
</div>
</div>
</div>]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Nullable Reference Types in C#: Enable Them and Fix the Warnings</title>
		<link>https://aspdotnethelp.com/csharp-nullable-reference-types/</link>
		
		<dc:creator><![CDATA[Bijay Kumar]]></dc:creator>
		<pubDate>Wed, 11 Nov 2026 15:15:00 +0000</pubDate>
				<category><![CDATA[C#]]></category>
		<guid isPermaLink="false">https://aspdotnethelp.com/?p=2792</guid>

					<description><![CDATA[The most common mistake when a team turns on C# nullable reference types is silencing every warning with the ! operator. The build goes quiet, but the app still crashes, because ! only hides the warning: Quick answer: keep &#60;Nullable&#62;enable&#60;/Nullable&#62; on, mark values that can be missing as string?, and fix each warning with a ... <a title="Nullable Reference Types in C#: Enable Them and Fix the Warnings" class="read-more" href="https://aspdotnethelp.com/csharp-nullable-reference-types/" aria-label="Read more about Nullable Reference Types in C#: Enable Them and Fix the Warnings">Read more &#62;&#62;</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">The most common mistake when a team turns on <strong>C# nullable reference types</strong> is silencing every warning with the <code>!</code> operator. The build goes quiet, but the app still crashes, because <code>!</code> only hides the warning:</p>



<pre class="wp-block-code"><code>var tyler = FindCustomer(&quot;TX-1042&quot;);
Console.WriteLine(tyler!.Email!.ToUpper());

static Customer? FindCustomer(string id) =&gt;
    id == &quot;TX-1042&quot; ? new Customer(&quot;Tyler Brooks&quot;, null) : null;

record Customer(string Name, string? Email);</code></pre>



<pre class="wp-block-code"><code>Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
   at Program.&lt;Main&gt;$(String[] args) in C:\DotNetDemo\NullableDemo\Crash.cs:line 2</code></pre>



<p class="wp-block-paragraph"><strong>Quick answer:</strong> keep <code>&lt;Nullable&gt;enable&lt;/Nullable&gt;</code> on, mark values that can be missing as <code>string?</code>, and fix each warning with a null check, <code>??</code>, a constructor or a <code>required</code> member.</p>



<p class="wp-block-paragraph">Below is the playbook we follow on .NET 10, with real compiler output for each warning.</p>



<h2 class="wp-block-heading">What You Need Before You Turn On Nullable Reference Types</h2>



<ul class="wp-block-list">
<li>The .NET 10 SDK. Every demo is a single-file app (<a href="https://aspdotnethelp.com/dotnet-run-file-based-apps/">how dotnet run app.cs works</a>), plus one small project.</li>

<li>The sample: a US customer and order book (Tyler Brooks in Austin, Jenna Morales, ZIP codes, shipping addresses).</li>

<li>One habit: nullable warnings print only when the compiler really runs. A cached <code>dotnet run</code> prints nothing, so I use <code>dotnet run --no-cache File.cs</code> or <code>dotnet build --no-incremental</code>.</li>
</ul>



<h2 class="wp-block-heading">Step 1: Enable Nullable in the .csproj (c# nullable enable)</h2>



<p class="wp-block-paragraph">New projects from the .NET 6 and later templates already have this line, and .NET 10 file-based apps turn it on by default. Older projects need it added to the <code>PropertyGroup</code> (not sure which SDK you&#8217;re on? <a href="https://aspdotnethelp.com/check-dotnet-version/">check your .NET version</a> first):</p>



<pre class="wp-block-code"><code>&lt;Nullable&gt;enable&lt;/Nullable&gt;</code></pre>



<p class="wp-block-paragraph">The setting takes four values, and the same names work in a <code>#nullable</code> directive at the top of a file:</p>



<figure class="wp-block-table"><table><thead><tr><th>Value</th><th><code>?</code> annotations</th><th>Warnings</th><th>When we use it</th></tr></thead><tbody><tr><td><code>enable</code></td><td>On</td><td>On</td><td>Every new project; the end goal</td></tr><tr><td><code>disable</code></td><td>Off</td><td>Off</td><td>Old default; legacy code you aren&#8217;t touching</td></tr><tr><td><code>warnings</code></td><td>Off</td><td>On</td><td>Phase 1 of a migration: find real null bugs first</td></tr><tr><td><code>annotations</code></td><td>On</td><td>Off</td><td>Library authors annotating a public API first</td></tr></tbody></table></figure>



<p class="wp-block-paragraph"><code>#nullable enable</code>, <code>#nullable disable</code> and <code>#nullable restore</code> override the project value for the lines that follow; <code>restore</code> goes back to the .csproj setting.</p>



<h2 class="wp-block-heading">Does string? Change Anything at Run Time?</h2>



<p class="wp-block-paragraph">No. A nullable reference type is a compile-time annotation only. <code>string</code> and <code>string?</code> are the same <code>System.String</code>, and nothing stops a <code>null</code> from getting in:</p>



<pre class="wp-block-code"><code>string name = &quot;Jenna Morales&quot;;
string? nickname = null;

Console.WriteLine(name.GetType() == typeof(string));
Console.WriteLine(nickname is null);

string forced = null!;            // no warning, still null
Console.WriteLine(forced is null);</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>True
True
True</code></pre>



<p class="wp-block-paragraph">All three lines print <code>True</code>. That&#8217;s the key difference from <a href="https://aspdotnethelp.com/convert-null-to-int-in-csharp/">nullable value types like int?</a>, where <code>int?</code> really is a different type (<code>Nullable&lt;int&gt;</code>) with <code>HasValue</code> and <code>Value</code>.</p>



<h2 class="wp-block-heading">Step 2: Read the Six Nullable Warnings You&#8217;ll See First</h2>



<p class="wp-block-paragraph">I packed five of the six warnings our team hits most into one file; CS8618 gets its own step. They only apply to <a href="https://aspdotnethelp.com/data-types-in-csharp/">reference types like string and classes</a>, and each line is marked with the warning it causes:</p>



<pre class="wp-block-code"><code>string zip = LookupZip(&quot;Boise&quot;);            // CS8600
string? city = LookupZip(&quot;Boise&quot;);
Console.WriteLine(city.Length);             // CS8602
string? label = LookupZip(&quot;Boise&quot;);
PrintLabel(label);                          // CS8604
PrintLabel(null);                           // CS8625
Console.WriteLine(Shorten(zip));

static string? LookupZip(string city) =&gt; city == &quot;Boise&quot; ? &quot;83702&quot; : null;
static string Shorten(string? text) =&gt; text; // CS8603
static void PrintLabel(string text) =&gt; Console.WriteLine($&quot;Label: {text}&quot;);</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>C:\DotNetDemo\NullableDemo\AllWarnings.cs(1,14): warning CS8600: Converting null literal or possible null value to non-nullable type.
C:\DotNetDemo\NullableDemo\AllWarnings.cs(3,19): warning CS8602: Dereference of a possibly null reference.
C:\DotNetDemo\NullableDemo\AllWarnings.cs(5,12): warning CS8604: Possible null reference argument for parameter &#x27;text&#x27; in &#x27;void PrintLabel(string text)&#x27;.
C:\DotNetDemo\NullableDemo\AllWarnings.cs(6,12): warning CS8625: Cannot convert null literal to non-nullable reference type.
C:\DotNetDemo\NullableDemo\AllWarnings.cs(10,40): warning CS8603: Possible null reference return.
5
Label: 83702
Label: 
83702</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="1032" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-nullable-warnings-cs8600-cs8602-cs8625-dotnet-run.png" alt="C# nullable reference type warnings CS8600, CS8602, CS8604, CS8625 and CS8603 printed by dotnet run --no-cache in the VS Code terminal on .NET 10" class="wp-image-2823" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-nullable-warnings-cs8600-cs8602-cs8625-dotnet-run.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-nullable-warnings-cs8600-cs8602-cs8625-dotnet-run-300x286.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-nullable-warnings-cs8600-cs8602-cs8625-dotnet-run-1024x975.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-nullable-warnings-cs8600-cs8602-cs8625-dotnet-run-768x731.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">One file, five nullable warnings, and the program still runs</figcaption></figure>
</div>


<p class="wp-block-paragraph">Every one is a warning, not an error, so the program still printed <code>5</code> and the labels. That&#8217;s why warnings pile up unless the team treats them as work.</p>



<figure class="wp-block-table"><table><thead><tr><th>Warning</th><th>What the compiler is telling you</th></tr></thead><tbody><tr><td>CS8600</td><td>You store a maybe-null value in a non-nullable variable</td></tr><tr><td>CS8602</td><td>You dereference (<code>.Length</code>, a method call) something that might be null</td></tr><tr><td>CS8603</td><td>A method declared to return <code>string</code> might return null</td></tr><tr><td>CS8604</td><td>You pass a maybe-null argument to a non-nullable parameter</td></tr><tr><td>CS8618</td><td>A non-nullable property or field is still null when the constructor ends</td></tr><tr><td>CS8625</td><td>You pass or assign the <code>null</code> literal to a non-nullable type</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Step 3: Fix CS8600, CS8602, CS8603, CS8604 and CS8625</h2>



<p class="wp-block-paragraph">Same ZIP lookup, now fixed. Reno isn&#8217;t in the lookup, so this run also proves the fallbacks work:</p>



<pre class="wp-block-code"><code>string zip = LookupZip(&quot;Reno&quot;) ?? &quot;00000&quot;;        // CS8600: ??
string? city = LookupZip(&quot;Reno&quot;);
Console.WriteLine(city?.Length ?? 0);             // CS8602: ?.
string? label = LookupZip(&quot;Boise&quot;);
if (label is not null) PrintLabel(label);         // CS8604: null check
PrintLabel(zip);                                  // CS8625: pass a real value
Console.WriteLine($&quot;[{Shorten(null)}]&quot;);          // CS8603: fallback

static string? LookupZip(string city) =&gt; city == &quot;Boise&quot; ? &quot;83702&quot; : null;
static string Shorten(string? text) =&gt; text ?? &quot;&quot;;
static void PrintLabel(string text)
{
    ArgumentNullException.ThrowIfNull(text);      // guard public APIs
    Console.WriteLine($&quot;Label: {text}&quot;);
}</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>0
Label: 83702
Label: 00000
[]</code></pre>



<p class="wp-block-paragraph">The build is clean. Reno&#8217;s city length falls back to 0, Boise prints its label, Reno&#8217;s ZIP becomes <code>00000</code>, and <code>Shorten(null)</code> returns an empty string. The <a href="https://aspdotnethelp.com/how-to-check-for-null-values-in-c-sharp/">ways to check for null in C#</a> cover <code>is null</code> versus <code>== null</code> in detail.</p>



<p class="wp-block-paragraph">Our rule of thumb: use <code>??</code> when there&#8217;s a sensible default, <code>?.</code> when skipping is fine, and a null check when the logic branches. For text that also can&#8217;t be blank, <a href="https://aspdotnethelp.com/check-if-a-string-is-not-null-or-empty-in-c-sharp-net/">string.IsNullOrEmpty</a> does both checks, and the compiler understands it.</p>



<p class="wp-block-paragraph"><strong>Note:</strong> annotations don&#8217;t stop callers with nullable turned off from passing null. Public methods still need <code>ArgumentNullException.ThrowIfNull</code>.</p>



<h2 class="wp-block-heading">Step 4: Fix CS8618 with a Constructor, required or a Default</h2>



<p class="wp-block-paragraph">CS8618 is the warning that floods a project on day one, mostly from model classes like this one:</p>



<pre class="wp-block-code"><code>var contact = new Contact();
Console.WriteLine(contact.Name is null);

class Contact
{
    public string Name { get; set; }
}</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>C:\DotNetDemo\NullableDemo\Cs8618.cs(6,19): warning CS8618: Non-nullable property &#x27;Name&#x27; must contain a non-null value when exiting constructor. Consider adding the &#x27;required&#x27; modifier or declaring the property as nullable.
True</code></pre>



<p class="wp-block-paragraph">The warning even suggests the fixes, and the second line proves the point: <code>Name</code> really is null. There are four honest fixes, and I put all four in one file:</p>



<pre class="wp-block-code"><code>var tyler = new Customer(&quot;Tyler Brooks&quot;, &quot;tyler@example.com&quot;);
var jenna = new Contact { Name = &quot;Jenna Morales&quot; };

Console.WriteLine($&quot;{tyler.Name} &lt;{tyler.Email}&gt;&quot;);
Console.WriteLine($&quot;{jenna.Name}, {jenna.Phone ?? &quot;no phone&quot;}&quot;);
Console.WriteLine($&quot;Notes: &#x27;{jenna.Notes}&#x27;&quot;);

class Customer(string name, string email)   // 1
{
    public string Name { get; } = name;
    public string Email { get; } = email;
}

class Contact
{
    public required string Name { get; init; }  // 2
    public string? Phone { get; init; }         // 3
    public string Notes { get; set; } = &quot;&quot;;     // 4
}</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>Tyler Brooks &lt;tyler@example.com&gt;
Jenna Morales, no phone
Notes: &#x27;&#x27;</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="1002" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-fix-cs8618-required-member-constructor-vscode.png" alt="C# CS8618 fixed four ways in VS Code: primary constructor, required member, nullable string? property and a default value, with clean dotnet run output" class="wp-image-2820" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-fix-cs8618-required-member-constructor-vscode.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-fix-cs8618-required-member-constructor-vscode-300x277.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-fix-cs8618-required-member-constructor-vscode-1024x947.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-fix-cs8618-required-member-constructor-vscode-768x710.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">Four fixes for CS8618, and the build prints no warnings</figcaption></figure>
</div>


<p class="wp-block-paragraph">In order: 1 is a <a href="https://aspdotnethelp.com/constructor-in-csharp/">constructor</a> for values known at creation, 2 is <code>required</code> (C# 11+) for object-initializer models, 3 is <code>string?</code> for values that can really be missing, and 4 is a default like <code>""</code>, only when empty means something.</p>



<p class="wp-block-paragraph"><code>required</code> moves the check to the caller. Forget the property and you get a compile error instead of a null:</p>



<pre class="wp-block-code"><code>var contact = new Contact();
Console.WriteLine(contact.Name);

class Contact
{
    public required string Name { get; init; }
}</code></pre>



<p class="wp-block-paragraph"><strong>Build output:</strong></p>



<pre class="wp-block-code"><code>C:\DotNetDemo\NullableDemo\RequiredMissing.cs(1,19): error CS9035: Required member &#x27;Contact.Name&#x27; must be set in the object initializer or attribute constructor.

The build failed. Fix the build errors and run again.</code></pre>



<p class="wp-block-paragraph">That&#8217;s CS9035, and it&#8217;s exactly what we want: the missing name is caught at build time, not in production.</p>



<h2 class="wp-block-heading">Step 5: Use the Null-Forgiving Operator (!) Only as a Last Resort</h2>



<p class="wp-block-paragraph">The <code>!</code> in the opening example tells the compiler &#8220;trust me&#8221;. It changes nothing at run time, which is why Tyler&#8217;s missing email still threw <code>NullReferenceException</code>.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="1002" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-null-forgiving-operator-nullreferenceexception-vscode.png" alt="C# null-forgiving operator ! hiding nullable warnings while dotnet run still throws System.NullReferenceException for a missing customer email, in VS Code" class="wp-image-2821" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-null-forgiving-operator-nullreferenceexception-vscode.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-null-forgiving-operator-nullreferenceexception-vscode-300x277.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-null-forgiving-operator-nullreferenceexception-vscode-1024x947.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-null-forgiving-operator-nullreferenceexception-vscode-768x710.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">The ! operator silences the warning, not the crash</figcaption></figure>
</div>


<p class="wp-block-paragraph">We allow <code>!</code> in two places only: tests that pass <code>null!</code> on purpose, and members a framework fills in, like an EF Core <code>DbSet&lt;T&gt;</code> declared <code>= null!</code> (our <a href="https://aspdotnethelp.com/aspnet-core-web-api-crud-ef-core/">Web API CRUD with EF Core</a> build skips even that with <code>=&gt; Set&lt;Product&gt;()</code>).</p>



<p class="wp-block-paragraph">Everywhere else, a pull request with <code>!</code> needs a comment saying why.</p>



<h2 class="wp-block-heading">Step 6: Teach the Compiler with [NotNullWhen], [MaybeNull] and [MemberNotNull]</h2>



<p class="wp-block-paragraph">The compiler doesn&#8217;t look inside your methods, so a helper that checks for null still leaves callers with warnings. The attributes in <code>System.Diagnostics.CodeAnalysis</code> describe the contract instead:</p>



<pre class="wp-block-code"><code>using System.Diagnostics.CodeAnalysis;

var book = new CustomerBook();
if (book.TryGetEmail(&quot;Tyler Brooks&quot;, out string? email))
    Console.WriteLine(email.ToUpperInvariant());     // no CS8602
Console.WriteLine(book.Region.Length);
string[] states = [&quot;Texas&quot;, &quot;Ohio&quot;];
Console.WriteLine(FirstMatch(states, s =&gt; s.StartsWith(&#x27;V&#x27;)) is null);

[return: MaybeNull]
static T FirstMatch&lt;T&gt;(IEnumerable&lt;T&gt; items, Func&lt;T, bool&gt; match)
{
    foreach (T item in items) if (match(item)) return item;
    return default;
}

class CustomerBook
{
    private readonly Dictionary&lt;string, string&gt; emails = new() { [&quot;Tyler Brooks&quot;] = &quot;tyler.brooks@example.com&quot; };
    public string Region { get; private set; }
    public CustomerBook() =&gt; Load();

    [MemberNotNull(nameof(Region))]
    private void Load() =&gt; Region = &quot;Pacific Northwest&quot;;

    public bool TryGetEmail(string name, [NotNullWhen(true)] out string? email) =&gt;
        emails.TryGetValue(name, out email);
}</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>TYLER.BROOKS@EXAMPLE.COM
17
True</code></pre>



<p class="wp-block-paragraph">No warnings. <code>[NotNullWhen(true)]</code> says <code>email</code> isn&#8217;t null when <code>TryGetEmail</code> returns true, <code>[MemberNotNull]</code> says <code>Load()</code> sets <code>Region</code>, so there&#8217;s no CS8618, and <code>[return: MaybeNull]</code> lets the generic method return <code>default</code>.</p>



<p class="wp-block-paragraph">The output is the uppercase email, 17 (the length of Pacific Northwest), and <code>True</code> because no state starts with V. The <a href="https://aspdotnethelp.com/csharp-pattern-matching/">pattern matching guide</a> shows how <code>is { }</code> and <code>is not null</code> narrow null the same way.</p>



<h2 class="wp-block-heading">Step 7: Turn Nullable Warnings into Build Errors</h2>



<p class="wp-block-paragraph">Warnings that never fail a build get ignored. Once a project is clean, we lock it with <code>WarningsAsErrors</code>. Here&#8217;s the file from a plain <a href="https://aspdotnethelp.com/create-a-console-application-in-visual-studio-code/">console project in VS Code</a>:</p>



<pre class="wp-block-code"><code>&lt;Project Sdk=&quot;Microsoft.NET.Sdk&quot;&gt;

  &lt;PropertyGroup&gt;
    &lt;OutputType&gt;Exe&lt;/OutputType&gt;
    &lt;TargetFramework&gt;net10.0&lt;/TargetFramework&gt;
    &lt;ImplicitUsings&gt;enable&lt;/ImplicitUsings&gt;
    &lt;Nullable&gt;enable&lt;/Nullable&gt;
    &lt;WarningsAsErrors&gt;nullable&lt;/WarningsAsErrors&gt;
  &lt;/PropertyGroup&gt;

&lt;/Project&gt;</code></pre>



<p class="wp-block-paragraph">And a <code>Program.cs</code> with two classic bugs, a missing <code>Customer</code> initializer and an unchecked <code>ShipTo</code>:</p>



<pre class="wp-block-code"><code>var order = new Order { Id = 1042, Customer = &quot;Tyler Brooks&quot; };
Console.WriteLine(order.ShipTo.City);

class Order
{
    public int Id { get; init; }
    public string Customer { get; init; }
    public Address? ShipTo { get; init; }
}

record Address(string City, string State);</code></pre>



<p class="wp-block-paragraph"><strong>dotnet build output:</strong></p>



<pre class="wp-block-code"><code>  Determining projects to restore...
  Restored C:\DotNetDemo\NullableDemo\Orders\Orders.csproj (in 73 ms).
C:\DotNetDemo\NullableDemo\Orders\Program.cs(7,19): error CS8618: Non-nullable property &#x27;Customer&#x27; must contain a non-null value when exiting constructor. Consider adding the &#x27;required&#x27; modifier or declaring the property as nullable. [C:\DotNetDemo\NullableDemo\Orders\Orders.csproj]
C:\DotNetDemo\NullableDemo\Orders\Program.cs(2,19): error CS8602: Dereference of a possibly null reference. [C:\DotNetDemo\NullableDemo\Orders\Orders.csproj]
Build FAILED.
C:\DotNetDemo\NullableDemo\Orders\Program.cs(7,19): error CS8618: Non-nullable property &#x27;Customer&#x27; must contain a non-null value when exiting constructor. Consider adding the &#x27;required&#x27; modifier or declaring the property as nullable. [C:\DotNetDemo\NullableDemo\Orders\Orders.csproj]
C:\DotNetDemo\NullableDemo\Orders\Program.cs(2,19): error CS8602: Dereference of a possibly null reference. [C:\DotNetDemo\NullableDemo\Orders\Orders.csproj]
    0 Warning(s)
    2 Error(s)
Time Elapsed 00:00:01.18</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="1002" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-warningsaserrors-nullable-build-errors-cs8602-cs8618.png" alt="dotnet build failing with error CS8602 and error CS8618 because the .csproj sets WarningsAsErrors to nullable, shown in the VS Code terminal" class="wp-image-2824" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-warningsaserrors-nullable-build-errors-cs8602-cs8618.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-warningsaserrors-nullable-build-errors-cs8602-cs8618-300x277.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-warningsaserrors-nullable-build-errors-cs8602-cs8618-1024x947.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-warningsaserrors-nullable-build-errors-cs8602-cs8618-768x710.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">WarningsAsErrors set to nullable turns CS8602 and CS8618 into build errors</figcaption></figure>
</div>


<p class="wp-block-paragraph">The string <code>nullable</code> covers every nullability warning, so new ones fail CI without a list of numbers. <code>&lt;TreatWarningsAsErrors&gt;true&lt;/TreatWarningsAsErrors&gt;</code> fails on every warning, nullable or not; we turn that on only in clean, mature projects.</p>



<h2 class="wp-block-heading">How Do You Migrate an Existing Project Gradually?</h2>



<p class="wp-block-paragraph">Don&#8217;t flip a 300-file project to <code>enable</code> in one pull request. Set the project to <code>enable</code>, add <code>#nullable disable</code> to the files you haven&#8217;t migrated, and remove one directive per file as you fix it:</p>



<pre class="wp-block-code"><code>Console.WriteLine(LegacyFormat(null, null));
Console.WriteLine(ModernFormat(null));

#nullable disable
static string LegacyFormat(string name, string? suffix) =&gt;
    name == null ? &quot;(no name)&quot; : name.ToUpper() + suffix;
#nullable restore

static string ModernFormat(string? name) =&gt;
    name is null ? &quot;(no name)&quot; : name.ToUpper();</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>C:\DotNetDemo\NullableDemo\LegacyHelper.cs(5,47): warning CS8632: The annotation for nullable reference types should only be used in code within a &#x27;#nullable&#x27; annotations context.
(no name)
(no name)</code></pre>



<p class="wp-block-paragraph">Both calls print <code>(no name)</code> and <code>LegacyFormat(null, null)</code> raises no warning, because its parameters are oblivious. The <code>string?</code> inside the disabled region causes CS8632, a sign that file is half-migrated.</p>



<figure class="wp-block-table"><table><thead><tr><th>Strategy</th><th>Project setting</th><th>Best for</th></tr></thead><tbody><tr><td>Enable by default</td><td><code>enable</code> + <code>#nullable disable</code> in old files</td><td>Active apps; new files start strict</td></tr><tr><td>Disable by default</td><td><code>disable</code> + <code>#nullable enable</code> per migrated file</td><td>Stable code that rarely changes</td></tr><tr><td>Warnings first</td><td><code>warnings</code>, then <code>enable</code></td><td>Hunting real NullReferenceException bugs</td></tr><tr><td>Annotations first</td><td><code>annotations</code>, then <code>enable</code></td><td>Libraries publishing a NuGet API</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Start with the leaf classes (models, helpers) and work outward, since annotating a type creates warnings in its callers. Moving to .NET 10 at the same time? Do the <a href="https://aspdotnethelp.com/upgrade-dotnet-8-to-dotnet-10/">.NET 8 to .NET 10 upgrade</a> first, then the nullable pass.</p>



<h2 class="wp-block-heading">Our Team Rules for Nullable Reference Types</h2>



<ol class="wp-block-list">
<li><code>&lt;Nullable&gt;enable&lt;/Nullable&gt;</code> in every project; no new <code>#nullable disable</code>.</li>

<li><code>?</code> means &#8220;can really be missing&#8221;. If it can&#8217;t, fix the initialization instead.</li>

<li><code>!</code> and <code>= null!</code> need a code comment, and only for tests or framework-filled members.</li>

<li>Models use constructors or <code>required</code>; no empty-string defaults just to quiet CS8618.</li>

<li>Clean projects set <code>&lt;WarningsAsErrors&gt;nullable&lt;/WarningsAsErrors&gt;</code>.</li>
</ol>



<h2 class="wp-block-heading">Common Mistakes to Avoid</h2>



<figure class="wp-block-table"><table><thead><tr><th>Mistake</th><th>What happens</th><th>Fix</th></tr></thead><tbody><tr><td><code>!</code> on every warning</td><td>Silence now, NullReferenceException later</td><td>Null check, <code>??</code> or a real value</td></tr><tr><td>Expecting <code>string?</code> to block nulls at run time</td><td>It&#8217;s compile-time only</td><td>Guard public methods</td></tr><tr><td>Plain <code>dotnet run</code> after a fix</td><td>Cached build shows no warnings</td><td><code>--no-cache</code> or <code>--no-incremental</code></td></tr><tr><td><code>= ""</code> on every property</td><td>Empty strings hide missing data</td><td><code>required</code> or a constructor</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Next Steps</h2>



<p class="wp-block-paragraph">Once warnings are under control, try the newer null-friendly syntax, such as null-conditional assignment (<code>customer?.Email = ...</code>) in <a href="https://aspdotnethelp.com/whats-new-in-csharp-14/">what is new in C# 14</a>. Records also pair well with non-nullable properties; see <a href="https://aspdotnethelp.com/csharp-record-vs-class-vs-struct/">record vs class vs struct</a>.</p>



<p class="wp-block-paragraph">Good follow-ups from our team&#8217;s reading list:</p>



<ul class="wp-block-list">
<li><a href="https://aspdotnethelp.com/try-catch-in-csharp/">Try-catch in C#, for the NullReferenceException you still need to handle</a></li>

<li><a href="https://aspdotnethelp.com/interface-in-csharp/">Interfaces in C#, where nullability must match the implementation</a></li>

<li><a href="https://aspdotnethelp.com/whats-new-in-dotnet-10/">What&#8217;s new in .NET 10</a></li>

<li><a href="https://aspdotnethelp.com/csharp-tutorials/">All C# tutorials on AspDotnetHelp</a></li>
</ul>



<h2 class="wp-block-heading">Frequently Asked Questions</h2>


<div id="rank-math-faq" class="rank-math-block">
<div class="rank-math-list ">
<div id="faq-question-27920000001" class="rank-math-list-item">
<h3 class="rank-math-question ">Are nullable reference types enabled by default in C#?</h3>
<div class="rank-math-answer ">

<p>In new projects, yes: the .NET 6 and later templates add &lt;Nullable&gt;enable&lt;/Nullable&gt;, and .NET 10 file-based apps enable it by default. Older projects stay disabled until you add the setting.</p>

</div>
</div>
<div id="faq-question-27920000002" class="rank-math-list-item">
<h3 class="rank-math-question ">Do nullable reference types prevent NullReferenceException?</h3>
<div class="rank-math-answer ">

<p>No. They&#8217;re compile-time warnings only. A null can still reach a non-nullable variable through ! , older libraries or callers with nullable disabled, so guard public methods.</p>

</div>
</div>
<div id="faq-question-27920000003" class="rank-math-list-item">
<h3 class="rank-math-question ">How do I fix CS8618 in C#?</h3>
<div class="rank-math-answer ">

<p>Set the member in a constructor, mark it required (C# 11+), give it a meaningful default, or declare it nullable with ? if it can really be missing. Use = null! only for members a framework fills in.</p>

</div>
</div>
<div id="faq-question-27920000004" class="rank-math-list-item">
<h3 class="rank-math-question ">What is the difference between string and string? in C#?</h3>
<div class="rank-math-answer ">

<p>Nothing at run time; both are System.String. The ? tells the compiler the value can be null, so it warns when you dereference it without a check.</p>

</div>
</div>
<div id="faq-question-27920000005" class="rank-math-list-item">
<h3 class="rank-math-question ">How do I make nullable warnings errors?</h3>
<div class="rank-math-answer ">

<p>Add &lt;WarningsAsErrors&gt;nullable&lt;/WarningsAsErrors&gt; to the project file. The <a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-options/errors-warnings" target="_blank" rel="noopener">errors and warnings compiler options</a> page on Microsoft Learn documents the nullable shorthand.</p>

</div>
</div>
<div id="faq-question-27920000006" class="rank-math-list-item">
<h3 class="rank-math-question ">Where is the full list of nullable warnings?</h3>
<div class="rank-math-answer ">

<p>The <a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/nullable-warnings" target="_blank" rel="noopener">nullable reference type warnings</a> page covers every CS86xx warning and the fix for each.</p>

</div>
</div>
</div>
</div>]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Blazor Components and Parameters (with Examples) in .NET 10</title>
		<link>https://aspdotnethelp.com/blazor-components/</link>
		
		<dc:creator><![CDATA[Bijay Kumar]]></dc:creator>
		<pubDate>Tue, 10 Nov 2026 21:20:00 +0000</pubDate>
				<category><![CDATA[Blazor]]></category>
		<guid isPermaLink="false">https://aspdotnethelp.com/?p=2791</guid>

					<description><![CDATA[Say you run the web store for a Portland outdoor gear shop. The product page repeats the same card markup for six items, and every price fix means editing six copies. Blazor components fix that: you write the card once as a .razor file, pass each product in with [Parameter] properties, and send clicks back ... <a title="Blazor Components and Parameters (with Examples) in .NET 10" class="read-more" href="https://aspdotnethelp.com/blazor-components/" aria-label="Read more about Blazor Components and Parameters (with Examples) in .NET 10">Read more &#62;&#62;</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Say you run the web store for a Portland outdoor gear shop. The product page repeats the same card markup for six items, and every price fix means editing six copies.</p>



<p class="wp-block-paragraph"><strong>Blazor components</strong> fix that: you write the card once as a <code>.razor</code> file, pass each product in with <code>[Parameter]</code> properties, and send clicks back to the page with <code>EventCallback</code>. We&#8217;ll build that store on .NET 10, one component at a time.</p>



<p class="wp-block-paragraph">Here&#8217;s what we&#8217;re building:</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1102" height="975" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-components-finished-shop-product-cards-order-list.png" alt="Blazor components demo: the finished Cascade Trail Outfitters shop on localhost:5791 with six ProductCard components, free shipping badges and an order panel showing Subtotal: $477.94 (4 items)" class="wp-image-2809" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-components-finished-shop-product-cards-order-list.png 1102w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-components-finished-shop-product-cards-order-list-300x265.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-components-finished-shop-product-cards-order-list-1024x906.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-components-finished-shop-product-cards-order-list-768x679.png 768w" sizes="auto, (max-width: 1102px) 100vw, 1102px" /><figcaption class="wp-element-caption">The finished shop after three real clicks in headless Edge</figcaption></figure>
</div>


<ul class="wp-block-list">
<li><code>ProductCard</code>: one card per product, with a quantity stepper and an Add to cart button</li>

<li><code>Panel</code>: a titled box that wraps whatever markup you put inside it</li>

<li><code>OrderList</code>: the cart lines and subtotal, with its C# in a code-behind file</li>
</ul>



<h2 class="wp-block-heading">Before You Start Building Blazor Components</h2>



<p class="wp-block-paragraph">You need the .NET 10 SDK (I used 10.0.402) and an editor; I use VS Code. If <code>dotnet --version</code> doesn&#8217;t print 10.x, <a href="https://aspdotnethelp.com/check-dotnet-version/">check which .NET versions you have</a> first.</p>



<p class="wp-block-paragraph">If you&#8217;ve never run a Blazor app, my <a href="https://aspdotnethelp.com/blazor-tutorial-for-beginners/">Blazor tutorial for beginners</a> covers pages and routing. This post stays on components. The sample data is six products, all priced in USD:</p>



<figure class="wp-block-table"><table><thead><tr><th>Id</th><th>Product</th><th>Category</th><th>Price</th></tr></thead><tbody><tr><td>1</td><td>Trail Daypack 24L</td><td>Packs</td><td>$64.99</td></tr><tr><td>2</td><td>Insulated Water Bottle</td><td>Hydration</td><td>$24.50</td></tr><tr><td>3</td><td>Two-Person Tent</td><td>Camping</td><td>$189.00</td></tr><tr><td>4</td><td>Headlamp 400</td><td>Lighting</td><td>$34.95</td></tr><tr><td>5</td><td>Rain Shell Jacket</td><td>Apparel</td><td>$119.00</td></tr><tr><td>6</td><td>Camp Stove</td><td>Cooking</td><td>$79.99</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">What Is a Blazor Component?</h2>



<p class="wp-block-paragraph">A Blazor component is a <code>.razor</code> file that mixes HTML markup with a C# <code>@code</code> block. The compiler turns each file into a C# partial class that inherits <code>ComponentBase</code>, named after the file. That&#8217;s plain <a href="https://aspdotnethelp.com/inheritance-in-csharp/">C# inheritance</a> at work.</p>



<figure class="wp-block-table"><table><thead><tr><th>Piece</th><th>What it does</th></tr></thead><tbody><tr><td><code>ProductCard.razor</code></td><td>File name = class name. It must start with an uppercase letter</td></tr><tr><td><code>@code { ... }</code></td><td>Fields, methods and parameters of the class</td></tr><tr><td><code>[Parameter]</code></td><td>A public property the parent sets as an attribute</td></tr><tr><td><code>@page &quot;/shop&quot;</code></td><td>Turns a component into a routable page</td></tr><tr><td><code>&lt;ProductCard ... /&gt;</code></td><td>Uses the component inside another one</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">A page is just a component with <code>@page</code>. Everything below works the same in both.</p>



<h2 class="wp-block-heading">Step 1: Create a Blazor Web App in .NET 10</h2>



<p class="wp-block-paragraph">Create the app from the Blazor Web App template. Its default interactivity is Server, applied per page:</p>



<pre class="wp-block-code"><code>dotnet new blazor -o BlazorComponents
cd BlazorComponents</code></pre>



<p class="wp-block-paragraph">I deleted the two <code>appsettings</code> files (this demo doesn&#8217;t use them) and trimmed <code>Properties/launchSettings.json</code> to one http profile on port 5791:</p>



<pre class="wp-block-code"><code>{
  &quot;$schema&quot;: &quot;https://json.schemastore.org/launchsettings.json&quot;,
  &quot;profiles&quot;: {
    &quot;http&quot;: {
      &quot;commandName&quot;: &quot;Project&quot;,
      &quot;dotnetRunMessages&quot;: true,
      &quot;launchBrowser&quot;: false,
      &quot;applicationUrl&quot;: &quot;http://localhost:5791&quot;,
      &quot;environmentVariables&quot;: {
        &quot;ASPNETCORE_ENVIRONMENT&quot;: &quot;Development&quot;
      }
    }
  }
}</code></pre>



<p class="wp-block-paragraph">The project file and Program.cs are what the template wrote for <a href="https://aspdotnethelp.com/whats-new-in-dotnet-10/">.NET 10</a>. Program.cs already registers the Interactive Server services we&#8217;ll need in Step 4:</p>



<pre class="wp-block-code"><code>&lt;Project Sdk=&quot;Microsoft.NET.Sdk.Web&quot;&gt;

  &lt;PropertyGroup&gt;
    &lt;TargetFramework&gt;net10.0&lt;/TargetFramework&gt;
    &lt;Nullable&gt;enable&lt;/Nullable&gt;
    &lt;ImplicitUsings&gt;enable&lt;/ImplicitUsings&gt;
    &lt;BlazorDisableThrowNavigationException&gt;true&lt;/BlazorDisableThrowNavigationException&gt;
  &lt;/PropertyGroup&gt;

&lt;/Project&gt;</code></pre>



<pre class="wp-block-code"><code>using BlazorComponents.Components;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler(&quot;/Error&quot;, createScopeForErrors: true);
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}
app.UseStatusCodePagesWithReExecute(&quot;/not-found&quot;, createScopeForStatusCodePages: true);
app.UseHttpsRedirection();

app.UseAntiforgery();

app.MapStaticAssets();
app.MapRazorComponents&lt;App&gt;()
    .AddInteractiveServerRenderMode();

app.Run();</code></pre>



<p class="wp-block-paragraph">New components go in the <code>Components</code> folder. The template&#8217;s <code>_Imports.razor</code> already has <code>@using BlazorComponents.Components</code>, so pages can use them without extra lines.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1002" height="462" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-web-app-template-running-localhost-5791.png" alt="Blazor Web App template home page running on localhost:5791 with Hello, world! and the Home, Counter and Weather links" class="wp-image-2815" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-web-app-template-running-localhost-5791.png 1002w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-web-app-template-running-localhost-5791-300x138.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-web-app-template-running-localhost-5791-768x354.png 768w" sizes="auto, (max-width: 1002px) 100vw, 1002px" /><figcaption class="wp-element-caption">The untouched template on http://localhost:5791</figcaption></figure>
</div>


<p class="wp-block-paragraph"><strong>Checkpoint:</strong> <code>dotnet run</code> prints <code>Now listening on: http://localhost:5791</code>, and the browser shows &#8220;Hello, world!&#8221;.</p>



<h2 class="wp-block-heading">Step 2: Build Your First Blazor Component with [Parameter]</h2>



<p class="wp-block-paragraph">First, the data types in <code>Models/ShopModels.cs</code>. They&#8217;re <a href="https://aspdotnethelp.com/csharp-record-vs-class-vs-struct/">C# records</a>, so each one is a single line plus the cart total:</p>



<pre class="wp-block-code"><code>namespace BlazorComponents.Models;

public record Product(int Id, string Name, string Category, decimal Price);

public record CartLine(Product Product, int Quantity)
{
    public decimal Total =&gt; Product.Price * Quantity;
}

public record StoreInfo(string Name, string City, decimal FreeShippingOver);</code></pre>



<p class="wp-block-paragraph">Now the component, <code>Components/ProductCard.razor</code>. The <code>Product</code> property is the <code>[Parameter]</code> the parent fills in:</p>



<pre class="wp-block-code"><code>
@using System.Globalization
@using BlazorComponents.Models

&lt;div class=&quot;card mb-2&quot;&gt;
    &lt;div class=&quot;card-body py-2&quot;&gt;
        &lt;h5 class=&quot;card-title mb-1&quot;&gt;@Product.Name&lt;/h5&gt;
        &lt;p class=&quot;mb-0&quot;&gt;@Product.Category · @Product.Price.ToString(&quot;C&quot;, Us)&lt;/p&gt;
    &lt;/div&gt;
&lt;/div&gt;

@code {
    private static readonly CultureInfo Us = new(&quot;en-US&quot;);

    [Parameter, EditorRequired]
    public Product Product { get; set; } = default!;
}
</code></pre>



<p class="wp-block-paragraph">The <code>&quot;C&quot;</code> format with an en-US culture always prints dollars, even on a PC set to another region (more in <a href="https://aspdotnethelp.com/format-number-with-commas-and-decimal-in-csharp/">formatting numbers in C#</a>).</p>



<p class="wp-block-paragraph">Then the page, <code>Components/Pages/Shop.razor</code>. It loops over the products and passes each one as an attribute:</p>



<pre class="wp-block-code"><code>
@page &quot;/shop&quot;
@using BlazorComponents.Models

&lt;PageTitle&gt;Shop&lt;/PageTitle&gt;

&lt;h1&gt;Cascade Trail Outfitters&lt;/h1&gt;

&lt;div class=&quot;row&quot;&gt;
    &lt;div class=&quot;col-md-7&quot;&gt;
        @foreach (var product in products)
        {
            &lt;ProductCard Product=&quot;product&quot; /&gt;
        }
    &lt;/div&gt;
&lt;/div&gt;

@code {
    private List&lt;Product&gt; products =
    [
        new(1, &quot;Trail Daypack 24L&quot;, &quot;Packs&quot;, 64.99m),
        new(2, &quot;Insulated Water Bottle&quot;, &quot;Hydration&quot;, 24.50m),
        new(3, &quot;Two-Person Tent&quot;, &quot;Camping&quot;, 189.00m),
        new(4, &quot;Headlamp 400&quot;, &quot;Lighting&quot;, 34.95m),
        new(5, &quot;Rain Shell Jacket&quot;, &quot;Apparel&quot;, 119.00m),
        new(6, &quot;Camp Stove&quot;, &quot;Cooking&quot;, 79.99m),
    ];
}
</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1002" height="762" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-components-productcard-parameter-six-cards.png" alt="Blazor component parameter demo: six ProductCard components on localhost:5791/shop, from Trail Daypack 24L at $64.99 to Camp Stove at $79.99" class="wp-image-2811" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-components-productcard-parameter-six-cards.png 1002w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-components-productcard-parameter-six-cards-300x228.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-components-productcard-parameter-six-cards-768x584.png 768w" sizes="auto, (max-width: 1002px) 100vw, 1002px" /><figcaption class="wp-element-caption">One ProductCard component, six Product parameters</figcaption></figure>
</div>


<p class="wp-block-paragraph">Keep parameters as plain auto-properties. Microsoft Learn warns that logic in a parameter&#8217;s <code>set</code> can cause an infinite render loop.</p>



<p class="wp-block-paragraph"><strong>Checkpoint:</strong> curl of <code>/shop</code> returned 200 with six cards, starting with &#8220;Packs · $64.99&#8221;.</p>



<h2 class="wp-block-heading">Step 3: Make a Blazor Component Parameter Required with [EditorRequired]</h2>



<p class="wp-block-paragraph"><code>[EditorRequired]</code> tells the editor and the build that a parent must set the parameter. To see it, I added a card with no product under the loop:</p>



<pre class="wp-block-code"><code>&lt;ProductCard /&gt;</code></pre>



<p class="wp-block-paragraph"><code>dotnet build --no-incremental</code> printed this warning:</p>



<pre class="wp-block-code"><code>Components\Pages\Shop.razor(14,9): warning RZ2012: Component &#x27;ProductCard&#x27; expects a value for the parameter &#x27;Product&#x27;, but a value may not have been provided.</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/vs-code-blazor-editorrequired-rz2012-warning-dotnet-build.png" alt="VS Code with Shop.razor open showing a ProductCard tag without the Product parameter and the terminal showing warning RZ2012 from dotnet build" class="wp-image-2816" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/vs-code-blazor-editorrequired-rz2012-warning-dotnet-build.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/vs-code-blazor-editorrequired-rz2012-warning-dotnet-build-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/vs-code-blazor-editorrequired-rz2012-warning-dotnet-build-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/vs-code-blazor-editorrequired-rz2012-warning-dotnet-build-768x674.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">The RZ2012 warning for a missing [EditorRequired] parameter</figcaption></figure>
</div>


<p class="wp-block-paragraph">It&#8217;s only a warning, so the build still succeeds. At runtime, curl of <code>/shop</code> returned 500:</p>



<pre class="wp-block-code"><code>NullReferenceException: Object reference not set to an instance of an object.
at BlazorComponents.Components.ProductCard.BuildRenderTree(RenderTreeBuilder __builder) in C:\DotNetDemo\BlazorComponents\Components\ProductCard.razor:line 6</code></pre>



<p class="wp-block-paragraph">The attribute isn&#8217;t checked at runtime and doesn&#8217;t promise a non-null value, so <a href="https://aspdotnethelp.com/how-to-check-for-null-values-in-c-sharp/">null checks</a> still matter for optional parameters. I removed the broken tag before moving on.</p>



<p class="wp-block-paragraph"><strong>Checkpoint:</strong> one RZ2012 warning in the build output, and none once the tag is gone.</p>



<h2 class="wp-block-heading">Step 4: Send Clicks from Child to Parent with EventCallback<T></h2>



<p class="wp-block-paragraph">The card has the Add button, but the cart lives in the page. A child can&#8217;t reach into its parent, so it exposes an <code>EventCallback&lt;CartLine&gt;</code> parameter and invokes it:</p>



<pre class="wp-block-code"><code>
@using System.Globalization
@using BlazorComponents.Models

&lt;div class=&quot;card mb-2&quot;&gt;
    &lt;div class=&quot;card-body py-2&quot;&gt;
        &lt;h5 class=&quot;card-title mb-1&quot;&gt;@Product.Name&lt;/h5&gt;
        &lt;p class=&quot;mb-2&quot;&gt;@Product.Category · @Product.Price.ToString(&quot;C&quot;, Us)&lt;/p&gt;
        &lt;button class=&quot;btn btn-outline-secondary btn-sm minus&quot; @onclick=&quot;() =&gt; quantity = Math.Max(1, quantity - 1)&quot;&gt;-&lt;/button&gt;
        &lt;span class=&quot;qty mx-2&quot;&gt;@quantity&lt;/span&gt;
        &lt;button class=&quot;btn btn-outline-secondary btn-sm plus&quot; @onclick=&quot;() =&gt; quantity++&quot;&gt;+&lt;/button&gt;
        &lt;button class=&quot;btn btn-primary btn-sm ms-2 add&quot; @onclick=&quot;Add&quot;&gt;Add to cart&lt;/button&gt;
    &lt;/div&gt;
&lt;/div&gt;

@code {
    private static readonly CultureInfo Us = new(&quot;en-US&quot;);
    private int quantity = 1;

    [Parameter, EditorRequired]
    public Product Product { get; set; } = default!;

    [Parameter]
    public EventCallback&lt;CartLine&gt; OnAdd { get; set; }

    private async Task Add() =&gt; await OnAdd.InvokeAsync(new CartLine(Product, quantity));
}
</code></pre>



<p class="wp-block-paragraph">The <code>quantity</code> field is the card&#8217;s own private state. Learn recommends the typed <code>EventCallback&lt;TValue&gt;</code> over plain <code>EventCallback</code>, because a wrong argument type then fails at compile time.</p>



<p class="wp-block-paragraph">In the page, hand the callback a method and give the page a render mode. Here&#8217;s the updated <code>Shop.razor</code>:</p>



<pre class="wp-block-code"><code>@page &quot;/shop&quot;
@rendermode InteractiveServer
@using BlazorComponents.Models

&lt;PageTitle&gt;Shop&lt;/PageTitle&gt;

&lt;h1&gt;Cascade Trail Outfitters&lt;/h1&gt;

&lt;div class=&quot;row&quot;&gt;
    &lt;div class=&quot;col-md-7&quot;&gt;
        @foreach (var product in products)
        {
            &lt;ProductCard Product=&quot;product&quot; OnAdd=&quot;AddToCart&quot; /&gt;
        }
    &lt;/div&gt;
    &lt;div class=&quot;col-md-5&quot;&gt;
        &lt;p class=&quot;cart&quot;&gt;Cart: @cart.Count line(s), @cart.Sum(l =&gt; l.Quantity) item(s)&lt;/p&gt;
    &lt;/div&gt;
&lt;/div&gt;

@code {
    private List&lt;Product&gt; products =
    [
        new(1, &quot;Trail Daypack 24L&quot;, &quot;Packs&quot;, 64.99m),
        new(2, &quot;Insulated Water Bottle&quot;, &quot;Hydration&quot;, 24.50m),
        new(3, &quot;Two-Person Tent&quot;, &quot;Camping&quot;, 189.00m),
        new(4, &quot;Headlamp 400&quot;, &quot;Lighting&quot;, 34.95m),
        new(5, &quot;Rain Shell Jacket&quot;, &quot;Apparel&quot;, 119.00m),
        new(6, &quot;Camp Stove&quot;, &quot;Cooking&quot;, 79.99m),
    ];
    private readonly List&lt;CartLine&gt; cart = [];

    private void AddToCart(CartLine line) =&gt; cart.Add(line);
}
</code></pre>



<p class="wp-block-paragraph">You don&#8217;t call <code>StateHasChanged</code> in <code>AddToCart</code>. Blazor rerenders the parent after an <code>EventCallback</code> runs, which is how it differs from a raw <a href="https://aspdotnethelp.com/delegates-in-csharp/">C# delegate</a> like <code>Action&lt;T&gt;</code>.</p>



<p class="wp-block-paragraph">I tried it first without the <code>@rendermode InteractiveServer</code> line. After three clicks, the page still said &#8220;Cart: 0 line(s), 0 item(s)&#8221;, because a page without a render mode is static HTML.</p>



<p class="wp-block-paragraph">With the line back, the same clicks gave &#8220;Cart: 2 line(s), 3 item(s)&#8221;. My <a href="https://aspdotnethelp.com/blazor-render-modes/">Blazor render modes guide</a> explains what each mode does.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1002" height="830" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-eventcallback-child-to-parent-add-to-cart.png" alt="Blazor EventCallback demo: ProductCard Add to cart buttons updating the parent Shop page, which shows Cart: 2 line(s), 3 item(s)" class="wp-image-2812" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-eventcallback-child-to-parent-add-to-cart.png 1002w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-eventcallback-child-to-parent-add-to-cart-300x249.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-eventcallback-child-to-parent-add-to-cart-768x636.png 768w" sizes="auto, (max-width: 1002px) 100vw, 1002px" /><figcaption class="wp-element-caption">Each Add to cart click reaches the parent through EventCallback<CartLine></figcaption></figure>
</div>


<p class="wp-block-paragraph"><strong>Checkpoint:</strong> Add on the Daypack, then + and Add on the Tent: &#8220;Cart: 2 line(s), 3 item(s)&#8221;.</p>



<h2 class="wp-block-heading">Step 5: Pass Markup to a Blazor Component with ChildContent</h2>



<p class="wp-block-paragraph">A parameter can also carry markup. Name a <code>RenderFragment</code> parameter <code>ChildContent</code>, and whatever sits between the component&#8217;s tags lands there. Here&#8217;s <code>Components/Panel.razor</code>:</p>



<pre class="wp-block-code"><code>
&lt;div class=&quot;card&quot;&gt;
    &lt;div class=&quot;card-header fw-bold&quot;&gt;@Title&lt;/div&gt;
    &lt;div class=&quot;card-body&quot;&gt;
        @ChildContent
    &lt;/div&gt;
&lt;/div&gt;

@code {
    [Parameter]
    public string Title { get; set; } = &quot;&quot;;

    [Parameter]
    public RenderFragment? ChildContent { get; set; }
}
</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/vs-code-blazor-panel-razor-childcontent-renderfragment.png" alt="VS Code with Panel.razor open showing the Title parameter and the RenderFragment ChildContent parameter, and a successful dotnet build in the terminal" class="wp-image-2818" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/vs-code-blazor-panel-razor-childcontent-renderfragment.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/vs-code-blazor-panel-razor-childcontent-renderfragment-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/vs-code-blazor-panel-razor-childcontent-renderfragment-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/vs-code-blazor-panel-razor-childcontent-renderfragment-768x674.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">Panel.razor: a Title parameter plus ChildContent</figcaption></figure>
</div>


<p class="wp-block-paragraph">The name matters: Blazor only maps the content between the tags to a property called ChildContent. In the page, the cart text moves inside the panel:</p>



<pre class="wp-block-code"><code>        &lt;Panel Title=&quot;Your order&quot;&gt;
            &lt;p class=&quot;cart&quot;&gt;Cart: @cart.Count line(s), @cart.Sum(l =&gt; l.Quantity) item(s)&lt;/p&gt;
            &lt;p class=&quot;text-muted mb-0&quot;&gt;Free shipping on orders over $75.&lt;/p&gt;
        &lt;/Panel&gt;</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1002" height="830" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-renderfragment-childcontent-panel-component.png" alt="Blazor ChildContent RenderFragment demo: a Panel component titled Your order wrapping the cart count and a free shipping note" class="wp-image-2814" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-renderfragment-childcontent-panel-component.png 1002w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-renderfragment-childcontent-panel-component-300x249.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-renderfragment-childcontent-panel-component-768x636.png 768w" sizes="auto, (max-width: 1002px) 100vw, 1002px" /><figcaption class="wp-element-caption">Panel renders its Title parameter and the ChildContent markup</figcaption></figure>
</div>


<p class="wp-block-paragraph"><strong>Checkpoint:</strong> the right column shows &#8220;Your order&#8221; with &#8220;Cart: 2 line(s), 3 item(s)&#8221; inside it.</p>



<h2 class="wp-block-heading">Step 6: Move Component Logic to a Code-Behind Partial Class</h2>



<p class="wp-block-paragraph">For the cart list, I split markup and C# into two files. <code>Components/OrderList.razor</code> holds only markup:</p>



<pre class="wp-block-code"><code>
@if (Lines.Count == 0)
{
    &lt;p class=&quot;text-muted mb-0&quot;&gt;Your cart is empty.&lt;/p&gt;
}
else
{
    &lt;ul class=&quot;list-group mb-2&quot;&gt;
        @foreach (var line in Lines)
        {
            &lt;li class=&quot;list-group-item d-flex justify-content-between align-items-center&quot;&gt;
                &lt;span&gt;@line.Quantity x @line.Product.Name&lt;/span&gt;
                &lt;span&gt;
                    @Money(line.Total)
                    &lt;button class=&quot;btn btn-link btn-sm remove&quot; @onclick=&quot;() =&gt; OnRemove.InvokeAsync(line)&quot;&gt;Remove&lt;/button&gt;
                &lt;/span&gt;
            &lt;/li&gt;
        }
    &lt;/ul&gt;
    &lt;p class=&quot;fw-bold mb-0 subtotal&quot;&gt;Subtotal: @Money(Subtotal) (@ItemCount items)&lt;/p&gt;
}
</code></pre>



<p class="wp-block-paragraph">And <code>Components/OrderList.razor.cs</code> holds the class. It&#8217;s a <a href="https://aspdotnethelp.com/partial-class-in-csharp/">partial class</a> with the same name and namespace as the <code>.razor</code> file:</p>



<pre class="wp-block-code"><code>using System.Globalization;
using BlazorComponents.Models;
using Microsoft.AspNetCore.Components;

namespace BlazorComponents.Components;

public partial class OrderList : ComponentBase
{
    private static readonly CultureInfo Us = new(&quot;en-US&quot;);

    [Parameter, EditorRequired]
    public IReadOnlyList&lt;CartLine&gt; Lines { get; set; } = [];

    [Parameter]
    public EventCallback&lt;CartLine&gt; OnRemove { get; set; }

    private decimal Subtotal =&gt; Lines.Sum(l =&gt; l.Total);
    private int ItemCount =&gt; Lines.Sum(l =&gt; l.Quantity);

    private static string Money(decimal value) =&gt; value.ToString(&quot;C&quot;, Us);
}</code></pre>



<p class="wp-block-paragraph">Two details trip people up. <code>_Imports.razor</code> doesn&#8217;t apply to <code>.cs</code> files, so the <code>using</code> lines are needed, and <code>: ComponentBase</code> is optional because the <code>.razor</code> half already sets it.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/vs-code-blazor-orderlist-razor-cs-partial-class-componentbase.png" alt="VS Code with OrderList.razor.cs open showing public partial class OrderList : ComponentBase with Lines and OnRemove parameters and a successful dotnet build" class="wp-image-2817" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/vs-code-blazor-orderlist-razor-cs-partial-class-componentbase.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/vs-code-blazor-orderlist-razor-cs-partial-class-componentbase-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/vs-code-blazor-orderlist-razor-cs-partial-class-componentbase-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/vs-code-blazor-orderlist-razor-cs-partial-class-componentbase-768x674.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">OrderList.razor.cs, the code-behind half of the component</figcaption></figure>
</div>


<p class="wp-block-paragraph">The subtotal is a <a href="https://aspdotnethelp.com/linq-in-csharp/">LINQ Sum</a> over the lines. In the page, the panel now wraps the new component, and <code>RemoveLine</code> handles its callback:</p>



<pre class="wp-block-code"><code>        &lt;Panel Title=&quot;Your order&quot;&gt;
            &lt;OrderList Lines=&quot;cart&quot; OnRemove=&quot;RemoveLine&quot; /&gt;
        &lt;/Panel&gt;

@code {
    ...
    private void RemoveLine(CartLine line) =&gt; cart.Remove(line);
}</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1002" height="830" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-code-behind-orderlist-subtotal.png" alt="Blazor OrderList component built with a code-behind partial class showing three cart lines and Subtotal: $477.94 (4 items)" class="wp-image-2808" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-code-behind-orderlist-subtotal.png 1002w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-code-behind-orderlist-subtotal-300x249.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-code-behind-orderlist-subtotal-768x636.png 768w" sizes="auto, (max-width: 1002px) 100vw, 1002px" /><figcaption class="wp-element-caption">The OrderList component inside the Panel</figcaption></figure>
</div>


<p class="wp-block-paragraph"><strong>Checkpoint:</strong> after adding the Headlamp too: &#8220;Subtotal: $477.94 (4 items)&#8221;. Remove on the Tent line left &#8220;Subtotal: $99.94 (2 items)&#8221;.</p>



<h2 class="wp-block-heading">Step 7: Share Values with a Blazor Cascading Parameter</h2>



<p class="wp-block-paragraph">Every card needs the store&#8217;s free shipping threshold, and passing it to each card by hand gets old. A <code>CascadingValue</code> hands a value to every component below it. The loop in <code>Shop.razor</code> becomes:</p>



<pre class="wp-block-code"><code>        &lt;CascadingValue Value=&quot;store&quot; IsFixed=&quot;true&quot;&gt;
            @foreach (var product in products)
            {
                &lt;ProductCard Product=&quot;product&quot; OnAdd=&quot;AddToCart&quot; /&gt;
            }
        &lt;/CascadingValue&gt;</code></pre>



<p class="wp-block-paragraph">The page also gets a <code>store</code> field: <code>new(&quot;Cascade Trail Outfitters&quot;, &quot;Portland, OR&quot;, 75m)</code>. <code>IsFixed=&quot;true&quot;</code> says it won&#8217;t change, so Blazor skips change tracking.</p>



<p class="wp-block-paragraph">The card picks it up with <code>[CascadingParameter]</code>, matched by type. Here&#8217;s the finished <code>ProductCard.razor</code>:</p>



<pre class="wp-block-code"><code>
@using System.Globalization
@using BlazorComponents.Models

&lt;div class=&quot;card mb-2&quot;&gt;
    &lt;div class=&quot;card-body py-2&quot;&gt;
        &lt;h5 class=&quot;card-title mb-1&quot;&gt;@Product.Name&lt;/h5&gt;
        &lt;p class=&quot;mb-2&quot;&gt;@Product.Category · @Product.Price.ToString(&quot;C&quot;, Us)&lt;/p&gt;
        @if (Store is not null &amp;&amp; Product.Price &gt;= Store.FreeShippingOver)
        {
            &lt;p class=&quot;mb-2&quot;&gt;&lt;span class=&quot;badge text-bg-success&quot;&gt;Free shipping from @Store.City&lt;/span&gt;&lt;/p&gt;
        }
        &lt;button class=&quot;btn btn-outline-secondary btn-sm minus&quot; @onclick=&quot;() =&gt; quantity = Math.Max(1, quantity - 1)&quot;&gt;-&lt;/button&gt;
        &lt;span class=&quot;qty mx-2&quot;&gt;@quantity&lt;/span&gt;
        &lt;button class=&quot;btn btn-outline-secondary btn-sm plus&quot; @onclick=&quot;() =&gt; quantity++&quot;&gt;+&lt;/button&gt;
        &lt;button class=&quot;btn btn-primary btn-sm ms-2 add&quot; @onclick=&quot;Add&quot;&gt;Add to cart&lt;/button&gt;
    &lt;/div&gt;
&lt;/div&gt;

@code {
    private static readonly CultureInfo Us = new(&quot;en-US&quot;);
    private int quantity = 1;

    [Parameter, EditorRequired]
    public Product Product { get; set; } = default!;

    [Parameter]
    public EventCallback&lt;CartLine&gt; OnAdd { get; set; }

    [CascadingParameter]
    private StoreInfo? Store { get; set; }

    private async Task Add() =&gt; await OnAdd.InvokeAsync(new CartLine(Product, quantity));
}
</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1002" height="926" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-cascading-parameter-free-shipping-badge.png" alt="Blazor cascading parameter demo: free shipping from Portland, OR badges on Two-Person Tent, Rain Shell Jacket, Camp Stove" class="wp-image-2807" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-cascading-parameter-free-shipping-badge.png 1002w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-cascading-parameter-free-shipping-badge-300x277.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-cascading-parameter-free-shipping-badge-768x710.png 768w" sizes="auto, (max-width: 1002px) 100vw, 1002px" /><figcaption class="wp-element-caption">The cascaded StoreInfo drives the free shipping badge</figcaption></figure>
</div>


<p class="wp-block-paragraph">For an app-wide value, register it as a service with <code>builder.Services.AddCascadingValue(...)</code>, the same container as in <a href="https://aspdotnethelp.com/dependency-injection-lifetimes-aspnet-core/">DI lifetimes in ASP.NET Core</a>.</p>



<p class="wp-block-paragraph"><strong>Important:</strong> cascading values don&#8217;t cross render mode boundaries. A <code>CascadingValue</code> in a static layout won&#8217;t reach an interactive page, while a root-level <code>AddCascadingValue</code> will.</p>



<p class="wp-block-paragraph"><strong>Checkpoint:</strong> badges on 3 cards, the ones at $75 or more: Two-Person Tent, Rain Shell Jacket, Camp Stove.</p>



<h2 class="wp-block-heading">Step 8: Keep Component State in Lists with @key</h2>



<p class="wp-block-paragraph">Last, a Sort by price button. Here&#8217;s the final <code>Shop.razor</code>; note <code>@key=&quot;product.Id&quot;</code> on the card:</p>



<pre class="wp-block-code"><code>@page &quot;/shop&quot;
@rendermode InteractiveServer
@using BlazorComponents.Models

&lt;PageTitle&gt;Shop&lt;/PageTitle&gt;

&lt;h1&gt;Cascade Trail Outfitters&lt;/h1&gt;

&lt;button class=&quot;btn btn-secondary btn-sm mb-3 sort&quot; @onclick=&quot;SortByPrice&quot;&gt;Sort by price&lt;/button&gt;

&lt;div class=&quot;row&quot;&gt;
    &lt;div class=&quot;col-md-7&quot;&gt;
        &lt;CascadingValue Value=&quot;store&quot; IsFixed=&quot;true&quot;&gt;
            @foreach (var product in products)
            {
                &lt;ProductCard @key=&quot;product.Id&quot; Product=&quot;product&quot; OnAdd=&quot;AddToCart&quot; /&gt;
            }
        &lt;/CascadingValue&gt;
    &lt;/div&gt;
    &lt;div class=&quot;col-md-5&quot;&gt;
        &lt;Panel Title=&quot;Your order&quot;&gt;
            &lt;OrderList Lines=&quot;cart&quot; OnRemove=&quot;RemoveLine&quot; /&gt;
        &lt;/Panel&gt;
    &lt;/div&gt;
&lt;/div&gt;

@code {
    private readonly StoreInfo store = new(&quot;Cascade Trail Outfitters&quot;, &quot;Portland, OR&quot;, 75m);
    private List&lt;Product&gt; products =
    [
        new(1, &quot;Trail Daypack 24L&quot;, &quot;Packs&quot;, 64.99m),
        new(2, &quot;Insulated Water Bottle&quot;, &quot;Hydration&quot;, 24.50m),
        new(3, &quot;Two-Person Tent&quot;, &quot;Camping&quot;, 189.00m),
        new(4, &quot;Headlamp 400&quot;, &quot;Lighting&quot;, 34.95m),
        new(5, &quot;Rain Shell Jacket&quot;, &quot;Apparel&quot;, 119.00m),
        new(6, &quot;Camp Stove&quot;, &quot;Cooking&quot;, 79.99m),
    ];
    private readonly List&lt;CartLine&gt; cart = [];

    private void AddToCart(CartLine line) =&gt; cart.Add(line);
    private void RemoveLine(CartLine line) =&gt; cart.Remove(line);
    private void SortByPrice() =&gt; products = [.. products.OrderBy(p =&gt; p.Price)];
}
</code></pre>



<p class="wp-block-paragraph">I ran the test without <code>@key</code> first: + twice on the Daypack, so it showed 3, then Sort. Row 1 became &#8220;Insulated Water Bottle qty 3&#8221;, and the Daypack in row 3 dropped back to 1.</p>



<p class="wp-block-paragraph">Without a key, Blazor reuses the card in row 1 and only swaps its <code>Product</code> parameter, so the private <code>quantity</code> stays behind. With <code>@key</code>, the same clicks left &#8220;Trail Daypack 24L qty 3&#8221; in row 3.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="932" height="514" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-key-without-vs-with-key-sort-before-after.png" alt="Blazor @key before and after: without @key the quantity 3 stays on the first row (Insulated Water Bottle), with @key the quantity 3 moves with the Trail Daypack 24L to row three" class="wp-image-2813" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-key-without-vs-with-key-sort-before-after.png 932w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-key-without-vs-with-key-sort-before-after-300x165.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-key-without-vs-with-key-sort-before-after-768x424.png 768w" sizes="auto, (max-width: 932px) 100vw, 932px" /><figcaption class="wp-element-caption">Same clicks, same sort: without @key vs with @key</figcaption></figure>
</div>


<p class="wp-block-paragraph"><code>SortByPrice</code> builds a new <a href="https://aspdotnethelp.com/list-in-csharp/">List<T></a> rather than sorting in place, which is fine. Use a unique, stable key such as a database id or the object itself.</p>



<p class="wp-block-paragraph">Duplicate keys under one parent throw an exception, and keys only compare among siblings.</p>



<p class="wp-block-paragraph"><strong>Checkpoint:</strong> after sorting with <code>@key</code>, the Daypack sits third and still shows 3.</p>



<h2 class="wp-block-heading">Test the Blazor Components</h2>



<p class="wp-block-paragraph">Here&#8217;s every check I ran on the finished app, with the real results:</p>



<figure class="wp-block-table"><table><thead><tr><th>Check</th><th>How</th><th>Result</th></tr></thead><tbody><tr><td><code>/shop</code> loads</td><td>curl</td><td>200</td></tr><tr><td>Missing <code>[EditorRequired]</code> value</td><td>dotnet build</td><td>warning RZ2012</td></tr><tr><td>Add to cart, no render mode</td><td>3 clicks, headless Edge</td><td>Cart: 0 line(s), 0 item(s)</td></tr><tr><td>Add to cart, <code>InteractiveServer</code></td><td>3 clicks, headless Edge</td><td>Cart: 2 line(s), 3 item(s)</td></tr><tr><td>Final cart</td><td>4 clicks, headless Edge</td><td>Subtotal: $477.94 (4 items)</td></tr><tr><td>Sort without <code>@key</code></td><td>qty 3, then Sort</td><td>Insulated Water Bottle qty 3</td></tr><tr><td>Sort with <code>@key</code></td><td>qty 3, then Sort</td><td>Trail Daypack 24L qty 3</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">The page is still prerendered: curl got full HTML plus a <code>&lt;!--Blazor:</code> marker. That&#8217;s why my headless runs wait 3.5 seconds before clicking, so the circuit is connected.</p>



<h2 class="wp-block-heading">Troubleshooting Common Blazor Component Parameter Mistakes</h2>



<p class="wp-block-paragraph"><strong>Writing to a parameter inside the child.</strong> This <code>QtyBadge.razor</code> increments its own <code>Quantity</code> parameter:</p>



<pre class="wp-block-code"><code>
@using BlazorComponents.Models

&lt;p class=&quot;mb-2&quot;&gt;
    @Product.Name: &lt;span class=&quot;qty&quot;&gt;@Quantity&lt;/span&gt;
    &lt;button class=&quot;btn btn-outline-secondary btn-sm plus&quot; @onclick=&quot;() =&gt; Quantity++&quot;&gt;+&lt;/button&gt;
&lt;/p&gt;

@code {
    [Parameter, EditorRequired]
    public Product Product { get; set; } = default!;

    [Parameter]
    public int Quantity { get; set; }   // the child writes to its own parameter: don&#x27;t
}
</code></pre>



<p class="wp-block-paragraph">I used it on a test page, <code>Components/Pages/Mistake.razor</code>, with a button that only rerenders the parent:</p>



<pre class="wp-block-code"><code>
@page &quot;/mistake&quot;
@rendermode InteractiveServer
@using BlazorComponents.Models

&lt;PageTitle&gt;Mistake&lt;/PageTitle&gt;

&lt;h1&gt;Writing to a parameter&lt;/h1&gt;

&lt;QtyBadge Product=&quot;headlamp&quot; Quantity=&quot;1&quot; /&gt;

&lt;p&gt;Parent renders: &lt;span class=&quot;renders&quot;&gt;@renders&lt;/span&gt;&lt;/p&gt;
&lt;button class=&quot;btn btn-secondary btn-sm refresh&quot; @onclick=&quot;() =&gt; renders++&quot;&gt;Re-render parent&lt;/button&gt;

@code {
    private readonly Product headlamp = new(4, &quot;Headlamp 400&quot;, &quot;Lighting&quot;, 34.95m);
    private int renders = 1;
}
</code></pre>



<p class="wp-block-paragraph">Two clicks on + showed <code>3</code>. One click on Re-render parent reset it to <code>1</code>, because the parent sent <code>Quantity=&quot;1&quot;</code> again. Keep the value in a private field, as <code>ProductCard</code> does, and report changes with an <code>EventCallback</code>, Blazor&#8217;s take on <a href="https://aspdotnethelp.com/events-in-csharp/">C# events</a>.</p>



<p class="wp-block-paragraph"><strong>Buttons that do nothing.</strong> The page has no <code>@rendermode</code>, so it&#8217;s static SSR. Add <code>@rendermode InteractiveServer</code> to the page, as in Step 4.</p>



<p class="wp-block-paragraph"><strong>A component tag that renders as plain HTML.</strong> The file name starts with a lowercase letter, or its namespace isn&#8217;t imported. Rename it to <code>ProductCard.razor</code> and check <code>_Imports.razor</code>.</p>



<p class="wp-block-paragraph"><strong><code>required</code> or <code>init</code> on a parameter.</strong> Learn says not to use either. Blazor sets parameters by reflection, which bypasses both, so use <code>[EditorRequired]</code> instead.</p>



<h2 class="wp-block-heading">Where to Take It Next with Blazor Components</h2>



<p class="wp-block-paragraph">Swap the hard-coded list for real data and a grid. My <a href="https://aspdotnethelp.com/blazor-quickgrid/">Blazor QuickGrid tutorial</a> sorts and pages 200 orders with the same kind of component parameters.</p>



<p class="wp-block-paragraph">You can also make <code>ProductCard</code> generic with <code>@typeparam</code>, or add two-way binding with a <code>Value</code> parameter plus a <code>ValueChanged</code> callback. New posts land in <a href="https://aspdotnethelp.com/blazor-tutorials/">the Blazor tutorials</a>.</p>



<p class="wp-block-paragraph">A few more reads for the C# behind these components:</p>



<ul class="wp-block-list">
<li><a href="https://aspdotnethelp.com/asp-net-core-tutorials/">More ASP.NET Core tutorials for the server side</a></li>

<li><a href="https://aspdotnethelp.com/interface-in-csharp/">Interfaces in C# for swappable services</a></li>

<li><a href="https://aspdotnethelp.com/whats-new-in-csharp-14/">New C# 14 features you can use in @code blocks</a></li>

<li><a href="https://aspdotnethelp.com/upgrade-dotnet-8-to-dotnet-10/">Upgrade a .NET 8 Blazor app to .NET 10</a></li>
</ul>



<h2 class="wp-block-heading">Frequently Asked Questions</h2>


<div id="rank-math-faq" class="rank-math-block">
<div class="rank-math-list ">
<div id="faq-question-bc0000001" class="rank-math-list-item">
<h3 class="rank-math-question ">What are Blazor components?</h3>
<div class="rank-math-answer ">

<p>They&#8217;re reusable pieces of UI written as .razor files that mix HTML and C#. Each file compiles to a C# class that inherits ComponentBase, and a component becomes a page when you add an @page directive.</p>

</div>
</div>
<div id="faq-question-bc0000002" class="rank-math-list-item">
<h3 class="rank-math-question ">How do I pass parameters to a Blazor component?</h3>
<div class="rank-math-answer ">

<p>Declare a public auto-property with the [Parameter] attribute in the child, then set it as an attribute where you use the component, for example . Add [EditorRequired] if the parent must always set it.</p>

</div>
</div>
<div id="faq-question-bc0000003" class="rank-math-list-item">
<h3 class="rank-math-question ">How does a child component pass data to its parent in Blazor?</h3>
<div class="rank-math-answer ">

<p>Give the child an EventCallback parameter and call await OnAdd.InvokeAsync(value) from its event handler. The parent assigns a method to it, and Blazor rerenders the parent after the method runs.</p>

</div>
</div>
<div id="faq-question-bc0000004" class="rank-math-list-item">
<h3 class="rank-math-question ">What is the difference between a component parameter and a cascading parameter?</h3>
<div class="rank-math-answer ">

<p>A component parameter is set by the direct parent as an attribute. A cascading parameter is supplied by a CascadingValue component, or AddCascadingValue in Program.cs, and reaches every component below it, matched by type or name.</p>

</div>
</div>
<div id="faq-question-bc0000005" class="rank-math-list-item">
<h3 class="rank-math-question ">What is ComponentBase in Blazor?</h3>
<div class="rank-math-answer ">

<p>ComponentBase is the base class every .razor component inherits. It provides the lifecycle methods such as OnInitialized and OnParametersSet, StateHasChanged and the rendering logic. A code-behind partial class can inherit it explicitly.</p>

</div>
</div>
<div id="faq-question-bc0000006" class="rank-math-list-item">
<h3 class="rank-math-question ">Why doesn&#8217;t my Blazor button do anything?</h3>
<div class="rank-math-answer ">

<p>The page renders with static SSR, which is the default in a Blazor Web App. Add @rendermode InteractiveServer to the page or turn on global interactivity. See <a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/components/?view=aspnetcore-10.0" target="_blank" rel="noopener">ASP.NET Core Razor components</a> on Microsoft Learn.</p>

</div>
</div>
</div>
</div>]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Middleware in ASP.NET Core: Write Custom Middleware and Get the Order Right</title>
		<link>https://aspdotnethelp.com/aspnet-core-middleware/</link>
		
		<dc:creator><![CDATA[Bijay Kumar]]></dc:creator>
		<pubDate>Tue, 10 Nov 2026 15:15:00 +0000</pubDate>
				<category><![CDATA[ASP.NET Core]]></category>
		<guid isPermaLink="false">https://aspdotnethelp.com/?p=2790</guid>

					<description><![CDATA[ASP.NET Core middleware is the chain of components every request passes through. Each one gets the HttpContext, does its job and calls next() to hand the request on, or skips next() to answer early. The order you register them in Program.cs is the order they run, and the response travels back in reverse. I built ... <a title="Middleware in ASP.NET Core: Write Custom Middleware and Get the Order Right" class="read-more" href="https://aspdotnethelp.com/aspnet-core-middleware/" aria-label="Read more about Middleware in ASP.NET Core: Write Custom Middleware and Get the Order Right">Read more &#62;&#62;</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"><strong>ASP.NET Core middleware</strong> is the chain of components every request passes through. Each one gets the <code>HttpContext</code>, does its job and calls <code>next()</code> to hand the request on, or skips <code>next()</code> to answer early.</p>



<p class="wp-block-paragraph">The order you register them in Program.cs is the order they run, and the response travels back in reverse. I built a .NET 10 orders API with a request timer, a correlation ID and an API-key check, then broke its order on purpose.</p>



<h2 class="wp-block-heading">What Is the ASP.NET Core Middleware Pipeline?</h2>



<p class="wp-block-paragraph">The pipeline is a list of request delegates called one after another. A <code>RequestDelegate</code> is just a <a href="https://aspdotnethelp.com/delegates-in-csharp/">delegate in C#</a> that takes an <code>HttpContext</code> and returns a <code>Task</code>.</p>



<p class="wp-block-paragraph">Code before <code>await next(context)</code> works on the request on its way in. Code after it runs on the way out, once everything later in the pipeline has finished. That&#8217;s why exception handlers go first: they wrap everything below them.</p>



<h2 class="wp-block-heading">The Sample Orders API We&#8217;ll Use for Middleware</h2>



<p class="wp-block-paragraph">Everything runs on the .NET 10 SDK (10.0.402); <a href="https://aspdotnethelp.com/check-dotnet-version/">check your .NET version</a> if you&#8217;re not sure what you have. The main project is an empty web template called OrdersApi:</p>



<pre class="wp-block-code"><code>dotnet new web -n OrdersApi -o OrdersApi</code></pre>



<p class="wp-block-paragraph">It serves three orders from Austin, Columbus and Seattle. Four endpoints exercise the pipeline in different ways:</p>



<figure class="wp-block-table"><table><thead><tr><th>Endpoint</th><th>Rule</th><th>Why it&#8217;s in the demo</th></tr></thead><tbody><tr><td>GET /orders</td><td>Public</td><td>The happy path, returns the three orders</td></tr><tr><td>GET /orders/fail</td><td>Public</td><td>Throws, so the exception handler has work</td></tr><tr><td>GET /admin/report</td><td>Needs an <code>X-Api-Key</code> header</td><td>Proves authentication and authorization order</td></tr><tr><td>GET /wp-login.php</td><td>Blocked</td><td>Short-circuited by inline middleware</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">I deleted appsettings.Development.json and the Properties folder, and I start it with <code>dotnet run -- --urls http://localhost:5790 --environment Development</code>. Here are the project file and appsettings.json, both straight from the template:</p>



<pre class="wp-block-code"><code>&lt;Project Sdk=&quot;Microsoft.NET.Sdk.Web&quot;&gt;

  &lt;PropertyGroup&gt;
    &lt;TargetFramework&gt;net10.0&lt;/TargetFramework&gt;
    &lt;Nullable&gt;enable&lt;/Nullable&gt;
    &lt;ImplicitUsings&gt;enable&lt;/ImplicitUsings&gt;
  &lt;/PropertyGroup&gt;

&lt;/Project&gt;</code></pre>



<pre class="wp-block-code"><code>{
  &quot;Logging&quot;: {
    &quot;LogLevel&quot;: {
      &quot;Default&quot;: &quot;Information&quot;,
      &quot;Microsoft.AspNetCore&quot;: &quot;Warning&quot;
    }
  },
  &quot;AllowedHosts&quot;: &quot;*&quot;
}</code></pre>



<h2 class="wp-block-heading">Method 1: Inline Middleware with app.Use and app.Run (Recommended to Start)</h2>



<p class="wp-block-paragraph">Inline middleware is the fastest way to see how the pipeline behaves, and it&#8217;s fine in real apps for one-off jobs like the bot blocker you&#8217;ll see later.</p>



<p class="wp-block-paragraph">This demo, like the next two, is one of the <a href="https://aspdotnethelp.com/dotnet-run-file-based-apps/">file-based apps</a> that <a href="https://aspdotnethelp.com/whats-new-in-dotnet-10/">.NET 10</a> added. The <code>#:sdk Microsoft.NET.Sdk.Web</code> line makes it a web app that starts, calls itself and stops.</p>



<p class="wp-block-paragraph">Same order #1042 from the sample data. Here&#8217;s the code, with two Use calls and one Run:</p>



<pre class="wp-block-code"><code>#:sdk Microsoft.NET.Sdk.Web
var builder = WebApplication.CreateBuilder();
builder.Logging.ClearProviders();
var app = builder.Build();

app.Use(async (context, next) =&gt;
{
    Console.WriteLine(&quot;1. Logger: request in&quot;);
    await next(context);
    Console.WriteLine(&quot;1. Logger: response out&quot;);
});
app.Use(async (context, next) =&gt;
{
    Console.WriteLine(&quot;2. Timer: request in&quot;);
    await next(context);
    Console.WriteLine(&quot;2. Timer: response out&quot;);
});
app.Run(async context =&gt;
{
    Console.WriteLine(&quot;3. Run: writes the response&quot;);
    await context.Response.WriteAsync(&quot;Order #1042 shipped to Austin, TX&quot;);
});

app.Urls.Add(&quot;http://localhost:5790&quot;);
await app.StartAsync();
Console.WriteLine(await new HttpClient().GetStringAsync(&quot;http://localhost:5790/&quot;));
await app.StopAsync();</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>1. Logger: request in
2. Timer: request in
3. Run: writes the response
2. Timer: response out
1. Logger: response out
Order #1042 shipped to Austin, TX</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/asp-net-core-middleware-pipeline-order-app-use-run.png" alt="ASP.NET Core middleware order in VS Code: PipelineOrder.cs with two app.Use delegates and app.Run, output shows 1 and 2 request in, 3 Run writes the response, then 2 and 1 response out" class="wp-image-2804" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/asp-net-core-middleware-pipeline-order-app-use-run.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/asp-net-core-middleware-pipeline-order-app-use-run-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/asp-net-core-middleware-pipeline-order-app-use-run-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/asp-net-core-middleware-pipeline-order-app-use-run-768x674.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">The request goes 1 then 2 then Run, and the response comes back 2 then 1</figcaption></figure>
</div>


<p class="wp-block-paragraph">The client got <code>Order #1042 shipped to Austin, TX</code>, and the console shows the request going in and coming back out in reverse.</p>



<p class="wp-block-paragraph"><strong>How does this work?</strong></p>



<p class="wp-block-paragraph">The first <code>app.Use</code> prints its line, then <code>await next(context)</code> hands the request to the second one. That one prints and calls <code>next</code> too, which reaches <code>app.Run</code>.</p>



<p class="wp-block-paragraph"><code>app.Run</code> has no <code>next</code> parameter, so it&#8217;s terminal. It writes the response, and control unwinds back through the second and then the first middleware, which is why the out lines come in reverse order.</p>



<p class="wp-block-paragraph"><strong>Note:</strong> <code>Use</code> has two overloads. Prefer the one where <code>next</code> is a <code>RequestDelegate</code> and you call <code>next(context)</code>; Microsoft says it saves two internal allocations per request over the <code>Func&lt;Task&gt;</code> one.</p>



<h2 class="wp-block-heading">Method 2: Branch the Middleware Pipeline with Map, MapWhen and UseWhen</h2>



<p class="wp-block-paragraph">Branch when only some requests need different middleware: a health check, an old API version or an admin area.</p>



<p class="wp-block-paragraph">Here&#8217;s the code, with paths borrowed from the same orders API:</p>



<pre class="wp-block-code"><code>#:sdk Microsoft.NET.Sdk.Web
var builder = WebApplication.CreateBuilder();
builder.Logging.ClearProviders();
var app = builder.Build();

app.Map(&quot;/health&quot;, health =&gt; health.Run(c =&gt; c.Response.WriteAsync(&quot;Healthy&quot;)));
app.MapWhen(c =&gt; c.Request.Query.ContainsKey(&quot;legacy&quot;),
    v1 =&gt; v1.Run(c =&gt; c.Response.WriteAsync(&quot;Legacy v1 orders&quot;)));
app.UseWhen(c =&gt; c.Request.Path.StartsWithSegments(&quot;/admin&quot;),
    admin =&gt; admin.Use(async (c, next) =&gt;
    {
        c.Response.Headers[&quot;X-Admin-Area&quot;] = &quot;true&quot;;
        await next(c);
    }));
app.Run(c =&gt; c.Response.WriteAsync($&quot;Main pipeline: {c.Request.Path}&quot;));

app.Urls.Add(&quot;http://localhost:5790&quot;);
await app.StartAsync();
var http = new HttpClient { BaseAddress = new Uri(&quot;http://localhost:5790&quot;) };
foreach (var path in new[] { &quot;/health&quot;, &quot;/orders?legacy=1&quot;, &quot;/admin/report&quot;, &quot;/orders&quot; })
{
    var res = await http.GetAsync(path);
    var tag = res.Headers.Contains(&quot;X-Admin-Area&quot;) ? &quot;  [X-Admin-Area: true]&quot; : &quot;&quot;;
    Console.WriteLine($&quot;{path,-17} -&gt; {await res.Content.ReadAsStringAsync()}{tag}&quot;);
}
await app.StopAsync();</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>/health           -&gt; Healthy
/orders?legacy=1  -&gt; Legacy v1 orders
/admin/report     -&gt; Main pipeline: /admin/report  [X-Admin-Area: true]
/orders           -&gt; Main pipeline: /orders</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/asp-net-core-map-mapwhen-usewhen-branch-pipeline.png" alt="Branch the ASP.NET Core middleware pipeline: BranchPipeline.cs in VS Code with app.Map /health, app.MapWhen on a legacy query string and app.UseWhen for /admin, output shows which branch answered each path" class="wp-image-2802" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/asp-net-core-map-mapwhen-usewhen-branch-pipeline.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/asp-net-core-map-mapwhen-usewhen-branch-pipeline-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/asp-net-core-map-mapwhen-usewhen-branch-pipeline-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/asp-net-core-map-mapwhen-usewhen-branch-pipeline-768x674.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">Map and MapWhen end in their branch, while UseWhen adds a header and rejoins</figcaption></figure>
</div>


<p class="wp-block-paragraph"><strong>How does this work?</strong></p>



<p class="wp-block-paragraph"><code>app.Map(&quot;/health&quot;, ...)</code> matched the path prefix and ran its own <code>Run</code>, so the answer was Healthy. Map also moves the matched segment from <code>Request.Path</code> to <code>Request.PathBase</code> inside the branch.</p>



<p class="wp-block-paragraph"><code>app.MapWhen</code> takes any <code>Func&lt;HttpContext, bool&gt;</code>; <code>Query.ContainsKey</code> works like it does on a <a href="https://aspdotnethelp.com/dictionary-in-csharp/">Dictionary in C#</a>. The <code>?legacy=1</code> request went into its branch and never came back to the main pipeline.</p>



<p class="wp-block-paragraph"><code>app.UseWhen</code> is different. It added the <code>X-Admin-Area</code> header to /admin/report, then rejoined the main pipeline, so the main <code>Run</code> still wrote the body.</p>



<p class="wp-block-paragraph"><strong>Important:</strong> <code>app.Map</code> branches the middleware pipeline. It isn&#8217;t <code>app.MapGet</code>, which maps an endpoint for routing.</p>



<h2 class="wp-block-heading">Method 3: Write a Custom Middleware Class (Convention-Based)</h2>



<p class="wp-block-paragraph">Once middleware has more than a few lines, move it into a class. Microsoft&#8217;s rules for a convention-based class are short:</p>



<ul class="wp-block-list">
<li>a public constructor with a <code>RequestDelegate</code> parameter;</li>

<li>a public method named <code>Invoke</code> or <code>InvokeAsync</code> that returns a <code>Task</code> and takes an <code>HttpContext</code> first;</li>

<li>any other constructor or InvokeAsync parameters come from dependency injection.</li>
</ul>



<p class="wp-block-paragraph">Here&#8217;s the request timing middleware for the same orders API, with its <code>UseRequestTiming</code> extension method in the same file (Middleware/RequestTimingMiddleware.cs):</p>



<pre class="wp-block-code"><code>using System.Diagnostics;

namespace OrdersApi.Middleware;

// Convention-based middleware: created once at startup, InvokeAsync runs for every request.
public class RequestTimingMiddleware(RequestDelegate next, ILogger&lt;RequestTimingMiddleware&gt; logger)
{
    public const string Header = &quot;X-Response-Time-ms&quot;;

    public async Task InvokeAsync(HttpContext context)
    {
        var timer = Stopwatch.StartNew();
        context.Response.OnStarting(() =&gt;
        {
            // runs just before the headers go out, so it&#x27;s still safe to add one
            context.Response.Headers[Header] = timer.ElapsedMilliseconds.ToString();
            return Task.CompletedTask;
        });
        try
        {
            await next(context);
        }
        finally
        {
            logger.LogInformation(&quot;{Method} {Path} -&gt; {Status} in {Elapsed} ms&quot;,
                context.Request.Method, context.Request.Path, context.Response.StatusCode, timer.ElapsedMilliseconds);
        }
    }
}

public static class RequestTimingExtensions
{
    public static IApplicationBuilder UseRequestTiming(this IApplicationBuilder app) =&gt;
        app.UseMiddleware&lt;RequestTimingMiddleware&gt;();
}</code></pre>



<p class="wp-block-paragraph">Program.cs adds it with <code>app.UseRequestTiming();</code> right after the exception handler, so you&#8217;ll see an <code>X-Response-Time-ms</code> header on every response.</p>



<p class="wp-block-paragraph"><strong>How does this work?</strong></p>



<p class="wp-block-paragraph">The <code>Stopwatch</code> starts as the request arrives. <code>Response.OnStarting</code> runs its callback just before the headers go out, so the header lands even though an endpoint writes the body later.</p>



<p class="wp-block-paragraph">The log line sits in a <code>finally</code> block, the same idea as a <a href="https://aspdotnethelp.com/try-catch-in-csharp/">try-catch-finally in C#</a>: it still runs when something deeper throws. The <code>await</code> keeps it all non-blocking, as in <a href="https://aspdotnethelp.com/async-await-in-csharp/">async/await in C#</a>.</p>



<p class="wp-block-paragraph">The extension method just wraps <code>UseMiddleware&lt;RequestTimingMiddleware&gt;()</code>, the same <code>UseXxx</code> pattern the built-in middleware uses.</p>



<p class="wp-block-paragraph"><strong>Important:</strong> this class is built once, when the app starts, so never put a scoped service in its constructor. I added <code>CorrelationContext</code> there as a test, and the API wouldn&#8217;t even start in Development:</p>



<pre class="wp-block-code"><code>System.InvalidOperationException: Cannot resolve scoped service &#x27;OrdersApi.Middleware.CorrelationContext&#x27; from root provider.</code></pre>



<p class="wp-block-paragraph">Ask for scoped services as extra <code>InvokeAsync</code> parameters instead. My <a href="https://aspdotnethelp.com/dependency-injection-lifetimes-aspnet-core/">DI lifetimes guide for ASP.NET Core</a> explains why a singleton can&#8217;t hold a scoped service.</p>



<h2 class="wp-block-heading">Method 4: Factory-Based Middleware with IMiddleware and Scoped Services</h2>



<p class="wp-block-paragraph">If your middleware needs scoped services in its constructor, implement <code>IMiddleware</code>. ASP.NET Core resolves it from DI for every request, and you get strong typing from the interface.</p>



<p class="wp-block-paragraph">Same orders API, now with a correlation ID that ties a request to its log lines (Middleware/CorrelationIdMiddleware.cs):</p>



<pre class="wp-block-code"><code>namespace OrdersApi.Middleware;

// Scoped: one instance per request, shared by the middleware and the endpoint.
public class CorrelationContext
{
    public string CorrelationId { get; set; } = &quot;&quot;;
}

// Factory-based middleware: resolved from DI for every request, so scoped services can go in the constructor.
public class CorrelationIdMiddleware(CorrelationContext correlation, ILogger&lt;CorrelationIdMiddleware&gt; logger) : IMiddleware
{
    public const string Header = &quot;X-Correlation-ID&quot;;

    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        string? incoming = context.Request.Headers[Header];
        correlation.CorrelationId = string.IsNullOrWhiteSpace(incoming)
            ? Guid.NewGuid().ToString(&quot;N&quot;)[..12]
            : incoming;
        context.Response.Headers[Header] = correlation.CorrelationId;   // nothing is written yet

        logger.LogInformation(&quot;{Id} {Path} user={User}&quot;, correlation.CorrelationId,
            context.Request.Path, context.User.Identity?.Name ?? &quot;anonymous&quot;);
        await next(context);
    }
}

public static class CorrelationIdExtensions
{
    public static IApplicationBuilder UseCorrelationId(this IApplicationBuilder app) =&gt;
        app.UseMiddleware&lt;CorrelationIdMiddleware&gt;();
}</code></pre>



<p class="wp-block-paragraph">Program.cs registers both classes with <code>AddScoped&lt;CorrelationContext&gt;()</code> and <code>AddTransient&lt;CorrelationIdMiddleware&gt;()</code>, then calls <code>app.UseCorrelationId()</code> after authorization.</p>



<p class="wp-block-paragraph"><strong>How does this work?</strong></p>



<p class="wp-block-paragraph"><code>UseMiddleware</code> sees that the class implements <code>IMiddleware</code> and asks the <code>IMiddlewareFactory</code> for an instance. That&#8217;s why the class has to be registered, as scoped or transient.</p>



<p class="wp-block-paragraph">It reuses the caller&#8217;s <code>X-Correlation-ID</code> or makes a short <a href="https://aspdotnethelp.com/how-to-create-a-guid-in-c-net/">GUID in C#</a>. The header is set before <code>next</code>, while nothing is written yet.</p>



<p class="wp-block-paragraph">The <code>/orders</code> endpoint asks for <code>CorrelationContext</code> too, and because it&#8217;s scoped, it&#8217;s the same object, so the JSON echoes the ID back.</p>



<p class="wp-block-paragraph"><strong>Important:</strong> I deleted the <code>AddTransient</code> line to see what breaks. The app started, but every request returned 500 with this logged:</p>



<pre class="wp-block-code"><code>System.InvalidOperationException: No service for type &#x27;OrdersApi.Middleware.CorrelationIdMiddleware&#x27; has been registered.</code></pre>



<p class="wp-block-paragraph">Also, you can&#8217;t pass extra arguments to an <code>IMiddleware</code> class through <code>UseMiddleware&lt;T&gt;(args)</code>; Microsoft documents that as a <code>NotSupportedException</code>. The <a href="https://aspdotnethelp.com/interface-in-csharp/">interface in C#</a> contract is the only signature it gets.</p>



<h2 class="wp-block-heading">The Recommended ASP.NET Core Middleware Order</h2>



<p class="wp-block-paragraph">Microsoft&#8217;s rule: the order in the Program file is the order middleware runs on a request, and the reverse on the response. Here&#8217;s the order the docs recommend for common apps, with my one-line reason for each:</p>



<figure class="wp-block-table"><table><thead><tr><th>#</th><th>Middleware</th><th>Why it sits here</th></tr></thead><tbody><tr><td>1</td><td>UseExceptionHandler (developer exception page in Development)</td><td>Catches exceptions thrown by everything after it</td></tr><tr><td>2</td><td>UseHsts (outside Development)</td><td>Adds the Strict-Transport-Security header</td></tr><tr><td>3</td><td>UseHttpsRedirection</td><td>Redirects HTTP to HTTPS before anything reads the URL</td></tr><tr><td>4</td><td>UseStaticFiles or MapStaticAssets</td><td>Serves files early and short-circuits</td></tr><tr><td>5</td><td>UseRouting</td><td>Picks the endpoint</td></tr><tr><td>6</td><td>UseCors</td><td>Before authentication, authorization and response caching</td></tr><tr><td>7</td><td>UseAuthentication</td><td>Sets HttpContext.User</td></tr><tr><td>8</td><td>UseAuthorization</td><td>Right after authentication, checks the endpoint&#8217;s policy</td></tr><tr><td>9</td><td>Your custom middleware</td><td>Can see the endpoint and the signed-in user</td></tr><tr><td>10</td><td>Endpoints (MapGet, MapControllers, MapRazorPages)</td><td>Run last and write the response</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Here&#8217;s the full Program.cs of the sample orders API in that order. The timing middleware and the bot blocker go up front on purpose, because they should see every request:</p>



<pre class="wp-block-code"><code>using System.Globalization;
using Microsoft.AspNetCore.Authentication;
using OrdersApi;
using OrdersApi.Middleware;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddProblemDetails();
builder.Services.AddCors(o =&gt; o.AddDefaultPolicy(p =&gt; p.WithOrigins(&quot;https://shop.example.com&quot;)));
builder.Services.AddAuthentication(ApiKeyHandler.SchemeName)
    .AddScheme&lt;AuthenticationSchemeOptions, ApiKeyHandler&gt;(ApiKeyHandler.SchemeName, null);
builder.Services.AddAuthorization();
builder.Services.AddScoped&lt;CorrelationContext&gt;();
builder.Services.AddTransient&lt;CorrelationIdMiddleware&gt;();   // IMiddleware classes must be registered

var app = builder.Build();

// 1. Exception handler first, so it catches errors thrown anywhere below it
app.UseExceptionHandler();
if (!app.Environment.IsDevelopment())
{
    app.UseHsts();
}
// app.UseHttpsRedirection();   // on in production; this demo listens on http only
// app.UseStaticFiles();        // or app.MapStaticAssets() when the app serves files

// 2. Custom middleware that must see every request
app.UseRequestTiming();
app.Use(async (context, next) =&gt;
{
    var path = context.Request.Path;
    if (path.StartsWithSegments(&quot;/wp-admin&quot;) || path.Value!.EndsWith(&quot;.php&quot;))
    {
        context.Response.StatusCode = StatusCodes.Status404NotFound;
        return;                                  // short-circuit: next is never called
    }
    await next(context);
});

// 3. Routing, CORS, authentication, authorization - in this order
app.UseRouting();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();

// 4. Custom middleware that needs the matched endpoint and the signed-in user
app.UseCorrelationId();

// 5. Endpoints
var usDollars = CultureInfo.GetCultureInfo(&quot;en-US&quot;);
app.MapGet(&quot;/orders&quot;, (CorrelationContext correlation) =&gt;
    new { correlation.CorrelationId, Orders = SampleOrders.All });
app.MapGet(&quot;/orders/fail&quot;, string () =&gt; throw new InvalidOperationException(&quot;Payment gateway timed out&quot;));
app.MapGet(&quot;/admin/report&quot;, (HttpContext context) =&gt;
    $&quot;Q4 2026 report for {context.User.Identity!.Name}: {SampleOrders.All.Sum(o =&gt; o.Total).ToString(&quot;C&quot;, usDollars)}&quot;)
    .RequireAuthorization();

app.Run();

record Order(int Id, string Customer, string City, decimal Total);

static class SampleOrders
{
    public static readonly Order[] All =
    [
        new(1042, &quot;Jenna Morales&quot;, &quot;Austin, TX&quot;, 84.50m),
        new(1043, &quot;Tyler Brooks&quot;, &quot;Columbus, OH&quot;, 129.99m),
        new(1044, &quot;Rachel Kim&quot;, &quot;Seattle, WA&quot;, 42.25m),
    ];
}</code></pre>



<p class="wp-block-paragraph">The three orders are a <a href="https://aspdotnethelp.com/csharp-record-vs-class-vs-struct/">C# record</a>, and the report total uses a <a href="https://aspdotnethelp.com/linq-in-csharp/">LINQ Sum</a> formatted as en-US dollars. The demo authentication handler (ApiKeyHandler.cs) is deliberately tiny:</p>



<pre class="wp-block-code"><code>using System.Security.Claims;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;

namespace OrdersApi;

// Demo only: a fixed API key. Use a real identity provider in production.
public class ApiKeyHandler(IOptionsMonitor&lt;AuthenticationSchemeOptions&gt; options, ILoggerFactory logger, UrlEncoder encoder)
    : AuthenticationHandler&lt;AuthenticationSchemeOptions&gt;(options, logger, encoder)
{
    public const string SchemeName = &quot;ApiKey&quot;;

    protected override Task&lt;AuthenticateResult&gt; HandleAuthenticateAsync()
    {
        if (!Request.Headers.TryGetValue(&quot;X-Api-Key&quot;, out var key))
            return Task.FromResult(AuthenticateResult.NoResult());
        if (key != &quot;demo-key-2026&quot;)
            return Task.FromResult(AuthenticateResult.Fail(&quot;Invalid API key&quot;));

        var identity = new ClaimsIdentity([new Claim(ClaimTypes.Name, &quot;jenna.morales&quot;)], SchemeName);
        var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), SchemeName);
        return Task.FromResult(AuthenticateResult.Success(ticket));
    }
}</code></pre>



<p class="wp-block-paragraph"><strong>Note:</strong> <code>WebApplication</code> adds <code>UseRouting</code>, <code>UseAuthentication</code> and <code>UseAuthorization</code> for you when you don&#8217;t call them, right at the start of the pipeline. Call them yourself, as above, when middleware like <code>UseCorrelationId</code> must come after them.</p>



<h2 class="wp-block-heading">Test the Request Timing and Correlation ID Middleware</h2>



<p class="wp-block-paragraph">With the API running in the background, this client sends six requests and prints the status and both headers. It&#8217;s a plain <a href="https://aspdotnethelp.com/httpclient-in-csharp/">HttpClient in C#</a> call per line:</p>



<pre class="wp-block-code"><code>var http = new HttpClient { BaseAddress = new Uri(&quot;http://localhost:5790&quot;) };

async Task Call(string path, string? apiKey = null, string? correlationId = null)
{
    var request = new HttpRequestMessage(HttpMethod.Get, path);
    if (apiKey is not null) request.Headers.Add(&quot;X-Api-Key&quot;, apiKey);
    if (correlationId is not null) request.Headers.Add(&quot;X-Correlation-ID&quot;, correlationId);
    var res = await http.SendAsync(request);
    string Header(string name) =&gt; res.Headers.TryGetValues(name, out var v) ? v.First() : &quot;-&quot;;
    Console.WriteLine($&quot;{path,-15}{(int)res.StatusCode,-5}id={Header(&quot;X-Correlation-ID&quot;),-15}{Header(&quot;X-Response-Time-ms&quot;)} ms&quot;);
}

await Call(&quot;/orders&quot;);
await Call(&quot;/orders&quot;, correlationId: &quot;order-1042-tx&quot;);
await Call(&quot;/admin/report&quot;);
await Call(&quot;/admin/report&quot;, apiKey: &quot;demo-key-2026&quot;);
await Call(&quot;/wp-login.php&quot;);
await Call(&quot;/orders/fail&quot;);</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>/orders        200  id=701240978db2   45 ms
/orders        200  id=order-1042-tx  4 ms
/admin/report  401  id=-              5 ms
/admin/report  200  id=117c47a0bd4f   8 ms
/wp-login.php  404  id=-              0 ms
/orders/fail   500  id=-              22 ms</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/asp-net-core-custom-middleware-correlation-id-timing-test.png" alt="Test ASP.NET Core custom middleware: TestApi.cs in VS Code calls the orders API and prints status 200, 401, 404 and 500 with the X-Correlation-ID and X-Response-Time-ms headers for each request" class="wp-image-2801" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/asp-net-core-custom-middleware-correlation-id-timing-test.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/asp-net-core-custom-middleware-correlation-id-timing-test-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/asp-net-core-custom-middleware-correlation-id-timing-test-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/asp-net-core-custom-middleware-correlation-id-timing-test-768x674.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">Every response has a timing header, but only requests that reach the correlation middleware get an ID</figcaption></figure>
</div>


<p class="wp-block-paragraph"><strong>How does this work?</strong></p>



<p class="wp-block-paragraph">The first <code>/orders</code> call got a generated 12-character ID, and the second got back the <code>order-1042-tx</code> it sent. The timing header is on every single line because that middleware sits near the top.</p>



<p class="wp-block-paragraph"><code>/admin/report</code> with no key returned 401 and no ID. Authorization stopped the request before it reached the correlation middleware, so the order decides which middleware sees a request.</p>



<p class="wp-block-paragraph">With the key it returned 200 (<code>Q4 2026 report for jenna.morales: $256.74</code>), and <code>/wp-login.php</code> got 404 from the bot blocker.</p>



<p class="wp-block-paragraph">So here&#8217;s the catch: <code>/orders/fail</code> returned 500 without its correlation ID. The exception handler clears the response before it writes the error, but the timing header survived because <code>OnStarting</code> runs later.</p>



<p class="wp-block-paragraph">Here&#8217;s the same /orders call through curl, with the headers:</p>



<pre class="wp-block-code"><code>curl.exe -i -H &quot;X-Correlation-ID: order-1042-tx&quot; http://localhost:5790/orders</code></pre>



<pre class="wp-block-code"><code>HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Server: Kestrel
Transfer-Encoding: chunked
X-Correlation-ID: order-1042-tx
X-Response-Time-ms: 0

{&quot;correlationId&quot;:&quot;order-1042-tx&quot;,&quot;orders&quot;:[{&quot;id&quot;:1042,&quot;customer&quot;:&quot;Jenna Morales&quot;,&quot;city&quot;:&quot;Austin, TX&quot;,&quot;total&quot;:84.50},{&quot;id&quot;:1043,&quot;customer&quot;:&quot;Tyler Brooks&quot;,&quot;city&quot;:&quot;Columbus, OH&quot;,&quot;total&quot;:129.99},{&quot;id&quot;:1044,&quot;customer&quot;:&quot;Rachel Kim&quot;,&quot;city&quot;:&quot;Seattle, WA&quot;,&quot;total&quot;:42.25}]}</code></pre>



<p class="wp-block-paragraph">And the API&#8217;s own log for the /orders/fail request:</p>



<pre class="wp-block-code"><code>info: OrdersApi.Middleware.CorrelationIdMiddleware[0]
      4ad5e92245fb /orders/fail user=anonymous
info: OrdersApi.Middleware.RequestTimingMiddleware[0]
      GET /orders/fail -&gt; 200 in 1 ms</code></pre>



<p class="wp-block-paragraph"><strong>Note:</strong> the timing log says <code>/orders/fail -&gt; 200</code>, while the client got 500. The exception handler is outside the timing middleware, so it set the status code after timing had already logged.</p>



<h2 class="wp-block-heading">How Do You Short-Circuit ASP.NET Core Middleware?</h2>



<p class="wp-block-paragraph">You short-circuit by not calling <code>next</code>, like the bot blocker in Program.cs: it sets 404 and returns. The rest of the pipeline is skipped, but middleware registered earlier still runs its after-<code>next</code> code.</p>



<p class="wp-block-paragraph">A probe for <code>/wp-admin/install.php</code> got 404 with no routing, authentication or endpoint work at all. Built-in middleware does this too: static files end the pipeline when a file matches, and authorization returns 401 or 403 without calling the endpoint.</p>



<h2 class="wp-block-heading">Common ASP.NET Core Middleware Mistakes</h2>



<h3 class="wp-block-heading">UseAuthorization before UseAuthentication</h3>



<p class="wp-block-paragraph">This is the order bug I see most, and my <a href="https://aspdotnethelp.com/jwt-authentication-aspnet-core/">JWT authentication guide</a> hit it too. I swapped the two lines in the same Program.cs:</p>



<pre class="wp-block-code"><code>app.UseAuthorization();
app.UseAuthentication();</code></pre>



<p class="wp-block-paragraph">A request with the valid key now got <code>401 Unauthorized</code>. Authorization ran first and saw an anonymous user, because authentication hadn&#8217;t set <code>HttpContext.User</code> yet. Put them back in that order: authentication, then authorization.</p>



<h3 class="wp-block-heading">Changing headers after next() has started the response</h3>



<p class="wp-block-paragraph">This is the timing middleware written the obvious way, setting the header after the endpoint wrote its body:</p>



<pre class="wp-block-code"><code>#:sdk Microsoft.NET.Sdk.Web
using System.Diagnostics;

var builder = WebApplication.CreateBuilder();
builder.Logging.ClearProviders();
var app = builder.Build();

app.Use(async (context, next) =&gt;
{
    var timer = Stopwatch.StartNew();
    await next(context);
    try
    {
        context.Response.Headers[&quot;X-Response-Time-ms&quot;] = timer.ElapsedMilliseconds.ToString();
    }
    catch (InvalidOperationException ex)
    {
        Console.WriteLine($&quot;HasStarted = {context.Response.HasStarted}&quot;);
        Console.WriteLine($&quot;{ex.GetType().Name}: {ex.Message}&quot;);
    }
});
app.Run(context =&gt; context.Response.WriteAsync(&quot;3 open orders&quot;));

app.Urls.Add(&quot;http://localhost:5790&quot;);
await app.StartAsync();
var res = await new HttpClient().GetAsync(&quot;http://localhost:5790/&quot;);
Console.WriteLine($&quot;Client got {(int)res.StatusCode}, timing header sent: {res.Headers.Contains(&quot;X-Response-Time-ms&quot;)}&quot;);
await app.StopAsync();</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>HasStarted = True
InvalidOperationException: Headers are read-only, response has already started.
Client got 200, timing header sent: False</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/headers-are-read-only-response-has-already-started-middleware.png" alt="Headers are read-only, response has already started: LateHeader.cs in VS Code sets X-Response-Time-ms after await next, output shows HasStarted True, the InvalidOperationException and that the client never got the header" class="wp-image-2805" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/headers-are-read-only-response-has-already-started-middleware.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/headers-are-read-only-response-has-already-started-middleware-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/headers-are-read-only-response-has-already-started-middleware-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/headers-are-read-only-response-has-already-started-middleware-768x674.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">Once the body starts, headers are read-only and the client never sees the late header</figcaption></figure>
</div>


<p class="wp-block-paragraph">The client still got 200, so this bug hides easily. Check <code>context.Response.HasStarted</code> before touching headers, or register them in <code>OnStarting</code> like Method 3 does.</p>



<h2 class="wp-block-heading">Which ASP.NET Core Middleware Approach Should You Use?</h2>



<figure class="wp-block-table"><table><thead><tr><th>Approach</th><th>Use it when</th><th>Lifetime</th></tr></thead><tbody><tr><td>Inline <code>app.Use</code></td><td>A few lines, one app, like the bot blocker</td><td>Built once</td></tr><tr><td><code>Map</code> / <code>MapWhen</code></td><td>A path or condition gets its own mini pipeline</td><td>Built once</td></tr><tr><td><code>UseWhen</code></td><td>Extra middleware for some requests, then rejoin</td><td>Built once</td></tr><tr><td>Convention-based class</td><td>Reusable middleware with singleton-safe dependencies</td><td>Once per app; scoped services go in InvokeAsync</td></tr><tr><td><code>IMiddleware</code></td><td>You want scoped services in the constructor and strong typing</td><td>Per request, from DI</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">I&#8217;d pick a convention-based class with a UseXxx extension for most shared middleware, and IMiddleware when you need a DbContext in the constructor.</p>



<h2 class="wp-block-heading">Things to Keep in Mind</h2>



<ul class="wp-block-list">
<li><strong>Exception handler first.</strong> It only catches what runs after it, so register it before everything else.</li>

<li><strong>Authentication before authorization.</strong> Swapping them returned 401 for a valid key in my test.</li>

<li><strong>Headers before the body.</strong> Set them before <code>next</code> or in <code>OnStarting</code>; after the response starts they&#8217;re read-only.</li>

<li><strong>Mind the automatic calls.</strong> Call <code>UseRouting</code> and the auth middleware yourself when your own middleware must sit between them.</li>
</ul>



<p class="wp-block-paragraph">These pages pair well with this one:</p>



<ul class="wp-block-list">
<li><a href="https://aspdotnethelp.com/asp-net-core-tutorials/">All ASP.NET Core tutorials on AspDotnetHelp</a></li>

<li><a href="https://aspdotnethelp.com/aspnet-core-web-api-crud-ef-core/">Build an ASP.NET Core Web API with EF Core</a></li>

<li><a href="https://aspdotnethelp.com/openapi-scalar-aspnet-core/">Add OpenAPI and Scalar to ASP.NET Core</a></li>

<li><a href="https://aspdotnethelp.com/upgrade-dotnet-8-to-dotnet-10/">Upgrade an app from .NET 8 to .NET 10</a></li>
</ul>



<h2 class="wp-block-heading">Frequently Asked Questions</h2>


<div id="rank-math-faq" class="rank-math-block">
<div class="rank-math-list ">
<div id="faq-question-mw0000001" class="rank-math-list-item">
<h3 class="rank-math-question ">What is the difference between middleware and filters in ASP.NET Core?</h3>
<div class="rank-math-answer ">

<p>Middleware runs for every request and only knows the HttpContext. Filters run inside MVC, or as endpoint filters in minimal APIs, so they see the action and its arguments. Use a filter when you need that context.</p>

</div>
</div>
<div id="faq-question-mw0000002" class="rank-math-list-item">
<h3 class="rank-math-question ">What is the correct middleware order in ASP.NET Core?</h3>
<div class="rank-math-answer ">

<p>Exception handling, HSTS, HTTPS redirection, static files, routing, CORS, authentication, authorization, your custom middleware, then endpoints.</p>

</div>
</div>
<div id="faq-question-mw0000003" class="rank-math-list-item">
<h3 class="rank-math-question ">What is the difference between app.Use and app.Run?</h3>
<div class="rank-math-answer ">

<p><code>app.Use</code> gets a <code>next</code> delegate and can pass the request on. <code>app.Run</code> has no <code>next</code>, ends the pipeline, and anything registered after the first Run never runs.</p>

</div>
</div>
<div id="faq-question-mw0000004" class="rank-math-list-item">
<h3 class="rank-math-question ">Can ASP.NET Core middleware use dependency injection?</h3>
<div class="rank-math-answer ">

<p>Yes. Convention-based middleware takes singleton-safe services in its constructor and scoped ones as extra <code>InvokeAsync</code> parameters. A class that implements <code>IMiddleware</code> is resolved per request, so it can take scoped services in its constructor.</p>

</div>
</div>
<div id="faq-question-mw0000005" class="rank-math-list-item">
<h3 class="rank-math-question ">How do I add a header to every response in middleware?</h3>
<div class="rank-math-answer ">

<p>Set it before calling <code>next</code>, or register it with <code>context.Response.OnStarting</code>. Setting it after the endpoint wrote the body throws &quot;Headers are read-only, response has already started.&quot;</p>

</div>
</div>
<div id="faq-question-mw0000006" class="rank-math-list-item">
<h3 class="rank-math-question ">Do I have to call UseRouting in .NET 10?</h3>
<div class="rank-math-answer ">

<p>No. WebApplication adds UseRouting, UseAuthentication and UseAuthorization automatically when you don&#8217;t call them. Call them yourself when your middleware must run between them. See <a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-10.0" target="_blank" rel="noopener">ASP.NET Core middleware on Microsoft Learn</a>.</p>

</div>
</div>
</div>
</div>]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Pattern Matching in C#: is, switch Expressions and Property Patterns</title>
		<link>https://aspdotnethelp.com/csharp-pattern-matching/</link>
		
		<dc:creator><![CDATA[Bijay Kumar]]></dc:creator>
		<pubDate>Mon, 09 Nov 2026 21:20:00 +0000</pubDate>
				<category><![CDATA[C#]]></category>
		<guid isPermaLink="false">https://aspdotnethelp.com/?p=2789</guid>

					<description><![CDATA[Say you&#8217;re writing shipping rules for an online outdoor store. Orders of $100 or more ship free, Alaska and Hawaii pay $24.99, and anything over 50 pounds goes freight. C# pattern matching tests a value&#8217;s type, ranges and properties in one expression with is or a switch expression, and the compiler warns you about cases ... <a title="Pattern Matching in C#: is, switch Expressions and Property Patterns" class="read-more" href="https://aspdotnethelp.com/csharp-pattern-matching/" aria-label="Read more about Pattern Matching in C#: is, switch Expressions and Property Patterns">Read more &#62;&#62;</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Say you&#8217;re writing shipping rules for an online outdoor store. Orders of $100 or more ship free, Alaska and Hawaii pay $24.99, and anything over 50 pounds goes freight.</p>



<p class="wp-block-paragraph"><strong>C# pattern matching</strong> tests a value&#8217;s type, ranges and properties in one expression with <code>is</code> or a <code>switch</code> expression, and the compiler warns you about cases you forgot.</p>



<p class="wp-block-paragraph"><strong>Quick answer:</strong> use <code>x is Type t</code> for one check and a switch expression for a rule set. I ran everything below on .NET 10 against the same store orders.</p>



<pre class="wp-block-code"><code>decimal Shipping(Order order) =&gt; order switch
{
    { Total: &gt;= 100m } =&gt; 0m,
    { ShipTo.State: &quot;AK&quot; or &quot;HI&quot; } =&gt; 24.99m,
    { WeightLbs: &gt; 50 } =&gt; 89.00m,
    _ =&gt; 7.99m
};</code></pre>



<h2 class="wp-block-heading">What Is Pattern Matching in C#?</h2>



<p class="wp-block-paragraph">A pattern is a test you write on the right side of <code>is</code>, after <code>case</code> in a switch statement, or on the left of <code>=&gt;</code> in a switch expression. If the value matches, you can also capture parts of it into new variables.</p>



<p class="wp-block-paragraph">Each kind arrived in a different C# version. On .NET 10 (C# 14) you get all of them:</p>



<figure class="wp-block-table"><table><thead><tr><th>Pattern</th><th>Example</th><th>Since</th></tr></thead><tbody><tr><td>Declaration / type</td><td><code>payment is decimal amount</code>, <code>Car =&gt;</code></td><td>C# 7 / C# 9</td></tr><tr><td>Constant</td><td><code>"AK"</code>, <code>null</code>, <code>0</code></td><td>C# 7</td></tr><tr><td>var and discard</td><td><code>var x</code>, <code>_</code></td><td>C# 7 / C# 8</td></tr><tr><td>Property</td><td><code>{ Total: &gt;= 100m }</code></td><td>C# 8</td></tr><tr><td>Positional</td><td><code>("Denver", _, _)</code></td><td>C# 8</td></tr><tr><td>Relational</td><td><code>&lt; 20</code>, <code>&gt;= 1</code></td><td>C# 9</td></tr><tr><td>Logical</td><td><code>and</code>, <code>or</code>, <code>not</code></td><td>C# 9</td></tr><tr><td>Extended property</td><td><code>{ ShipTo.State: "AK" }</code></td><td>C# 10</td></tr><tr><td>List and slice</td><td><code>["Created", .., "Delivered"]</code></td><td>C# 11</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">New file-based apps target the latest language version, so every sample here is a single .cs file you start with <code>dotnet run</code>. If that&#8217;s new to you, read my guide to <a href="https://aspdotnethelp.com/dotnet-run-file-based-apps/">dotnet run file-based apps</a> first.</p>



<h2 class="wp-block-heading">Type Patterns and Declaration Patterns with is</h2>



<p class="wp-block-paragraph">The store accepts several payment shapes, and they arrive as object. A declaration pattern checks the runtime type and gives you a typed variable in one step:</p>



<pre class="wp-block-code"><code>using System.Globalization;
CultureInfo.CurrentCulture = new(&quot;en-US&quot;);

object?[] payments = [49.99m, &quot;PAYPAL-7781&quot;, 120, null, new Card(&quot;Visa&quot;, &quot;4242&quot;)];
foreach (object? p in payments)
    Console.WriteLine(Describe(p));

static string Describe(object? payment)
{
    if (payment is null) return &quot;No payment on file&quot;;
    if (payment is decimal amount) return $&quot;Cash {amount:C}&quot;;
    if (payment is string code &amp;&amp; code.StartsWith(&quot;PAYPAL&quot;))
        return $&quot;PayPal ref {code[7..]}&quot;;
    if (payment is Card card) return $&quot;{card.Brand} ending {card.Last4}&quot;;
    return $&quot;Unknown: {payment.GetType().Name}&quot;;
}
record Card(string Brand, string Last4);</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>Cash $49.99
PayPal ref 7781
Unknown: Int32
No payment on file
Visa ending 4242</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-pattern-matching-is-type-declaration-pattern.png" alt="VS Code running IsPatterns.cs: declaration patterns with is turn decimal, string, null and Card payments into labels, while a boxed int 120 falls through as Int32" class="wp-image-2796" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-pattern-matching-is-type-declaration-pattern.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-pattern-matching-is-type-declaration-pattern-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-pattern-matching-is-type-declaration-pattern-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-pattern-matching-is-type-declaration-pattern-768x674.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">Declaration patterns with is: one check, one typed variable</figcaption></figure>
</div>


<p class="wp-block-paragraph">The 120 printed <code>Unknown: Int32</code>. A boxed int doesn&#8217;t match <code>decimal</code>, because the pattern only unboxes to the exact type. That&#8217;s the same rule behind <a href="https://aspdotnethelp.com/boxing-and-unboxing-in-csharp/">boxing and unboxing in C#</a>.</p>



<p class="wp-block-paragraph">Before C# 7 you&#8217;d write <code>as</code> plus a null check, or a cast that throws. My <a href="https://aspdotnethelp.com/type-casting-in-csharp/">type casting in C#</a> post compares those; for type tests, I always use <code>is</code> now.</p>



<p class="wp-block-paragraph">Two details I lean on: <code>amount</code> only exists where the match succeeded, and <code>code.StartsWith</code> runs only after <code>code</code> is known to be a string, thanks to <code>&amp;&amp;</code>.</p>



<h2 class="wp-block-heading">Null Checks with Pattern Matching: is null and is not null</h2>



<p class="wp-block-paragraph">Microsoft Learn guarantees that <code>x is null</code> never calls a user-overloaded <code>==</code>. Here&#8217;s why that matters, with a product SKU class whose <code>==</code> treats a blank code as null:</p>



<pre class="wp-block-code"><code>var blank = new Sku(&quot;&quot;);
Sku? missing = null;

Console.WriteLine($&quot;blank == null:       {blank == null}&quot;);
Console.WriteLine($&quot;blank is null:       {blank is null}&quot;);
Console.WriteLine($&quot;missing is not null: {missing is not null}&quot;);

if (FindSku(&quot;TENT-2P&quot;) is { } sku)
    Console.WriteLine($&quot;Found {sku.Code}&quot;);

static Sku? FindSku(string code) =&gt; code == &quot;TENT-2P&quot; ? new Sku(code) : null;

class Sku(string code)
{
    public string Code =&gt; code;
    public static bool operator ==(Sku? a, Sku? b) =&gt; (a?.Code ?? &quot;&quot;) == (b?.Code ?? &quot;&quot;);
    public static bool operator !=(Sku? a, Sku? b) =&gt; !(a == b);
    public override bool Equals(object? o) =&gt; o is Sku s &amp;&amp; s.Code == Code;
    public override int GetHashCode() =&gt; Code.GetHashCode();
}</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>blank == null:       True
blank is null:       False
missing is not null: False
Found TENT-2P</code></pre>



<p class="wp-block-paragraph">The overloaded operator said a real object equals null. <code>is null</code> ignored it and told the truth. <code>is not null</code> is the readable way to write the opposite.</p>



<p class="wp-block-paragraph">The last line uses the empty property pattern <code>is { } sku</code>: it matches any non-null value and declares the variable. More null techniques are in <a href="https://aspdotnethelp.com/how-to-check-for-null-values-in-c-sharp/">how to check for null values in C#</a>.</p>



<h2 class="wp-block-heading">Pattern Matching Switch Expressions with Relational and Logical Patterns</h2>



<p class="wp-block-paragraph">Same store, now pricing by state and package weight. Constant patterns match exact values, relational patterns (<code>&lt;</code>, <code>&lt;=</code>, <code>&gt;</code>, <code>&gt;=</code>) match ranges, and <code>and</code>, <code>or</code>, <code>not</code> combine them:</p>



<pre class="wp-block-code"><code>foreach (var s in new[] { &quot;CA&quot;, &quot;TX&quot;, &quot;NY&quot;, &quot;AK&quot; })
    Console.WriteLine($&quot;{s}: {Zone(s)}&quot;);
foreach (var lbs in new[] { 0.5, 4, 25, 80 })
    Console.WriteLine($&quot;{lbs} lb: {Rate(lbs)}&quot;);

static string Zone(string state) =&gt; state switch
{
    &quot;CA&quot; or &quot;OR&quot; or &quot;WA&quot; =&gt; &quot;West&quot;,
    &quot;AK&quot; or &quot;HI&quot; =&gt; &quot;Noncontiguous&quot;,
    not (&quot;TX&quot; or &quot;OK&quot;) =&gt; &quot;Standard&quot;,
    _ =&gt; &quot;South&quot;
};

static string Rate(double lbs) =&gt; lbs switch
{
    &lt;= 0 =&gt; throw new ArgumentOutOfRangeException(nameof(lbs)),
    &lt; 1 =&gt; &quot;$4.99 envelope&quot;,
    &gt;= 1 and &lt; 20 =&gt; &quot;$8.99 ground&quot;,
    &gt;= 20 and &lt;= 70 =&gt; &quot;$19.99 heavy&quot;,
    _ =&gt; &quot;freight quote&quot;
};</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>CA: West
TX: South
NY: Standard
AK: Noncontiguous
0.5 lb: $4.99 envelope
4 lb: $8.99 ground
25 lb: $19.99 heavy
80 lb: freight quote</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-switch-expression-relational-logical-patterns.png" alt="VS Code running ShippingRates.cs: a switch expression maps CA to West, TX to South, NY to Standard, AK to Noncontiguous, and 0.5 to 80 lb weights to four rates" class="wp-image-2799" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-switch-expression-relational-logical-patterns.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-switch-expression-relational-logical-patterns-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-switch-expression-relational-logical-patterns-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-switch-expression-relational-logical-patterns-768x674.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">Constant, relational and logical patterns in two switch expressions</figcaption></figure>
</div>


<p class="wp-block-paragraph">Arms run top to bottom and the first match wins. That&#8217;s why <code>TX</code> reached the discard <code>_</code>: the <code>not (&quot;TX&quot; or &quot;OK&quot;)</code> arm sent every other state to Standard first.</p>



<p class="wp-block-paragraph">The right side of a relational pattern must be a constant, so you can&#8217;t write <code>&lt; maxWeight</code> with a variable. Use a <code>when</code> guard for that (below).</p>



<p class="wp-block-paragraph">A switch expression returns a value; the classic statement runs code. If you still use <code>case</code> labels and <code>break</code>, my <a href="https://aspdotnethelp.com/switch-case-in-csharp/">switch case in C#</a> guide covers both side by side.</p>



<h2 class="wp-block-heading">Property Patterns and Extended Property Patterns</h2>



<p class="wp-block-paragraph">This is the code from the top of the post, run on four sample orders. A property pattern checks properties or fields against nested patterns, and it never matches null:</p>



<pre class="wp-block-code"><code>using System.Globalization;
CultureInfo.CurrentCulture = new(&quot;en-US&quot;);

Order[] orders =
[
    new(1001, 142.50m, 12, new(&quot;Denver&quot;, &quot;CO&quot;)),
    new(1002, 38.00m, 3, new(&quot;Anchorage&quot;, &quot;AK&quot;)),
    new(1003, 64.25m, 58, new(&quot;Austin&quot;, &quot;TX&quot;)),
    new(1004, 19.99m, 1, new(&quot;Columbus&quot;, &quot;OH&quot;)),
];
foreach (var o in orders)
    Console.WriteLine($&quot;#{o.Id} {o.ShipTo.State}: {Shipping(o):C}&quot;);

static decimal Shipping(Order order) =&gt; order switch
{
    { Total: &gt;= 100m } =&gt; 0m,
    { ShipTo.State: &quot;AK&quot; or &quot;HI&quot; } =&gt; 24.99m,
    { WeightLbs: &gt; 50 } =&gt; 89.00m,
    _ =&gt; 7.99m
};
record Address(string City, string State);
record Order(int Id, decimal Total, double WeightLbs, Address ShipTo);</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>#1001 CO: $0.00
#1002 AK: $24.99
#1003 TX: $89.00
#1004 OH: $7.99</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-property-pattern-extended-property-switch.png" alt="VS Code running PropertyPatterns.cs: order 1001 to Colorado ships free, 1002 to Alaska pays $24.99, the 58 lb Texas order pays $89.00 and Ohio pays $7.99" class="wp-image-2797" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-property-pattern-extended-property-switch.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-property-pattern-extended-property-switch-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-property-pattern-extended-property-switch-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-property-pattern-extended-property-switch-768x674.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">Property patterns with an extended ShipTo.State pattern</figcaption></figure>
</div>


<p class="wp-block-paragraph"><code>ShipTo.State</code> is an extended property pattern (C# 10). It&#8217;s the short form of <code>{ ShipTo: { State: &quot;AK&quot; or &quot;HI&quot; } }</code>, and Visual Studio&#8217;s IDE0170 rule suggests it.</p>



<p class="wp-block-paragraph">Order matters here too. A $150 Alaska order ships free, because the free-shipping arm comes first. If Alaska should always pay, move that arm up.</p>



<p class="wp-block-paragraph">These are positional records, so the property names come from the constructor. Why I use records for orders is in <a href="https://aspdotnethelp.com/csharp-record-vs-class-vs-struct/">record vs class vs struct in C#</a>.</p>



<h2 class="wp-block-heading">Positional Patterns with Records, Tuples and Deconstruct</h2>



<p class="wp-block-paragraph">A positional pattern calls <code>Deconstruct</code> and matches each value in order. Records generate it; a class or struct can write its own. Tuples work too, which lets one switch test two inputs at once:</p>



<pre class="wp-block-code"><code>using System.Globalization;
CultureInfo.CurrentCulture = new(&quot;en-US&quot;);

var dock = new Address(&quot;Denver&quot;, &quot;CO&quot;, &quot;80202&quot;);
Console.WriteLine($&quot;Ships from Denver: {dock is (&quot;Denver&quot;, _, _)}&quot;);

Console.WriteLine(Tax(&quot;OR&quot;, 200m));
Console.WriteLine(Tax(&quot;WA&quot;, 200m));
Console.WriteLine(Tax(&quot;IL&quot;, 0m));

var box = new Package(14, 10, 6);
Console.WriteLine(box switch
{
    (&gt; 24, _, _) or (_, &gt; 24, _) =&gt; &quot;Oversize&quot;,
    (_, _, &lt;= 6) =&gt; &quot;Fits the flat-rate box&quot;,
    _ =&gt; &quot;Standard box&quot;
});

static string Tax(string state, decimal subtotal) =&gt; (state, subtotal) switch
{
    (_, &lt;= 0m) =&gt; &quot;Nothing to tax&quot;,
    (&quot;AK&quot; or &quot;DE&quot; or &quot;MT&quot; or &quot;NH&quot; or &quot;OR&quot;, _) =&gt; $&quot;{state}: no state sales tax&quot;,
    (&quot;WA&quot;, _) =&gt; $&quot;WA 6.5%: {subtotal * 0.065m:C}&quot;,
    _ =&gt; $&quot;{state}: look up the rate&quot;
};

record Address(string City, string State, string Zip);
class Package(int length, int width, int height)
{
    public void Deconstruct(out int l, out int w, out int h) =&gt; (l, w, h) = (length, width, height);
}</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>Ships from Denver: True
OR: no state sales tax
WA 6.5%: $13.00
Nothing to tax
Fits the flat-rate box</code></pre>



<p class="wp-block-paragraph">The tuple switch is my favorite trick for tax and pricing tables. The five states with no state sales tax sit in one <code>or</code> arm, and Washington&#8217;s 6.5% on $200 came out to $13.00.</p>



<p class="wp-block-paragraph">The <code>Package</code> class has no properties at all, just a <code>Deconstruct</code> method, and <code>(_, _, &lt;= 6)</code> still read its height. The pattern&#8217;s order must match the <code>out</code> parameters. Tuples themselves are covered in <a href="https://aspdotnethelp.com/tuple-in-csharp/">tuples in C#</a>.</p>



<h2 class="wp-block-heading">var Pattern, Discards and when Guards in a Switch Statement</h2>



<p class="wp-block-paragraph">Patterns also work after <code>case</code>. A <code>when</code> guard adds any Boolean condition, including ones that use variables, and <code>var (name, total, items)</code> deconstructs the tuple into locals for it:</p>



<pre class="wp-block-code"><code>using System.Globalization;
CultureInfo.CurrentCulture = new(&quot;en-US&quot;);

var carts = new (string Customer, decimal Total, int Items)[]
{
    (&quot;Jenna Morales&quot;, 250m, 6), (&quot;Tyler Brooks&quot;, 40m, 1), (&quot;Ava Chen&quot;, 95m, 12)
};
foreach (var cart in carts)
{
    switch (cart)
    {
        case { Total: &gt;= 200m }:
            Console.WriteLine($&quot;{cart.Customer}: free gift&quot;); break;
        case var (name, total, items) when total / items &lt; 10m:
            Console.WriteLine($&quot;{name}: bulk deal, {total / items:C} per item&quot;); break;
        default:
            Console.WriteLine($&quot;{cart.Customer}: no promo&quot;); break;
    }
}</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>Jenna Morales: free gift
Tyler Brooks: no promo
Ava Chen: bulk deal, $7.92 per item</code></pre>



<p class="wp-block-paragraph">Ava Chen&#8217;s cart is $95 for 12 items, so the guard computed $7.92 per item and gave her the bulk deal. Tyler Brooks matched neither case and hit <code>default</code>.</p>



<p class="wp-block-paragraph">The discard <code>_</code> matches anything, including null, but only in a switch expression. In <code>is</code> or a switch statement you write <code>var _</code> or <code>default</code> instead.</p>



<h2 class="wp-block-heading">List Patterns and Slice Patterns (C# 11+)</h2>



<p class="wp-block-paragraph">List patterns match arrays, lists and anything with a <code>Length</code> or <code>Count</code> plus an indexer. Same store, now reading package tracking histories:</p>



<pre class="wp-block-code"><code>string[][] histories =
[
    [],
    [&quot;Created&quot;],
    [&quot;Created&quot;, &quot;Shipped&quot;, &quot;Delivered&quot;],
    [&quot;Created&quot;, &quot;Shipped&quot;, &quot;Returned&quot;, &quot;Refunded&quot;],
    [&quot;Created&quot;, &quot;Shipped&quot;, &quot;In transit&quot;, &quot;In transit&quot;, &quot;Delivered&quot;],
];
foreach (var h in histories)
    Console.WriteLine(Track(h));

static string Track(string[] events) =&gt; events switch
{
    [] =&gt; &quot;No events yet&quot;,
    [var only] =&gt; $&quot;Only: {only}&quot;,
    [&quot;Created&quot;, .. var middle, &quot;Delivered&quot;] =&gt; $&quot;Delivered, {middle.Length} scan(s) between&quot;,
    [.., &quot;Refunded&quot;] =&gt; &quot;Refunded&quot;,
    [_, _, ..] =&gt; &quot;Still moving&quot;
};</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>No events yet
Only: Created
Delivered, 1 scan(s) between
Refunded
Delivered, 3 scan(s) between</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-list-patterns-slice-pattern-example.png" alt="VS Code running ListPatterns.cs: list patterns label an empty history, a single Created event, two delivered orders with 1 and 3 scans between, and a refund" class="wp-image-2794" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-list-patterns-slice-pattern-example.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-list-patterns-slice-pattern-example-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-list-patterns-slice-pattern-example-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-list-patterns-slice-pattern-example-768x674.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">List patterns with a slice: first and last events matter, the middle doesn&#8217;t</figcaption></figure>
</div>


<p class="wp-block-paragraph"><code>..</code> is the slice pattern. It matches zero or more elements, you get one per list pattern, and <code>.. var middle</code> captured the scans between Created and Delivered: 1 for the short trip, 3 for the long one.</p>



<p class="wp-block-paragraph"><code>[]</code> matches an empty array and <code>[var only]</code> exactly one element. The same syntax works on a <a href="https://aspdotnethelp.com/list-in-csharp/">List&lt;T&gt; in C#</a>, and on strings, since a string has Length and an indexer.</p>



<h2 class="wp-block-heading">Why Does a Switch Expression Warn CS8509 (and Error CS8510)?</h2>



<p class="wp-block-paragraph">A switch expression must return something for every input. Leave a value out and you get a compiler warning, plus an exception at run time. Here&#8217;s an order-status label with Canceled missing:</p>



<pre class="wp-block-code"><code>using System.Runtime.CompilerServices;

foreach (var status in new[] { &quot;Paid&quot;, &quot;Shipped&quot;, &quot;Canceled&quot; })
{
    try { Console.WriteLine(Label(status)); }
    catch (SwitchExpressionException ex)
    {
        Console.WriteLine($&quot;{ex.GetType().Name}: {ex.UnmatchedValue}&quot;);
    }
}

static string Label(string status) =&gt; status switch
{
    &quot;Paid&quot; =&gt; &quot;Ready to pack&quot;,
    &quot;Shipped&quot; =&gt; &quot;On the truck&quot;,
};</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>C:\DotNetDemo\PatternMatching\Exhaustive.cs(12,46): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern &#x27;&quot;&quot;&#x27; is not covered.
Ready to pack
On the truck
SwitchExpressionException: Canceled</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-switch-expression-cs8509-not-exhaustive.png" alt="VS Code terminal showing warning CS8509 for Exhaustive.cs, then Ready to pack, On the truck and SwitchExpressionException: Canceled" class="wp-image-2798" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-switch-expression-cs8509-not-exhaustive.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-switch-expression-cs8509-not-exhaustive-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-switch-expression-cs8509-not-exhaustive-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-switch-expression-cs8509-not-exhaustive-768x674.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">CS8509 at build time, SwitchExpressionException at run time</figcaption></figure>
</div>


<p class="wp-block-paragraph">The build printed <code>CS8509</code> and the program still ran. Canceled threw <code>SwitchExpressionException</code>, whose <code>UnmatchedValue</code> told me exactly what slipped through.</p>



<p class="wp-block-paragraph">I&#8217;d treat that warning as an error. Catching the exception is only for the demo; <a href="https://aspdotnethelp.com/try-catch-in-csharp/">try-catch in C#</a> explains when catching is right.</p>



<p class="wp-block-paragraph">With an enum, handling every named member still leaves a gap: I got <code>CS8524</code>, because <code>(Status)2</code> is a legal value. Add a discard arm that throws. The <a href="https://aspdotnethelp.com/enum-in-csharp/">enum in C#</a> post shows why casts create those values.</p>



<p class="wp-block-paragraph">The opposite problem is an arm that can never run. Putting <code>&lt; 20</code> before <code>&lt; 1</code> gave error <code>CS8510</code>: &#8220;The pattern is unreachable.&#8221; Order arms from the most specific to the most general.</p>



<h2 class="wp-block-heading">Common Pattern Matching Mistakes and How to Fix Them</h2>



<p class="wp-block-paragraph">The worst one is precedence: <code>not</code> binds tighter than <code>and</code>, which binds tighter than <code>or</code>. Here&#8217;s a quantity check that looks right:</p>



<pre class="wp-block-code"><code>Console.WriteLine($&quot;Qty 7 outside 1-5 (bug): {OutsideBug(7)}&quot;);
Console.WriteLine($&quot;Qty 7 outside 1-5 (fix): {Outside(7)}&quot;);

object count = 3;
Console.WriteLine($&quot;boxed int is long: {count is long}&quot;);
Console.WriteLine($&quot;boxed int is int:  {count is int}&quot;);

static bool OutsideBug(int qty) =&gt; qty is not &gt;= 1 and &lt;= 5;
static bool Outside(int qty) =&gt; qty is not (&gt;= 1 and &lt;= 5);</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>C:\DotNetDemo\PatternMatching\Mistakes.cs(8,56): warning CS9336: The pattern is redundant.
Qty 7 outside 1-5 (bug): False
Qty 7 outside 1-5 (fix): True
boxed int is long: False
boxed int is int:  True</code></pre>



<p class="wp-block-paragraph"><code>qty is not &gt;= 1 and &lt;= 5</code> means <code>(not &gt;= 1) and &lt;= 5</code>, so 7 came back False. Parentheses fixed it. On .NET 10 the compiler flagged the bug with <code>CS9336</code>: &#8220;The pattern is redundant.&#8221;</p>



<ul class="wp-block-list">
<li><strong>Variables in patterns:</strong> <code>qty is limit</code> fails with CS9135 (&#8220;A constant value of type &#8216;int&#8217; is expected&#8221;). Use <code>qty == limit</code> or a <code>when</code> guard.</li>

<li><strong>Boxed numbers:</strong> a boxed int isn&#8217;t a long, so <code>count is long</code> printed False. Match the exact type, then convert.</li>

<li><strong>Arm order:</strong> the first match wins, so put <code>Total: &gt;= 100m</code> after the surcharge arm if Alaska should always pay.</li>

<li><strong>Null in property patterns:</strong> <code>{ }</code> never matches null, so a null order falls to <code>_</code>. Add a <code>null =&gt; throw</code> arm if null means a bug.</li>
</ul>



<h2 class="wp-block-heading">Which C# Pattern Should You Use?</h2>



<figure class="wp-block-table"><table><thead><tr><th>You want to &#8230;</th><th>Use</th></tr></thead><tbody><tr><td>Check a type and get a typed variable</td><td><code>x is Order o</code></td></tr><tr><td>Test for null</td><td><code>is null</code> / <code>is not null</code></td></tr><tr><td>Map values or ranges to results</td><td>switch expression + relational/logical patterns</td></tr><tr><td>Rules on an object&#8217;s fields</td><td>property pattern <code>{ Total: &gt;= 100m }</code></td></tr><tr><td>Rules on two inputs at once</td><td>tuple + positional pattern</td></tr><tr><td>Compare with a variable</td><td><code>when</code> guard</td></tr><tr><td>Check the first or last items</td><td>list pattern with <code>..</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">If you only learn one, learn the switch expression with property patterns. It replaces most if-else chains I see in code reviews, and C# keeps adding to it: <a href="https://aspdotnethelp.com/whats-new-in-csharp-14/">what&#8217;s new in C# 14</a> lists the latest changes.</p>



<p class="wp-block-paragraph">For the next step, these fit well with patterns:</p>



<ul class="wp-block-list">
<li><a href="https://aspdotnethelp.com/linq-in-csharp/">Filter orders with LINQ and pattern-based lambdas</a></li>

<li><a href="https://aspdotnethelp.com/interface-in-csharp/">Interfaces you can match with type patterns</a></li>

<li><a href="https://aspdotnethelp.com/inheritance-in-csharp/">Class hierarchies behind type switches</a></li>

<li><a href="https://aspdotnethelp.com/csharp-tutorials/">Every C# tutorial on aspdotnethelp.com</a></li>
</ul>



<h2 class="wp-block-heading">Frequently Asked Questions</h2>


<div id="rank-math-faq" class="rank-math-block">
<div class="rank-math-list ">
<div id="faq-question-pm0000001" class="rank-math-list-item">
<h3 class="rank-math-question ">What is pattern matching in C#?</h3>
<div class="rank-math-answer ">

<p>It&#8217;s a way to test a value against a shape (a type, a constant, a range, property values or list elements) with is, switch statements or switch expressions, and pull matched parts into variables.</p>

</div>
</div>
<div id="faq-question-pm0000002" class="rank-math-list-item">
<h3 class="rank-math-question ">Is a switch expression faster than if-else?</h3>
<div class="rank-math-answer ">

<p>Usually about the same. The compiler can arrange the checks so it doesn&#8217;t repeat tests, but the main win is readability and the exhaustiveness warning, not speed.</p>

</div>
</div>
<div id="faq-question-pm0000003" class="rank-math-list-item">
<h3 class="rank-math-question ">What&#8217;s the difference between is null and == null?</h3>
<div class="rank-math-answer ">

<p>x is null never calls an overloaded == operator, so it always checks for a real null. In my test, an overloaded == said a non-null SKU equaled null; is null returned False.</p>

</div>
</div>
<div id="faq-question-pm0000004" class="rank-math-list-item">
<h3 class="rank-math-question ">How do I fix warning CS8509?</h3>
<div class="rank-math-answer ">

<p>Handle every possible input: add the missing values or a discard arm (_) that returns a default or throws. For enums, a discard is needed even when every named member is covered.</p>

</div>
</div>
<div id="faq-question-pm0000005" class="rank-math-list-item">
<h3 class="rank-math-question ">Which C# version added list patterns?</h3>
<div class="rank-math-answer ">

<p>C# 11 (.NET 7). Relational and logical patterns came in C# 9, extended property patterns in C# 10. The <a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/patterns" target="_blank" rel="noopener">patterns reference on Microsoft Learn</a> lists every kind.</p>

</div>
</div>
</div>
</div>]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Transient vs Scoped vs Singleton in ASP.NET Core: DI Lifetimes Explained (.NET 10)</title>
		<link>https://aspdotnethelp.com/dependency-injection-lifetimes-aspnet-core/</link>
		
		<dc:creator><![CDATA[Bijay Kumar]]></dc:creator>
		<pubDate>Mon, 09 Nov 2026 15:15:00 +0000</pubDate>
				<category><![CDATA[ASP.NET Core]]></category>
		<guid isPermaLink="false">https://aspdotnethelp.com/?p=2738</guid>

					<description><![CDATA[Use AddScoped for per-request state like a DbContext, AddTransient for small stateless helpers, and AddSingleton only for thread-safe objects the whole app can share. That&#8217;s my rule for transient vs scoped vs singleton in ASP.NET Core. The only real difference is how long the container keeps an instance: per injection, per request, or for the ... <a title="Transient vs Scoped vs Singleton in ASP.NET Core: DI Lifetimes Explained (.NET 10)" class="read-more" href="https://aspdotnethelp.com/dependency-injection-lifetimes-aspnet-core/" aria-label="Read more about Transient vs Scoped vs Singleton in ASP.NET Core: DI Lifetimes Explained (.NET 10)">Read more &#62;&#62;</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Use <code>AddScoped</code> for per-request state like a <code>DbContext</code>, <code>AddTransient</code> for small stateless helpers, and <code>AddSingleton</code> only for thread-safe objects the whole app can share. That&#8217;s my rule for <strong>transient vs scoped vs singleton</strong> in ASP.NET Core.</p>



<p class="wp-block-paragraph">The only real difference is how long the container keeps an instance: per injection, per request, or for the life of the app.</p>



<p class="wp-block-paragraph">On a .NET 10 minimal API, three requests kept the singleton at instance #1, gave each request its own scoped instance, and created a fresh transient on every injection.</p>



<h2 class="wp-block-heading">Transient vs Scoped vs Singleton at a Glance</h2>



<p class="wp-block-paragraph">The instance numbers in this table come from the real run further down:</p>



<figure class="wp-block-table"><table><thead><tr><th>Feature</th><th>Transient</th><th>Scoped</th><th>Singleton</th></tr></thead><tbody><tr><td>Register with</td><td><code>AddTransient</code></td><td><code>AddScoped</code></td><td><code>AddSingleton</code></td></tr><tr><td>New instance</td><td>Every time it&#8217;s resolved</td><td>Once per request (scope)</td><td>Once per app</td></tr><tr><td>Asked for twice in one request</td><td>Two instances (#3, #4)</td><td>Same one (#1, #1)</td><td>Same one</td></tr><tr><td>Next request</td><td>New instances</td><td>New instance (#2)</td><td>Same one (#1)</td></tr><tr><td>Disposed</td><td>End of the scope it came from</td><td>End of the request</td><td>App shutdown</td></tr><tr><td>Needs thread-safe code</td><td>No, unless a singleton holds it</td><td>Rarely</td><td>Yes, always</td></tr><tr><td>Inject into a singleton?</td><td>Works, but lives as long as the singleton</td><td>No: captive dependency</td><td>Yes</td></tr><tr><td>Typical use</td><td>Sales-tax calculator, mappers</td><td><code>DbContext</code>, order pricing</td><td>Counters, caches, config</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">None of this changed in the <a href="https://aspdotnethelp.com/whats-new-in-dotnet-10/">.NET 10 release</a>. Keyed registrations, covered below, arrived in .NET 8.</p>



<h2 class="wp-block-heading">The Sample: An Order-Pricing API with a Sales-Tax Calculator</h2>



<p class="wp-block-paragraph">Every test runs on one project, PricingApi (SDK 10.0.402). It prices a US order with state sales tax using a singleton <code>QuoteCounter</code>, a scoped <code>OrderPricingService</code> and a transient <code>SalesTaxCalculator</code>.</p>



<p class="wp-block-paragraph">Each class inherits <code>Tracked</code>, which numbers new instances per type (#1, #2, #3), so you can spot a repeat instantly. Here&#8217;s <code>Services.cs</code>:</p>



<pre class="wp-block-code"><code>using System.Collections.Concurrent;

namespace PricingApi;

// Every new instance gets the next number for its type: #1, #2, #3 ...
public abstract class Tracked : IDisposable
{
    static readonly ConcurrentDictionary&lt;Type, int&gt; created = new();
    public int Id { get; }
    protected Tracked() =&gt; Id = created.AddOrUpdate(GetType(), 1, (_, n) =&gt; n + 1);
    public void Dispose() =&gt; Console.WriteLine($&quot;disposed {GetType().Name} #{Id}&quot;);
}

// Singleton: one per app, so it must be thread-safe
public class QuoteCounter : Tracked
{
    int count;
    public int Next() =&gt; Interlocked.Increment(ref count);
}

// Transient: small, stateless, cheap to create
public class SalesTaxCalculator : Tracked
{
    public decimal Tax(decimal subtotal, decimal rate) =&gt;
        Math.Round(subtotal * rate, 2, MidpointRounding.AwayFromZero);
}

// Scoped: one per HTTP request
public class OrderPricingService(QuoteCounter counter, SalesTaxCalculator calculator) : Tracked
{
    public PriceQuote Quote(decimal subtotal, IStateSalesTax state)
    {
        var tax = calculator.Tax(subtotal, state.Rate);
        return new(counter.Next(), state.Code, subtotal, tax, subtotal + tax);
    }
}

public record PriceQuote(int QuoteNumber, string State, decimal Subtotal, decimal Tax, decimal Total);

// Keyed services: one implementation per state
public interface IStateSalesTax
{
    string Code { get; }
    decimal Rate { get; }
}

public class TexasSalesTax : IStateSalesTax
{
    public string Code =&gt; &quot;TX&quot;;
    public decimal Rate =&gt; 0.0625m;
}

public class CaliforniaSalesTax : IStateSalesTax
{
    public string Code =&gt; &quot;CA&quot;;
    public decimal Rate =&gt; 0.0725m;
}</code></pre>



<p class="wp-block-paragraph">The dependencies arrive through a primary <a href="https://aspdotnethelp.com/constructor-in-csharp/">constructor in C#</a>, and <code>PriceQuote</code> is a positional record (my <a href="https://aspdotnethelp.com/csharp-record-vs-class-vs-struct/">record vs class vs struct comparison</a> explains that choice). <code>Program.cs</code> registers everything and maps the endpoints:</p>



<pre class="wp-block-code"><code>using PricingApi;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton&lt;QuoteCounter&gt;();
builder.Services.AddScoped&lt;OrderPricingService&gt;();
builder.Services.AddTransient&lt;SalesTaxCalculator&gt;();
builder.Services.AddKeyedScoped&lt;IStateSalesTax, TexasSalesTax&gt;(&quot;TX&quot;);
builder.Services.AddKeyedScoped&lt;IStateSalesTax, CaliforniaSalesTax&gt;(&quot;CA&quot;);

var app = builder.Build();

// Middleware resolves the same services from the request scope
app.Use(async (context, next) =&gt;
{
    var sp = context.RequestServices;
    context.Items[&quot;mw&quot;] = Ids(sp.GetRequiredService&lt;QuoteCounter&gt;(),
        sp.GetRequiredService&lt;OrderPricingService&gt;(), sp.GetRequiredService&lt;SalesTaxCalculator&gt;());
    await next(context);
});

// The endpoint asks for the scoped and transient services twice
app.MapGet(&quot;/lifetimes&quot;, (HttpContext context, QuoteCounter singleton,
    OrderPricingService scoped1, OrderPricingService scoped2,
    SalesTaxCalculator transient1, SalesTaxCalculator transient2) =&gt;
    $&quot;middleware  {context.Items[&quot;mw&quot;]}\n&quot; +
    $&quot;endpoint    singleton #{singleton.Id}   scoped #{scoped1.Id} #{scoped2.Id}   &quot; +
    $&quot;transient #{transient1.Id} #{transient2.Id}&quot;);

app.MapGet(&quot;/quote/tx&quot;, (decimal subtotal, [FromKeyedServices(&quot;TX&quot;)] IStateSalesTax tax,
    OrderPricingService pricing) =&gt; pricing.Quote(subtotal, tax));

app.MapGet(&quot;/quote/{state}&quot;, (string state, decimal subtotal, HttpContext context,
    OrderPricingService pricing) =&gt;
    context.RequestServices.GetKeyedService&lt;IStateSalesTax&gt;(state.ToUpperInvariant()) is { } tax
        ? Results.Ok(pricing.Quote(subtotal, tax))
        : Results.NotFound($&quot;No sales tax service registered for {state}&quot;));

app.Run();

static string Ids(QuoteCounter s, OrderPricingService sc, SalesTaxCalculator t) =&gt;
    $&quot;singleton #{s.Id}   scoped #{sc.Id}      transient #{t.Id}&quot;;</code></pre>



<p class="wp-block-paragraph">The project needs no NuGet packages, since the container ships with ASP.NET Core. The launch profile pins port 5738 and Development:</p>



<pre class="wp-block-code"><code>&lt;Project Sdk=&quot;Microsoft.NET.Sdk.Web&quot;&gt;

  &lt;PropertyGroup&gt;
    &lt;TargetFramework&gt;net10.0&lt;/TargetFramework&gt;
    &lt;Nullable&gt;enable&lt;/Nullable&gt;
    &lt;ImplicitUsings&gt;enable&lt;/ImplicitUsings&gt;
  &lt;/PropertyGroup&gt;

&lt;/Project&gt;</code></pre>



<pre class="wp-block-code"><code>{
  &quot;$schema&quot;: &quot;https://json.schemastore.org/launchsettings.json&quot;,
  &quot;profiles&quot;: {
    &quot;http&quot;: {
      &quot;commandName&quot;: &quot;Project&quot;,
      &quot;dotnetRunMessages&quot;: true,
      &quot;launchBrowser&quot;: false,
      &quot;applicationUrl&quot;: &quot;http://localhost:5738&quot;,
      &quot;environmentVariables&quot;: {
        &quot;ASPNETCORE_ENVIRONMENT&quot;: &quot;Development&quot;
      }
    }
  }
}</code></pre>



<h2 class="wp-block-heading">AddScoped vs AddTransient: How Many Instances in One Request?</h2>



<p class="wp-block-paragraph">With the API running, this <a href="https://aspdotnethelp.com/dotnet-run-file-based-apps/">file-based app</a> calls <code>/lifetimes</code> three times and prints what the middleware and the endpoint received:</p>



<pre class="wp-block-code"><code>var api = new HttpClient { BaseAddress = new Uri(&quot;http://localhost:5738/&quot;) };

for (int i = 1; i &lt;= 3; i++)
{
    Console.WriteLine($&quot;Request {i}&quot;);
    Console.WriteLine(await api.GetStringAsync(&quot;lifetimes&quot;));
}</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>Request 1
middleware  singleton #1   scoped #1      transient #2
endpoint    singleton #1   scoped #1 #1   transient #3 #4
Request 2
middleware  singleton #1   scoped #2      transient #6
endpoint    singleton #1   scoped #2 #2   transient #7 #8
Request 3
middleware  singleton #1   scoped #3      transient #10
endpoint    singleton #1   scoped #3 #3   transient #11 #12</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/di-lifetimes-transient-vs-scoped-vs-singleton-instance-ids-three-requests.png" alt="Transient vs scoped vs singleton in ASP.NET Core: CallApi.cs in VS Code calls the .NET 10 API three times; the singleton stays #1, the scoped id repeats inside each request and the transient id changes on every injection" class="wp-image-2764" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/di-lifetimes-transient-vs-scoped-vs-singleton-instance-ids-three-requests.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/di-lifetimes-transient-vs-scoped-vs-singleton-instance-ids-three-requests-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/di-lifetimes-transient-vs-scoped-vs-singleton-instance-ids-three-requests-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/di-lifetimes-transient-vs-scoped-vs-singleton-instance-ids-three-requests-768x674.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">Three real requests: watch which instance numbers repeat</figcaption></figure>
</div>


<p class="wp-block-paragraph">In request 1, the endpoint asked for <code>OrderPricingService</code> twice and got #1 #1, the same object the middleware saw. It asked for <code>SalesTaxCalculator</code> twice and got #3 #4.</p>



<p class="wp-block-paragraph">Transient #1 went into the <code>OrderPricingService</code> constructor, so each request built four calculators but one pricing service. That&#8217;s the <strong>addscoped vs addtransient</strong> difference: scoped shares within a request, transient never shares.</p>



<h2 class="wp-block-heading">Singleton vs Scoped: What Changes Between Requests?</h2>



<p class="wp-block-paragraph">Same PricingApi run, read across the requests: the singleton stayed #1 all three times, while the scoped service went #1, #2, #3. ASP.NET Core creates a scope per request and disposes it after the response.</p>



<p class="wp-block-paragraph">Middleware is where you can trip. Resolve scoped services in <code>InvokeAsync</code> or from <code>RequestServices</code>, as mine does, never in a middleware constructor, which runs once and turns them into singletons.</p>



<p class="wp-block-paragraph">Anything about the current user, like the claims in my <a href="https://aspdotnethelp.com/jwt-authentication-aspnet-core/">JWT authentication guide</a>, belongs in scoped services for the same reason.</p>



<h2 class="wp-block-heading">Transient vs Scoped vs Singleton in a Console App with CreateScope</h2>



<p class="wp-block-paragraph">A console app has no requests, so you create the scopes with <code>CreateScope</code>. This generic host (Microsoft.Extensions.Hosting 10.0.14) does what ASP.NET Core does per request:</p>



<pre class="wp-block-code"><code>#:package Microsoft.Extensions.Hosting@10.0.14
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddSingleton&lt;QuoteCounter&gt;();
builder.Services.AddScoped&lt;OrderPricing&gt;();
builder.Services.AddTransient&lt;TaxCalculator&gt;();
using var host = builder.Build();

for (int i = 1; i &lt;= 2; i++)
{
    using var scope = host.Services.CreateScope();
    Console.WriteLine($&quot;Scope {i}&quot;);
    Show&lt;QuoteCounter&gt;(scope.ServiceProvider);
    Show&lt;OrderPricing&gt;(scope.ServiceProvider);
    Show&lt;TaxCalculator&gt;(scope.ServiceProvider);
}

// Resolve T twice from the same provider and print both ids
static void Show&lt;T&gt;(IServiceProvider sp) where T : Tracked
{
    var a = sp.GetRequiredService&lt;T&gt;();
    var b = sp.GetRequiredService&lt;T&gt;();
    Console.WriteLine($&quot;  {typeof(T).Name}: #{a.Id} #{b.Id}&quot;);
}

abstract class Tracked
{
    static readonly Dictionary&lt;Type, int&gt; made = [];
    public int Id { get; }
    protected Tracked() =&gt;
        Id = made[GetType()] = made.GetValueOrDefault(GetType()) + 1;
}

class QuoteCounter : Tracked;
class OrderPricing : Tracked;
class TaxCalculator : Tracked;</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>Scope 1
  QuoteCounter: #1 #1
  OrderPricing: #1 #1
  TaxCalculator: #1 #2
Scope 2
  QuoteCounter: #1 #1
  OrderPricing: #2 #2
  TaxCalculator: #3 #4</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/dependency-injection-console-app-generic-host-createscope-instance-ids.png" alt="Add dependency injection to a console app: HostDemo.cs uses Host.CreateApplicationBuilder and CreateScope; the singleton is #1 in both scopes, the scoped id is #1 then #2 and each transient is new" class="wp-image-2762" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/dependency-injection-console-app-generic-host-createscope-instance-ids.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/dependency-injection-console-app-generic-host-createscope-instance-ids-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/dependency-injection-console-app-generic-host-createscope-instance-ids-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/dependency-injection-console-app-generic-host-createscope-instance-ids-768x674.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">The same three lifetimes in a .NET 10 console app with the generic host</figcaption></figure>
</div>


<p class="wp-block-paragraph">Same pattern as the API: QuoteCounter is #1 in both scopes, OrderPricing repeats inside a scope (#1 #1, then #2 #2), and TaxCalculator is always new.</p>



<p class="wp-block-paragraph">That&#8217;s all it takes to <strong>add dependency injection to a console app</strong>. Starting fresh? Set up <a href="https://aspdotnethelp.com/create-a-console-application-in-visual-studio-code/">a console app in VS Code</a> first.</p>



<h2 class="wp-block-heading">When Are Transient, Scoped and Singleton Services Disposed?</h2>



<p class="wp-block-paragraph">Every sample class implements <code>IDisposable</code>, so the server log shows the cleanup at the end of request 1:</p>



<pre class="wp-block-code"><code>disposed SalesTaxCalculator #4
disposed SalesTaxCalculator #3
disposed SalesTaxCalculator #2
disposed OrderPricingService #1
disposed SalesTaxCalculator #1</code></pre>



<p class="wp-block-paragraph">The four transients and the scoped service were disposed when the request&#8217;s scope ended, newest first. The singleton lives until shutdown. You never call <code>Dispose</code> on something the container created.</p>



<p class="wp-block-paragraph">The risky case is a disposable transient resolved from the root provider instead of a scope. This test resolves one inside a scope and three from the root:</p>



<pre class="wp-block-code"><code>#:package Microsoft.Extensions.DependencyInjection@10.0.14
using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection().AddTransient&lt;TaxCalculator&gt;();
using (var root = services.BuildServiceProvider())
{
    using (var scope = root.CreateScope())
    {
        scope.ServiceProvider.GetRequiredService&lt;TaxCalculator&gt;();
        Console.WriteLine(&quot;Leaving the scope&quot;);
    }
    for (int i = 0; i &lt; 3; i++) root.GetRequiredService&lt;TaxCalculator&gt;();
    Console.WriteLine(&quot;3 transients resolved from the root provider&quot;);
}
Console.WriteLine(&quot;Root provider disposed&quot;);

class TaxCalculator : IDisposable
{
    static int made;
    public int Id { get; } = ++made;
    public void Dispose() =&gt; Console.WriteLine($&quot;  disposed TaxCalculator #{Id}&quot;);
}</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>Leaving the scope
  disposed TaxCalculator #1
3 transients resolved from the root provider
  disposed TaxCalculator #4
  disposed TaxCalculator #3
  disposed TaxCalculator #2
Root provider disposed</code></pre>



<p class="wp-block-paragraph"><strong>Important:</strong> #1 was disposed when its scope closed, but #2 to #4 lived until the root provider was disposed. Do that in a loop and you have a memory leak. My notes on <a href="https://aspdotnethelp.com/dispose-and-finalize-in-csharp/">Dispose and Finalize in C#</a> cover the pattern itself.</p>



<h2 class="wp-block-heading">Captive Dependency: A Scoped Service Inside a Singleton</h2>



<p class="wp-block-paragraph">A captive dependency is a longer-lived service holding a shorter-lived one. Here a singleton <code>PriceCache</code> takes a scoped <code>ShoppingCart</code>, and the host is built twice:</p>



<pre class="wp-block-code"><code>#:package Microsoft.Extensions.Hosting@10.0.14
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

foreach (var env in new[] { &quot;Production&quot;, &quot;Development&quot; })
{
    var settings = new HostApplicationBuilderSettings();
    settings.EnvironmentName = env;
    var builder = Host.CreateApplicationBuilder(settings);
    builder.Services.AddSingleton&lt;PriceCache&gt;();  // singleton
    builder.Services.AddScoped&lt;ShoppingCart&gt;();   // scoped
    try
    {
        using var host = builder.Build();
        int first = Cart(host), second = Cart(host);
        Console.WriteLine($&quot;{env}: cart #{first} then #{second}&quot;);
    }
    catch (AggregateException ex)
    {
        var inner = ex.InnerException!;
        Console.WriteLine($&quot;{env}: {inner.GetType().Name}&quot;);
        Console.WriteLine(inner.Message);
    }
}

// One &quot;request&quot;: a new scope that asks for the singleton
static int Cart(IHost host)
{
    using var scope = host.Services.CreateScope();
    var sp = scope.ServiceProvider;
    return sp.GetRequiredService&lt;PriceCache&gt;().Cart.Id;
}

class ShoppingCart
{
    static int made;
    public int Id { get; } = ++made;
}

class PriceCache(ShoppingCart cart)
{
    public ShoppingCart Cart =&gt; cart;
}</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>Production: cart #1 then #1
Development: InvalidOperationException
Error while validating the service descriptor &#x27;ServiceType: PriceCache Lifetime: Singleton ImplementationType: PriceCache&#x27;: Cannot consume scoped service &#x27;ShoppingCart&#x27; from singleton &#x27;PriceCache&#x27;.</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/captive-dependency-scoped-in-singleton-validateonbuild-invalidoperationexception.png" alt="Captive dependency in .NET 10: Captive.cs keeps the same scoped cart #1 in Production, and in Development ValidateOnBuild throws InvalidOperationException: Cannot consume scoped service &#x27;ShoppingCart&#x27; from singleton &#x27;PriceCache&#x27;" class="wp-image-2761" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/captive-dependency-scoped-in-singleton-validateonbuild-invalidoperationexception.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/captive-dependency-scoped-in-singleton-validateonbuild-invalidoperationexception-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/captive-dependency-scoped-in-singleton-validateonbuild-invalidoperationexception-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/captive-dependency-scoped-in-singleton-validateonbuild-invalidoperationexception-768x674.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">Production hides the bug; Development throws at Build()</figcaption></figure>
</div>


<p class="wp-block-paragraph">Production &#8220;works,&#8221; and that&#8217;s the bug: both requests got cart #1, so one customer&#8217;s cart leaks into the next request. Development threw an <code>InvalidOperationException</code> (inside an <code>AggregateException</code>) at <code>Build()</code>.</p>



<p class="wp-block-paragraph">That&#8217;s scope validation: <code>ValidateScopes</code> and <code>ValidateOnBuild</code> are on by default in Development. Turn them on everywhere with <code>builder.Host.UseDefaultServiceProvider(o =&gt; { o.ValidateScopes = true; o.ValidateOnBuild = true; })</code>.</p>



<h2 class="wp-block-heading">How Do You Use a Scoped Service from a Singleton or BackgroundService?</h2>



<p class="wp-block-paragraph">A <code>BackgroundService</code> is a singleton, so it can&#8217;t take a scoped service in its constructor. Inject <code>IServiceScopeFactory</code> and open a scope per unit of work:</p>



<pre class="wp-block-code"><code>#:package Microsoft.Extensions.Hosting@10.0.14
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

var builder = Host.CreateApplicationBuilder(args);
builder.Logging.ClearProviders();
builder.Services.AddScoped&lt;OrderPricing&gt;();
builder.Services.AddHostedService&lt;NightlyRepricer&gt;();
await builder.Build().RunAsync();

class NightlyRepricer(IServiceScopeFactory scopes,
    IHostApplicationLifetime app) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        for (int batch = 1; batch &lt;= 3; batch++)
        {
            await using var scope = scopes.CreateAsyncScope();
            var sp = scope.ServiceProvider;
            var pricing = sp.GetRequiredService&lt;OrderPricing&gt;();
            Console.WriteLine($&quot;Batch {batch}: OrderPricing #{pricing.Id}&quot;);
        }
        app.StopApplication();
    }
}

class OrderPricing : Tracked;

abstract class Tracked : IDisposable
{
    static readonly Dictionary&lt;Type, int&gt; made = [];
    public int Id { get; }
    protected Tracked() =&gt; Id = made[GetType()] = made.GetValueOrDefault(GetType()) + 1;
    public void Dispose() =&gt; Console.WriteLine($&quot;  disposed {GetType().Name} #{Id}&quot;);
}</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>Batch 1: OrderPricing #1
  disposed OrderPricing #1
Batch 2: OrderPricing #2
  disposed OrderPricing #2
Batch 3: OrderPricing #3
  disposed OrderPricing #3</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/backgroundservice-iservicescopefactory-createasyncscope-scoped-service.png" alt="BackgroundService with IServiceScopeFactory in .NET 10: Worker.cs opens CreateAsyncScope per batch, so each batch gets a new scoped OrderPricing #1, #2, #3 that is disposed when the batch ends" class="wp-image-2760" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/backgroundservice-iservicescopefactory-createasyncscope-scoped-service.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/backgroundservice-iservicescopefactory-createasyncscope-scoped-service-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/backgroundservice-iservicescopefactory-createasyncscope-scoped-service-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/backgroundservice-iservicescopefactory-createasyncscope-scoped-service-768x674.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">One scope per batch: a fresh scoped service each time, disposed right after</figcaption></figure>
</div>


<p class="wp-block-paragraph">Each batch got its own <code>OrderPricing</code>, disposed before the next. <code>CreateAsyncScope</code> with <code>await using</code> also handles <code>IAsyncDisposable</code> services; see <a href="https://aspdotnethelp.com/async-await-in-csharp/">async and await in C#</a> for that syntax.</p>



<h2 class="wp-block-heading">Thread Safety: The Real Cost of a Singleton</h2>



<p class="wp-block-paragraph">A singleton serves every request at once, so its fields are shared. This test hits one <code>QuoteCounter</code> 100,000 times from <code>Parallel.For</code>:</p>



<pre class="wp-block-code"><code>#:package Microsoft.Extensions.DependencyInjection@10.0.14
using Microsoft.Extensions.DependencyInjection;

using var sp = new ServiceCollection().AddSingleton&lt;QuoteCounter&gt;().BuildServiceProvider();

Parallel.For(0, 100_000, _ =&gt; sp.GetRequiredService&lt;QuoteCounter&gt;().Add());

var counter = sp.GetRequiredService&lt;QuoteCounter&gt;();
Console.WriteLine($&quot;Expected 100000 | count++ {counter.Unsafe} | Interlocked {counter.Safe}&quot;);

class QuoteCounter
{
    int unsafeCount, safeCount;
    public int Unsafe =&gt; unsafeCount;
    public int Safe =&gt; safeCount;
    public void Add()
    {
        unsafeCount++;                          // read, add, write: two threads can collide
        Interlocked.Increment(ref safeCount);   // atomic
    }
}</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>Expected 100000 | count++ 55885 | Interlocked 100000</code></pre>



<p class="wp-block-paragraph">Plain <code>count++</code> lost 44,115 increments because threads overwrote each other; <code>Interlocked.Increment</code> hit exactly 100,000. The container creates services safely, but your singleton&#8217;s own state needs <code>Interlocked</code>, a lock or a concurrent collection.</p>



<h2 class="wp-block-heading">DbContext and HttpClient Lifetimes in ASP.NET Core</h2>



<p class="wp-block-paragraph"><code>AddDbContext</code> registers the context as scoped by default, one change tracker per request. Never put it in a singleton; my <a href="https://aspdotnethelp.com/aspnet-core-web-api-crud-ef-core/">ASP.NET Core Web API with EF Core</a> gets it per request.</p>



<p class="wp-block-paragraph">Typed clients from <code>AddHttpClient&lt;T&gt;</code> are transient, and Learn warns against capturing one in a singleton (it stops seeing DNS changes). In a singleton, inject <code>IHttpClientFactory</code> and call <code>CreateClient</code>. More in my <a href="https://aspdotnethelp.com/httpclient-in-csharp/">HttpClient in C# guide</a>.</p>



<h2 class="wp-block-heading">Keyed Services with AddKeyedScoped and [FromKeyedServices]</h2>



<p class="wp-block-paragraph">Keyed services (.NET 8+) register several implementations of one <a href="https://aspdotnethelp.com/interface-in-csharp/">interface in C#</a> under keys. <code>/quote/tx</code> picks Texas with <code>[FromKeyedServices(&quot;TX&quot;)]</code>; <code>/quote/{state}</code> picks at run time with <code>GetKeyedService</code>. Same PricingApi project, $120 subtotal:</p>



<pre class="wp-block-code"><code>curl.exe -s &quot;http://localhost:5738/quote/tx?subtotal=120&quot;
{&quot;quoteNumber&quot;:1,&quot;state&quot;:&quot;TX&quot;,&quot;subtotal&quot;:120,&quot;tax&quot;:7.50,&quot;total&quot;:127.50}

curl.exe -s &quot;http://localhost:5738/quote/ca?subtotal=120&quot;
{&quot;quoteNumber&quot;:2,&quot;state&quot;:&quot;CA&quot;,&quot;subtotal&quot;:120,&quot;tax&quot;:8.70,&quot;total&quot;:128.70}

curl.exe -i &quot;http://localhost:5738/quote/fl?subtotal=120&quot;
HTTP/1.1 404 Not Found
&quot;No sales tax service registered for fl&quot;</code></pre>



<p class="wp-block-paragraph">Texas (6.25%) added $7.50, California (7.25%) added $8.70, and Florida got a 404 because no key matched. Quote numbers 1 and 2 came from the one singleton counter.</p>



<h2 class="wp-block-heading">GetService vs GetRequiredService, and TryAdd vs Add</h2>



<p class="wp-block-paragraph">This test adds Texas and California with Add, tries New York with TryAdd, then resolves a service nobody registered:</p>



<pre class="wp-block-code"><code>#:package Microsoft.Extensions.DependencyInjection@10.0.14
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;

var services = new ServiceCollection();
services.AddSingleton&lt;IStateSalesTax, TexasSalesTax&gt;();
services.AddSingleton&lt;IStateSalesTax, CaliforniaSalesTax&gt;();   // Add: appends a second registration
services.TryAddSingleton&lt;IStateSalesTax, NewYorkSalesTax&gt;();   // TryAdd: skipped, one already exists
using var sp = services.BuildServiceProvider();

Console.WriteLine($&quot;Registrations: {services.Count}&quot;);
Console.WriteLine($&quot;GetRequiredService&lt;IStateSalesTax&gt;: {sp.GetRequiredService&lt;IStateSalesTax&gt;().Code}&quot;);
Console.WriteLine($&quot;GetServices&lt;IStateSalesTax&gt;: {string.Join(&quot;, &quot;, sp.GetServices&lt;IStateSalesTax&gt;().Select(t =&gt; t.Code))}&quot;);
Console.WriteLine($&quot;GetService&lt;IShippingRate&gt;: {sp.GetService&lt;IShippingRate&gt;()?.ToString() ?? &quot;null&quot;}&quot;);
try
{
    sp.GetRequiredService&lt;IShippingRate&gt;();
}
catch (InvalidOperationException ex)
{
    Console.WriteLine($&quot;GetRequiredService&lt;IShippingRate&gt;: {ex.Message}&quot;);
}

interface IShippingRate { }
interface IStateSalesTax { string Code { get; } }
class TexasSalesTax : IStateSalesTax { public string Code =&gt; &quot;TX&quot;; }
class CaliforniaSalesTax : IStateSalesTax { public string Code =&gt; &quot;CA&quot;; }
class NewYorkSalesTax : IStateSalesTax { public string Code =&gt; &quot;NY&quot;; }</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>Registrations: 2
GetRequiredService&lt;IStateSalesTax&gt;: CA
GetServices&lt;IStateSalesTax&gt;: TX, CA
GetService&lt;IShippingRate&gt;: null
GetRequiredService&lt;IShippingRate&gt;: No service for type &#x27;IShippingRate&#x27; has been registered.</code></pre>



<p class="wp-block-paragraph"><code>GetService</code> returned <code>null</code>; <code>GetRequiredService</code> threw. I use <code>GetRequiredService</code> so a missing registration fails loudly. With two Adds, a single resolve returns the last one (CA), and <code>TryAddSingleton</code> skipped New York because the type was already registered.</p>



<h2 class="wp-block-heading">When Should You Use Transient, Scoped or Singleton?</h2>



<p class="wp-block-paragraph">Ask what state the class holds and who may see it:</p>



<figure class="wp-block-table"><table><thead><tr><th>Situation</th><th>Lifetime</th><th>Why</th></tr></thead><tbody><tr><td>Holds data for one request or user (cart, DbContext, current user)</td><td>Scoped</td><td>Shared inside the request, isolated between requests</td></tr><tr><td>Stateless helper (calculator, mapper, validator)</td><td>Transient</td><td>Cheap to build, nothing to share</td></tr><tr><td>Expensive to build or app-wide (cache, counter, config)</td><td>Singleton</td><td>Built once; must be thread-safe</td></tr><tr><td>Singleton or BackgroundService needs a scoped service</td><td>Inject IServiceScopeFactory</td><td>One scope per unit of work</td></tr><tr><td>Several implementations picked by a key</td><td>AddKeyed* with any lifetime</td><td>Lifetime and key are separate choices</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">If you&#8217;re torn on a stateless class, transient and scoped both work. Pick singleton only when there&#8217;s no per-request data and you&#8217;ve made it thread-safe.</p>



<p class="wp-block-paragraph">More ASP.NET Core and C# guides from the site:</p>



<ul class="wp-block-list">
<li><a href="https://aspdotnethelp.com/asp-net-core-tutorials/">All ASP.NET Core tutorials on AspDotnetHelp</a></li>

<li><a href="https://aspdotnethelp.com/upgrade-dotnet-8-to-dotnet-10/">Upgrade a project from .NET 8 to .NET 10</a></li>

<li><a href="https://aspdotnethelp.com/dot-net-core-interview-questions-and-answers/">.NET Core interview questions and answers</a></li>

<li><a href="https://aspdotnethelp.com/learn-csharp/">Learn C# from the ground up</a></li>
</ul>



<p class="wp-block-paragraph">Lifetimes matter most in two places I see go wrong all the time. Factory-based IMiddleware lets you inject scoped services safely, which I cover in <a href="https://aspdotnethelp.com/aspnet-core-middleware/">custom middleware in ASP.NET Core</a>, and a client like BlobServiceClient is a good singleton, as shown in <a href="https://aspdotnethelp.com/azure-blob-storage-csharp/">Azure Blob Storage in C#</a>.</p>



<h2 class="wp-block-heading">Frequently Asked Questions</h2>


<div id="rank-math-faq" class="rank-math-block">
<div class="rank-math-list ">
<div id="faq-question-dil0000001" class="rank-math-list-item">
<h3 class="rank-math-question ">What is the default lifetime in .NET dependency injection?</h3>
<div class="rank-math-answer ">

<p>There isn&#8217;t one; you choose it with <code>AddTransient</code>, <code>AddScoped</code> or <code>AddSingleton</code>. Framework helpers pick for you: <code>AddDbContext</code> registers the context as scoped.</p>

</div>
</div>
<div id="faq-question-dil0000002" class="rank-math-list-item">
<h3 class="rank-math-question ">Is scoped the same as singleton in a console app?</h3>
<div class="rank-math-answer ">

<p>Only if you never create a scope. Scoped services resolved from the root provider live until it&#8217;s disposed, so call <code>CreateScope</code> per unit of work.</p>

</div>
</div>
<div id="faq-question-dil0000003" class="rank-math-list-item">
<h3 class="rank-math-question ">Can a scoped service depend on a singleton?</h3>
<div class="rank-math-answer ">

<p>Yes. A service may depend on anything that lives as long or longer. Only a longer-lived service holding a shorter-lived one is a problem.</p>

</div>
</div>
<div id="faq-question-dil0000004" class="rank-math-list-item">
<h3 class="rank-math-question ">How do I use dependency injection with constructor parameters that aren&#8217;t services?</h3>
<div class="rank-math-answer ">

<p>Register a factory: <code>builder.Services.AddScoped(sp =&gt; new TaxClient(apiKey, sp.GetRequiredService&lt;ILogger&lt;TaxClient&gt;&gt;()))</code>. For settings, the options pattern (<code>IOptions&lt;T&gt;</code>) is cleaner.</p>

</div>
</div>
<div id="faq-question-dil0000005" class="rank-math-list-item">
<h3 class="rank-math-question ">Why do my transient services never get disposed?</h3>
<div class="rank-math-answer ">

<p>They were resolved from the root provider, so the container keeps them until the app stops. Resolve them inside a request or a <code>CreateScope</code> block. See <a href="https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection/service-lifetimes" target="_blank" rel="noopener">Service lifetimes on Microsoft Learn</a>.</p>

</div>
</div>
</div>
</div>]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Blazor QuickGrid Tutorial: Sorting, Filtering and Paging (.NET 10)</title>
		<link>https://aspdotnethelp.com/blazor-quickgrid/</link>
		
		<dc:creator><![CDATA[Bijay Kumar]]></dc:creator>
		<pubDate>Sun, 08 Nov 2026 21:20:00 +0000</pubDate>
				<category><![CDATA[Blazor]]></category>
		<guid isPermaLink="false">https://aspdotnethelp.com/?p=2737</guid>

					<description><![CDATA[Say the sales manager at a Dallas furniture store wants this year&#8217;s 200 orders on one screen, sorted by total, searchable by customer and ten rows a page. In Web Forms you&#8217;d drag in a GridView. Blazor doesn&#8217;t have one. The built-in answer is Blazor QuickGrid, Microsoft&#8217;s free grid component. Give it an IQueryable, mark ... <a title="Blazor QuickGrid Tutorial: Sorting, Filtering and Paging (.NET 10)" class="read-more" href="https://aspdotnethelp.com/blazor-quickgrid/" aria-label="Read more about Blazor QuickGrid Tutorial: Sorting, Filtering and Paging (.NET 10)">Read more &#62;&#62;</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Say the sales manager at a Dallas furniture store wants this year&#8217;s 200 orders on one screen, sorted by total, searchable by customer and ten rows a page. In Web Forms you&#8217;d drag in a GridView. Blazor doesn&#8217;t have one.</p>



<p class="wp-block-paragraph">The built-in answer is <strong>Blazor QuickGrid</strong>, Microsoft&#8217;s free grid component. Give it an <code>IQueryable</code>, mark columns <code>Sortable</code>, add a <code>Paginator</code> and a bound search box, and you&#8217;re done. Here&#8217;s what we&#8217;re building on .NET 10:</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1102" height="762" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-finished-orders-grid-dotnet-10.png" alt="Blazor QuickGrid result: the 2026 Orders page filtered to CA, sorted by Total descending, two rows ticked and the summary 31 orders, 2 selected ($4,652.41)" class="wp-image-2769" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-finished-orders-grid-dotnet-10.png 1102w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-finished-orders-grid-dotnet-10-300x207.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-finished-orders-grid-dotnet-10-1024x708.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-finished-orders-grid-dotnet-10-768x531.png 768w" sizes="auto, (max-width: 1102px) 100vw, 1102px" /><figcaption class="wp-element-caption">The finished grid: CA filter, Total sorted high to low, two orders picked (headless Edge, real run)</figcaption></figure>
</div>


<h2 class="wp-block-heading">Before You Start with Blazor QuickGrid</h2>



<p class="wp-block-paragraph">You need the .NET 10 SDK (I used 10.0.402; <a href="https://aspdotnethelp.com/check-dotnet-version/">check your .NET version</a> first) and any editor. If Blazor itself is new to you, my <a href="https://aspdotnethelp.com/blazor-tutorial-for-beginners/">Blazor tutorial for beginners</a> covers pages and components.</p>



<p class="wp-block-paragraph">Our sample data is 200 made-up orders from 2026. Each order has these fields:</p>



<figure class="wp-block-table"><table><thead><tr><th>Field</th><th>Type</th><th>Notes</th></tr></thead><tbody><tr><td><code>Id</code></td><td><code>int</code></td><td>Order number, 1001 to 1200</td></tr><tr><td><code>Customer</code></td><td><code>string</code></td><td>US names such as Tyler Bennett</td></tr><tr><td><code>State</code></td><td><code>string</code></td><td>CA, CO, FL, GA, IL, NY, TX or WA</td></tr><tr><td><code>OrderDate</code></td><td><code>DateOnly</code></td><td>Jan 1 to Oct 27, 2026</td></tr><tr><td><code>Total</code></td><td><code>decimal</code></td><td>$25 to $2,425, shown in USD</td></tr><tr><td><code>Status</code></td><td><code>string</code></td><td>Pending, Shipped, Delivered or Cancelled</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Step 1: Create the Blazor App and Add the QuickGrid Package</h2>



<p class="wp-block-paragraph">QuickGrid ships as its own NuGet package. Create a Blazor Web App and add it (10.0.14 was the current version on November 1, 2026):</p>



<pre class="wp-block-code"><code>dotnet new blazor -o QuickGridDemo
cd QuickGridDemo
dotnet add package Microsoft.AspNetCore.Components.QuickGrid --version 10.0.14</code></pre>



<p class="wp-block-paragraph">I deleted the two <code>appsettings</code> files (this demo doesn&#8217;t need them) and trimmed <code>Properties/launchSettings.json</code> to one http profile on port 5737:</p>



<pre class="wp-block-code"><code>{
  &quot;$schema&quot;: &quot;https://json.schemastore.org/launchsettings.json&quot;,
  &quot;profiles&quot;: {
    &quot;http&quot;: {
      &quot;commandName&quot;: &quot;Project&quot;,
      &quot;dotnetRunMessages&quot;: true,
      &quot;launchBrowser&quot;: false,
      &quot;applicationUrl&quot;: &quot;http://localhost:5737&quot;,
      &quot;environmentVariables&quot;: {
        &quot;ASPNETCORE_ENVIRONMENT&quot;: &quot;Development&quot;
      }
    }
  }
}</code></pre>



<p class="wp-block-paragraph">My PC isn&#8217;t set to US regional settings, so I pinned the app to en-US at the top of <code>Program.cs</code>. Without it, <code>Format=&quot;C&quot;</code> would print the local currency:</p>



<pre class="wp-block-code"><code>using System.Globalization;
using QuickGridDemo.Components;

// Show prices and dates the US way, whatever the server&#x27;s regional settings are
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(&quot;en-US&quot;);
CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo(&quot;en-US&quot;);</code></pre>



<p class="wp-block-paragraph">The project targets <code>net10.0</code>, the <a href="https://aspdotnethelp.com/whats-new-in-dotnet-10/">.NET 10 LTS release</a>. Its full project file is in the EF Core section, after all three packages are added.</p>



<h2 class="wp-block-heading">Step 2: Add the Order Model and 200 Sample Orders</h2>



<p class="wp-block-paragraph">Create <code>Models/Order.cs</code>. It&#8217;s a class, not a record, because Step 6 changes <code>Status</code> in place:</p>



<pre class="wp-block-code"><code>namespace QuickGridDemo.Models;

public class Order
{
    public int Id { get; set; }
    public string Customer { get; set; } = &quot;&quot;;
    public string State { get; set; } = &quot;&quot;;
    public DateOnly OrderDate { get; set; }
    public decimal Total { get; set; }
    public string Status { get; set; } = &quot;Pending&quot;;
}</code></pre>



<p class="wp-block-paragraph">Then <code>Services/OrderData.cs</code> builds the orders. A fixed <code>Random</code> seed returns the same <a href="https://aspdotnethelp.com/list-in-csharp/">List of orders</a> on every run, so your numbers will match mine:</p>



<pre class="wp-block-code"><code>using QuickGridDemo.Models;

namespace QuickGridDemo.Services;

public static class OrderData
{
    private static readonly string[] First = [&quot;Jenna&quot;, &quot;Tyler&quot;, &quot;Maria&quot;, &quot;Derek&quot;, &quot;Ashley&quot;, &quot;Marcus&quot;, &quot;Kayla&quot;, &quot;Brandon&quot;, &quot;Lauren&quot;, &quot;Caleb&quot;];
    private static readonly string[] Last = [&quot;Morales&quot;, &quot;Brooks&quot;, &quot;Bennett&quot;, &quot;Hayes&quot;, &quot;Foster&quot;, &quot;Coleman&quot;, &quot;Reed&quot;, &quot;Parker&quot;, &quot;Sullivan&quot;, &quot;Grant&quot;];
    private static readonly string[] States = [&quot;CA&quot;, &quot;TX&quot;, &quot;NY&quot;, &quot;FL&quot;, &quot;WA&quot;, &quot;IL&quot;, &quot;CO&quot;, &quot;GA&quot;];
    private static readonly string[] Statuses = [&quot;Pending&quot;, &quot;Shipped&quot;, &quot;Delivered&quot;, &quot;Cancelled&quot;];

    // Same seed = the same orders on every run
    public static List&lt;Order&gt; Create(int count, int seed = 2026)
    {
        var rng = new Random(seed);
        var start = new DateOnly(2026, 1, 1);
        return Enumerable.Range(1, count).Select(i =&gt; new Order
        {
            Id = 1000 + i,
            Customer = $&quot;{First[rng.Next(First.Length)]} {Last[rng.Next(Last.Length)]}&quot;,
            State = States[rng.Next(States.Length)],
            OrderDate = start.AddDays(rng.Next(0, 300)),
            Total = Math.Round((decimal)(rng.NextDouble() * 2400 + 25), 2),
            Status = Statuses[rng.Next(Statuses.Length)]
        }).ToList();
    }
}</code></pre>



<h2 class="wp-block-heading">Step 3: Render Your First QuickGrid with PropertyColumn</h2>



<p class="wp-block-paragraph">Create <code>Components/Pages/Orders.razor</code>. <code>Items</code> takes an <code>IQueryable&lt;Order&gt;</code>, and each <code>PropertyColumn</code> shows one property:</p>



<pre class="wp-block-code"><code>@page &quot;/orders&quot;
@rendermode InteractiveServer
@using Microsoft.AspNetCore.Components.QuickGrid
@using QuickGridDemo.Models
@using QuickGridDemo.Services

&lt;PageTitle&gt;Orders&lt;/PageTitle&gt;

&lt;h1&gt;2026 Orders&lt;/h1&gt;

&lt;QuickGrid Items=&quot;orders&quot;&gt;
    &lt;PropertyColumn Property=&quot;@(o =&gt; o.Id)&quot; /&gt;
    &lt;PropertyColumn Property=&quot;@(o =&gt; o.Customer)&quot; /&gt;
    &lt;PropertyColumn Property=&quot;@(o =&gt; o.State)&quot; /&gt;
    &lt;PropertyColumn Property=&quot;@(o =&gt; o.OrderDate)&quot; /&gt;
    &lt;PropertyColumn Property=&quot;@(o =&gt; o.Total)&quot; /&gt;
    &lt;PropertyColumn Property=&quot;@(o =&gt; o.Status)&quot; /&gt;
&lt;/QuickGrid&gt;

@code {
    private IQueryable&lt;Order&gt; orders = OrderData.Create(200).AsQueryable();
}
</code></pre>



<p class="wp-block-paragraph">Don&#8217;t skip <code>@rendermode InteractiveServer</code>. Microsoft Learn says QuickGrid needs an interactive render mode for sorting and paging, which my <a href="https://aspdotnethelp.com/blazor-render-modes/">Blazor render modes guide</a> explains in detail.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1102" height="562" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-propertycolumn-first-grid.png" alt="First Blazor QuickGrid with PropertyColumn: columns Id, Customer, State, OrderDate, Total and Status, with raw dates like 10/26/2026 and totals like 2223.74" class="wp-image-2772" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-propertycolumn-first-grid.png 1102w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-propertycolumn-first-grid-300x153.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-propertycolumn-first-grid-1024x522.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-propertycolumn-first-grid-768x392.png 768w" sizes="auto, (max-width: 1102px) 100vw, 1102px" /><figcaption class="wp-element-caption">Step 3: six PropertyColumns, no formatting yet</figcaption></figure>
</div>


<p class="wp-block-paragraph"><strong>Checkpoint:</strong> you should see a long, plain table. curl returned 200 with 201 <code>&lt;tr&gt;</code> rows (header + 200 orders), and the first row reads <code>1001 | Tyler Bennett | GA | 10/26/2026 | 2223.74 | Pending</code>.</p>



<h2 class="wp-block-heading">Step 4: Format QuickGrid Currency and Dates and Set a Default Sort</h2>



<p class="wp-block-paragraph">Raw <code>2223.74</code> isn&#8217;t money yet. Replace the columns with these (the Status column stays for now):</p>



<pre class="wp-block-code"><code>    &lt;PropertyColumn Property=&quot;@(o =&gt; o.Id)&quot; Title=&quot;Order #&quot; Sortable=&quot;true&quot; /&gt;
    &lt;PropertyColumn Property=&quot;@(o =&gt; o.Customer)&quot; Sortable=&quot;true&quot; /&gt;
    &lt;PropertyColumn Property=&quot;@(o =&gt; o.State)&quot; Sortable=&quot;true&quot; /&gt;
    &lt;PropertyColumn Property=&quot;@(o =&gt; o.OrderDate)&quot; Title=&quot;Order Date&quot; Format=&quot;MMM d, yyyy&quot; Sortable=&quot;true&quot;
                    IsDefaultSortColumn=&quot;true&quot; InitialSortDirection=&quot;SortDirection.Descending&quot; /&gt;
    &lt;PropertyColumn Property=&quot;@(o =&gt; o.Total)&quot; Format=&quot;C&quot; Sortable=&quot;true&quot; Align=&quot;Align.Right&quot; Class=&quot;total&quot; /&gt;</code></pre>



<p class="wp-block-paragraph"><code>Format</code> works on any <code>IFormattable</code> type, so <code>&quot;C&quot;</code> gives US dollars, the same format string from <a href="https://aspdotnethelp.com/format-number-with-commas-and-decimal-in-csharp/">formatting numbers with commas in C#</a>. <code>Title</code> renames a header, and <code>Align.Right</code> lines up the cents.</p>



<p class="wp-block-paragraph"><code>IsDefaultSortColumn</code> plus <code>InitialSortDirection=&quot;SortDirection.Descending&quot;</code> puts the newest orders on top. The date pattern works like any <a href="https://aspdotnethelp.com/convert-string-to-datetime-in-csharp/">DateTime format string in C#</a>.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1102" height="422" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-format-currency-date-default-sort.png" alt="Blazor QuickGrid with Format C and MMM d, yyyy: the first row is 1016 | Kayla Brooks | NY | Oct 27, 2026 | $189.37 | Pending, sorted by Order Date descending" class="wp-image-2770" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-format-currency-date-default-sort.png 1102w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-format-currency-date-default-sort-300x115.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-format-currency-date-default-sort-1024x392.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-format-currency-date-default-sort-768x294.png 768w" sizes="auto, (max-width: 1102px) 100vw, 1102px" /><figcaption class="wp-element-caption">Step 4: USD totals, readable dates, newest first</figcaption></figure>
</div>


<h2 class="wp-block-heading">Step 5: Sort QuickGrid Columns by Clicking a Header</h2>



<p class="wp-block-paragraph">Every column in Step 4 already has <code>Sortable=&quot;true&quot;</code>, so each header is now a button. Click <strong>Total</strong> once for ascending and again for descending.</p>



<p class="wp-block-paragraph">In headless Edge, the first click put <code>$35.56</code> (order 1168) on top. The second click gave <code>$2,416.01</code>, order 1051 from CA. The header&#8217;s <code>aria-sort</code> switched from <code>ascending</code> to <code>descending</code>.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1102" height="422" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-sortable-total-descending.png" alt="Blazor QuickGrid sortable column: Total sorted descending after two header clicks, with $2,416.01 for order 1051 on top" class="wp-image-2774" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-sortable-total-descending.png 1102w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-sortable-total-descending-300x115.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-sortable-total-descending-1024x392.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-sortable-total-descending-768x294.png 768w" sizes="auto, (max-width: 1102px) 100vw, 1102px" /><figcaption class="wp-element-caption">Step 5: Total clicked twice, largest order first</figcaption></figure>
</div>


<p class="wp-block-paragraph"><strong>Checkpoint:</strong> after two clicks on Total, you see $2,416.01, $2,411.95 and $2,395.34 at the top.</p>



<h2 class="wp-block-heading">Step 6: Add a QuickGrid TemplateColumn with a Status Badge and a Button</h2>



<p class="wp-block-paragraph">A <code>TemplateColumn</code> renders any markup you like, with the row in <code>context</code>. Swap the Status <code>PropertyColumn</code> for these two columns:</p>



<pre class="wp-block-code"><code>    &lt;TemplateColumn Title=&quot;Status&quot; SortBy=&quot;byStatus&quot;&gt;
        &lt;span class=&quot;badge @BadgeCss(context.Status)&quot;&gt;@context.Status&lt;/span&gt;
    &lt;/TemplateColumn&gt;
    &lt;TemplateColumn Title=&quot;Action&quot;&gt;
        @if (context.Status == &quot;Pending&quot;)
        {
            &lt;button class=&quot;btn btn-sm btn-outline-primary ship&quot; @onclick=&quot;@(() =&gt; Ship(context))&quot;&gt;Ship&lt;/button&gt;
        }
    &lt;/TemplateColumn&gt;</code></pre>



<p class="wp-block-paragraph">Template columns can&#8217;t guess how to sort, so <code>SortBy</code> takes a <code>GridSort&lt;Order&gt;</code>. Change the grid to <code>Items=&quot;orders.AsQueryable()&quot;</code> and add this to <code>@code</code>:</p>



<pre class="wp-block-code"><code>    private readonly List&lt;Order&gt; orders = OrderData.Create(200);
    private readonly GridSort&lt;Order&gt; byStatus = GridSort&lt;Order&gt;.ByAscending(o =&gt; o.Status);

    private void Ship(Order order) =&gt; order.Status = &quot;Shipped&quot;;

    private static string BadgeCss(string status) =&gt; status switch
    {
        &quot;Pending&quot; =&gt; &quot;text-bg-warning&quot;,
        &quot;Shipped&quot; =&gt; &quot;text-bg-primary&quot;,
        &quot;Delivered&quot; =&gt; &quot;text-bg-success&quot;,
        _ =&gt; &quot;text-bg-secondary&quot;
    };</code></pre>



<p class="wp-block-paragraph">The badge color comes from a <a href="https://aspdotnethelp.com/switch-case-in-csharp/">C# switch expression</a>. I clicked the first <strong>Ship</strong> button, on order 1016, and its row changed to <code>1016 | Kayla Brooks | NY | Oct 27, 2026 | $189.37 | Shipped</code>.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1102" height="422" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-templatecolumn-status-badge-button.png" alt="Blazor QuickGrid TemplateColumn with Bootstrap status badges and Ship buttons; order 1016 now shows Shipped after the click" class="wp-image-2776" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-templatecolumn-status-badge-button.png 1102w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-templatecolumn-status-badge-button-300x115.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-templatecolumn-status-badge-button-1024x392.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-templatecolumn-status-badge-button-768x294.png 768w" sizes="auto, (max-width: 1102px) 100vw, 1102px" /><figcaption class="wp-element-caption">Step 6: badges and a Ship button in TemplateColumns</figcaption></figure>
</div>


<h2 class="wp-block-heading">Step 7: Add a QuickGrid Filter with a Search Box and a State Dropdown</h2>



<p class="wp-block-paragraph">QuickGrid has no filter row. You filter the <code>IQueryable</code> yourself and hand the result to <code>Items</code>. Add this markup above the grid:</p>



<pre class="wp-block-code"><code>&lt;div class=&quot;d-flex gap-2 mb-2&quot; style=&quot;max-width: 36rem;&quot;&gt;
    &lt;input id=&quot;search&quot; class=&quot;form-control&quot; placeholder=&quot;Search customer&quot;
           @bind=&quot;search&quot; @bind:event=&quot;oninput&quot; /&gt;
    &lt;select id=&quot;state&quot; class=&quot;form-select&quot; @bind=&quot;state&quot;&gt;
        &lt;option value=&quot;&quot;&gt;All states&lt;/option&gt;
        @foreach (var s in States)
        {
            &lt;option value=&quot;@s&quot;&gt;@s&lt;/option&gt;
        }
    &lt;/select&gt;
&lt;/div&gt;

&lt;p id=&quot;summary&quot;&gt;@Filtered.Count() orders&lt;/p&gt;</code></pre>



<p class="wp-block-paragraph">Then point the grid at <code>Items=&quot;Filtered&quot;</code> and add the fields. <code>Filtered</code> is plain <a href="https://aspdotnethelp.com/linq-in-csharp/">LINQ Where</a> over the list:</p>



<pre class="wp-block-code"><code>    private static readonly string[] States = [&quot;CA&quot;, &quot;CO&quot;, &quot;FL&quot;, &quot;GA&quot;, &quot;IL&quot;, &quot;NY&quot;, &quot;TX&quot;, &quot;WA&quot;];
    private string search = &quot;&quot;;
    private string state = &quot;&quot;;

    private IQueryable&lt;Order&gt; Filtered =&gt; orders.AsQueryable()
        .Where(o =&gt; o.Customer.Contains(search, StringComparison.OrdinalIgnoreCase))
        .Where(o =&gt; state == &quot;&quot; || o.State == state);</code></pre>



<p class="wp-block-paragraph"><code>@bind:event=&quot;oninput&quot;</code> filters on every keystroke, and <code>StringComparison.OrdinalIgnoreCase</code> makes &#8220;brooks&#8221; match &#8220;Brooks&#8221; (<a href="https://aspdotnethelp.com/check-if-string-contains-a-character-in-csharp/">more on Contains checks in C#</a>).</p>



<p class="wp-block-paragraph">The summary went from <code>200 orders</code> to <code>22 orders</code> after typing &#8220;brooks&#8221;, then <code>5 orders</code> with TX picked.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1100" height="250" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-filter-search-state-before-after.png" alt="Blazor QuickGrid filter before and after: 200 orders unfiltered on the left, 5 orders for brooks in TX on the right" class="wp-image-2768" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-filter-search-state-before-after.png 1100w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-filter-search-state-before-after-300x68.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-filter-search-state-before-after-1024x233.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-filter-search-state-before-after-768x175.png 768w" sizes="auto, (max-width: 1100px) 100vw, 1100px" /><figcaption class="wp-element-caption">Step 7: search box plus state dropdown</figcaption></figure>
</div>


<p class="wp-block-paragraph"><strong>Checkpoint:</strong> &#8220;brooks&#8221; + TX leaves 5 rows, starting with <code>Tyler Brooks</code>.</p>



<h2 class="wp-block-heading">Step 8: Add QuickGrid Pagination with Paginator</h2>



<p class="wp-block-paragraph">Paging needs a <code>PaginationState</code> with <code>ItemsPerPage</code>, the grid&#8217;s <code>Pagination</code> parameter and a <code>&lt;Paginator&gt;</code> under it:</p>



<pre class="wp-block-code"><code>&lt;QuickGrid Items=&quot;Filtered&quot; Pagination=&quot;pagination&quot;&gt;
    ...
&lt;/QuickGrid&gt;

&lt;Paginator State=&quot;pagination&quot; /&gt;</code></pre>



<pre class="wp-block-code"><code>    private readonly PaginationState pagination = new() { ItemsPerPage = 10 };

    // Back to page 1 whenever the filter changes
    private Task ResetPage() =&gt; pagination.SetCurrentPageIndexAsync(0);</code></pre>



<p class="wp-block-paragraph">Why <code>ResetPage</code>? A filter shrinks the list, and a reader on page 3 should land back on page 1 instead of a page that may no longer exist. So both inputs get <code>@bind:after=&quot;ResetPage&quot;</code>:</p>



<pre class="wp-block-code"><code>@bind=&quot;search&quot; @bind:event=&quot;oninput&quot; @bind:after=&quot;ResetPage&quot;
@bind=&quot;state&quot; @bind:after=&quot;ResetPage&quot;</code></pre>



<p class="wp-block-paragraph">I went to page 3 and typed &#8220;a&#8221;: the pager went from <code>200 items Page 3 of 20</code> to <code>185 items Page 1 of 19</code>. <code>SetCurrentPageIndexAsync</code> returns a <code>Task</code>, the kind of method my <a href="https://aspdotnethelp.com/async-await-in-csharp/">async/await guide</a> covers.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1100" height="368" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-pagination-page-1-vs-page-2.png" alt="Blazor QuickGrid pagination: Paginator shows 200 items Page 1 of 20 on the left and 200 items Page 2 of 20 after clicking next on the right" class="wp-image-2771" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-pagination-page-1-vs-page-2.png 1100w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-pagination-page-1-vs-page-2-300x100.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-pagination-page-1-vs-page-2-1024x343.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-pagination-page-1-vs-page-2-768x257.png 768w" sizes="auto, (max-width: 1100px) 100vw, 1100px" /><figcaption class="wp-element-caption">Step 8: the Paginator, before and after clicking next</figcaption></figure>
</div>


<p class="wp-block-paragraph"><strong>Checkpoint:</strong> your pager reads <code>200 items Page 1 of 20</code>, and one click on next shows <code>200 items Page 2 of 20</code> starting at order 1157.</p>



<h2 class="wp-block-heading">Step 9: Style the QuickGrid with Class, CSS Isolation and Alignment</h2>



<p class="wp-block-paragraph">Out of the box, QuickGrid renders a bare table. Wrap it in a <code>div</code> and give it Bootstrap classes through <code>Class</code>:</p>



<pre class="wp-block-code"><code>&lt;div class=&quot;grid&quot;&gt;
&lt;QuickGrid Items=&quot;Filtered&quot; Pagination=&quot;pagination&quot; Class=&quot;table table-sm align-middle&quot;&gt;
    ...
&lt;/QuickGrid&gt;
&lt;/div&gt;</code></pre>



<p class="wp-block-paragraph">In the browser, the table&#8217;s class became <code>quickgrid table table-sm align-middle</code>, and it kept <code>theme=&quot;default&quot;</code>, the <code>Theme</code> value QuickGrid&#8217;s built-in styles match.</p>



<p class="wp-block-paragraph">For your own rules, add <code>Components/Pages/Orders.razor.css</code>. The grid is a child component, so isolated CSS needs <code>::deep</code> to reach its cells:</p>



<pre class="wp-block-code"><code>.grid ::deep table {
    min-width: 52rem;
}

.grid ::deep thead th {
    background: #eef2f7;
}

.grid ::deep td.total {
    font-variant-numeric: tabular-nums;
    font-weight: 600;
}

.grid ::deep tr.picked &gt; td {
    background: #fff4d6;
}</code></pre>



<p class="wp-block-paragraph"><code>td.total</code> works because the column&#8217;s <code>Class=&quot;total&quot;</code> lands on its header and every cell. <code>Align.Right</code> added <code>col-justify-right</code> to the same <code>th</code>.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1102" height="762" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-styling-class-css-isolation.png" alt="Blazor QuickGrid styling with Bootstrap table classes, a gray header from CSS isolation and bold right-aligned totals" class="wp-image-2775" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-styling-class-css-isolation.png 1102w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-styling-class-css-isolation-300x207.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-styling-class-css-isolation-1024x708.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-styling-class-css-isolation-768x531.png 768w" sizes="auto, (max-width: 1102px) 100vw, 1102px" /><figcaption class="wp-element-caption">Step 9: Class, ::deep CSS and right-aligned totals</figcaption></figure>
</div>


<h2 class="wp-block-heading">Step 10: Select QuickGrid Rows with a Checkbox TemplateColumn</h2>



<p class="wp-block-paragraph">QuickGrid in .NET 10 has no built-in row selection (a row click event, <code>OnRowClick</code>, only shows up in the .NET 11 docs). A checkbox <code>TemplateColumn</code> and a <code>HashSet</code> do the job.</p>



<p class="wp-block-paragraph">Add a first column to the grid, plus <code>RowClass=&quot;RowCss&quot;</code> on the <code>&lt;QuickGrid&gt;</code> tag. <code>RowClass</code>, new in .NET 10, adds a CSS class to each row you return one for:</p>



<pre class="wp-block-code"><code>    &lt;TemplateColumn Title=&quot;Pick&quot;&gt;
        &lt;input type=&quot;checkbox&quot; checked=&quot;@selected.Contains(context.Id)&quot;
               @onchange=&quot;@(() =&gt; Toggle(context.Id))&quot; /&gt;
    &lt;/TemplateColumn&gt;</code></pre>



<p class="wp-block-paragraph">Then change the summary line and add the selection code to <code>@code</code>. The <code>picked</code> class is the pale yellow rule from Step 9&#8217;s CSS file:</p>



<pre class="wp-block-code"><code>&lt;p id=&quot;summary&quot;&gt;@Filtered.Count() orders, @selected.Count selected (@SelectedTotal.ToString(&quot;C&quot;))&lt;/p&gt;</code></pre>



<pre class="wp-block-code"><code>    private readonly HashSet&lt;int&gt; selected = [];

    private decimal SelectedTotal =&gt; orders.Where(o =&gt; selected.Contains(o.Id)).Sum(o =&gt; o.Total);

    private void Toggle(int id)
    {
        if (!selected.Remove(id)) selected.Add(id);
    }

    private string? RowCss(Order order) =&gt; selected.Contains(order.Id) ? &quot;picked&quot; : null;</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/vs-code-blazor-quickgrid-orders-razor-dotnet-build.png" alt="VS Code with the finished Orders.razor open, showing QuickGrid with RowClass, a checkbox TemplateColumn and PropertyColumns, and a successful dotnet build" class="wp-image-2779" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/vs-code-blazor-quickgrid-orders-razor-dotnet-build.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/vs-code-blazor-quickgrid-orders-razor-dotnet-build-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/vs-code-blazor-quickgrid-orders-razor-dotnet-build-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/vs-code-blazor-quickgrid-orders-razor-dotnet-build-768x674.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">The finished Orders.razor in VS Code</figcaption></figure>
</div>


<p class="wp-block-paragraph">Selection is by order <code>Id</code>, so it survives paging. After ticking rows 2 and 4, the summary read <code>200 orders, 2 selected ($2,259.30)</code>, and still <code>200 orders, 2 selected ($2,259.30)</code> after a trip to page 2.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1102" height="762" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-select-row-checkbox-rowclass.png" alt="Blazor QuickGrid select row: two checkboxes ticked, both rows highlighted by RowClass, summary 200 orders, 2 selected ($2,259.30)" class="wp-image-2773" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-select-row-checkbox-rowclass.png 1102w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-select-row-checkbox-rowclass-300x207.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-select-row-checkbox-rowclass-1024x708.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-select-row-checkbox-rowclass-768x531.png 768w" sizes="auto, (max-width: 1102px) 100vw, 1102px" /><figcaption class="wp-element-caption">Step 10: checkbox selection with RowClass highlighting</figcaption></figure>
</div>


<p class="wp-block-paragraph"><strong>Checkpoint:</strong> when you tick two rows, they turn pale yellow and the summary shows <code>200 orders, 2 selected ($2,259.30)</code>.</p>



<h2 class="wp-block-heading">Test the Blazor QuickGrid</h2>



<p class="wp-block-paragraph">Here&#8217;s every check I ran on the finished page in headless Edge, with real clicks and typing:</p>



<figure class="wp-block-table"><table><thead><tr><th>Check</th><th>Action</th><th>Result</th></tr></thead><tbody><tr><td>Default sort</td><td>Load /orders</td><td>Order 1016, Oct 27, 2026, on top</td></tr><tr><td>Sort</td><td>Click Total twice</td><td>$2,416.01 first</td></tr><tr><td>Filter</td><td>&#8220;brooks&#8221; + TX</td><td>5 orders</td></tr><tr><td>Paging</td><td>Next</td><td>200 items Page 2 of 20</td></tr><tr><td>Reset on filter</td><td>Page 3, type &#8220;a&#8221;</td><td>185 items Page 1 of 19</td></tr><tr><td>Last page</td><td>Go to last page</td><td>185 items Page 19 of 19, 5 orders + 5 empty rows</td></tr><tr><td>Selection</td><td>Tick 2 rows</td><td>200 orders, 2 selected ($2,259.30)</td></tr><tr><td>Intro shot</td><td>CA, Total high to low, tick 2</td><td>31 orders, 2 selected ($4,652.41)</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">The empty rows on the last page are on purpose. Learn says the Paginator pads the final page so rows keep the same height on every page.</p>



<h2 class="wp-block-heading">Virtualize QuickGrid for 10,000 Rows</h2>



<p class="wp-block-paragraph">If you have thousands of rows, paging isn&#8217;t your only option. <code>Virtualize=&quot;true&quot;</code> renders only the rows near the scroll position. Set <code>ItemSize</code> to your row height in pixels and keep every row the same height:</p>



<pre class="wp-block-code"><code>@page &quot;/orders-virtual&quot;
@rendermode InteractiveServer
@using Microsoft.AspNetCore.Components.QuickGrid
@using QuickGridDemo.Models
@using QuickGridDemo.Services

&lt;PageTitle&gt;10,000 orders&lt;/PageTitle&gt;

&lt;h1&gt;10,000 orders, virtualized&lt;/h1&gt;

&lt;div class=&quot;virtual-grid&quot; style=&quot;height: 420px; overflow-y: auto;&quot;&gt;
    &lt;QuickGrid Items=&quot;orders&quot; Virtualize=&quot;true&quot; ItemSize=&quot;35&quot; Class=&quot;table table-sm&quot;&gt;
        &lt;PropertyColumn Property=&quot;@(o =&gt; o.Id)&quot; Title=&quot;Order #&quot; Sortable=&quot;true&quot; /&gt;
        &lt;PropertyColumn Property=&quot;@(o =&gt; o.Customer)&quot; Sortable=&quot;true&quot; /&gt;
        &lt;PropertyColumn Property=&quot;@(o =&gt; o.State)&quot; Sortable=&quot;true&quot; /&gt;
        &lt;PropertyColumn Property=&quot;@(o =&gt; o.Total)&quot; Format=&quot;C&quot; Sortable=&quot;true&quot; Align=&quot;Align.Right&quot; /&gt;
    &lt;/QuickGrid&gt;
&lt;/div&gt;

@code {
    private IQueryable&lt;Order&gt; orders = OrderData.Create(10_000).AsQueryable();
}</code></pre>



<p class="wp-block-paragraph">With 10,000 orders, the DOM held 21 <code>&lt;tr&gt;</code> elements on load and 25 after I scrolled the box down, showing orders 7209 to 7233.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1102" height="622" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-virtualize-10000-orders.png" alt="Blazor QuickGrid Virtualize with 10,000 orders in a 420px scroll box, scrolled down to orders 7209 to 7233" class="wp-image-2777" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-virtualize-10000-orders.png 1102w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-virtualize-10000-orders-300x169.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-virtualize-10000-orders-1024x578.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-virtualize-10000-orders-768x433.png 768w" sizes="auto, (max-width: 1102px) 100vw, 1102px" /><figcaption class="wp-element-caption">Virtualize: 10,000 orders, about two dozen rows in the DOM</figcaption></figure>
</div>


<p class="wp-block-paragraph"><strong>Important:</strong> pick paging <em>or</em> virtualization. I set both on a test page, and the grid showed 10 rows a page (<code>10000 items Page 1 of 1000</code>), so virtualization did nothing. Learn advises against the mix too.</p>



<h2 class="wp-block-heading">Load Server-Side Data with QuickGrid ItemsProvider</h2>



<p class="wp-block-paragraph">When the data lives behind an API, use <code>ItemsProvider</code> instead of <code>Items</code>. QuickGrid calls it with the start index, row count and sort for each page:</p>



<pre class="wp-block-code"><code>@page &quot;/orders-provider&quot;
@rendermode InteractiveServer
@using Microsoft.AspNetCore.Components.QuickGrid
@using QuickGridDemo.Models
@using QuickGridDemo.Services
@inject ILogger&lt;ProviderOrders&gt; Logger

&lt;PageTitle&gt;Orders (ItemsProvider)&lt;/PageTitle&gt;

&lt;h1&gt;Orders from an ItemsProvider&lt;/h1&gt;

&lt;QuickGrid ItemsProvider=&quot;provider&quot; Pagination=&quot;pagination&quot;&gt;
    &lt;PropertyColumn Property=&quot;@(o =&gt; o.Id)&quot; Title=&quot;Order #&quot; Sortable=&quot;true&quot; /&gt;
    &lt;PropertyColumn Property=&quot;@(o =&gt; o.Customer)&quot; Sortable=&quot;true&quot; /&gt;
    &lt;PropertyColumn Property=&quot;@(o =&gt; o.Total)&quot; Format=&quot;C&quot; Sortable=&quot;true&quot; Align=&quot;Align.Right&quot; Class=&quot;total&quot; /&gt;
&lt;/QuickGrid&gt;

&lt;Paginator State=&quot;pagination&quot; /&gt;

@code {
    private static readonly List&lt;Order&gt; all = OrderData.Create(200);
    private readonly PaginationState pagination = new() { ItemsPerPage = 10 };
    private GridItemsProvider&lt;Order&gt; provider = default!;

    protected override void OnInitialized()
    {
        // In a real app, this is where you&#x27;d call your web API with skip/take/sort parameters
        provider = request =&gt;
        {
            Logger.LogInformation(&quot;StartIndex={Start}, Count={Count}, SortBy={Sort}&quot;, request.StartIndex,
                request.Count, string.Join(&quot;,&quot;, request.GetSortByProperties().Select(p =&gt; $&quot;{p.PropertyName} {p.Direction}&quot;)));
            var page = request.ApplySorting(all.AsQueryable())
                .Skip(request.StartIndex)
                .Take(request.Count ?? 10)
                .ToList();
            return ValueTask.FromResult(GridItemsProviderResult.From(page, all.Count));
        };
    }
}</code></pre>



<p class="wp-block-paragraph">My logger printed these three requests: page 1, then next, then a click on Total:</p>



<pre class="wp-block-code"><code>StartIndex=0, Count=10, SortBy=
StartIndex=10, Count=10, SortBy=
StartIndex=10, Count=10, SortBy=Total Ascending</code></pre>



<p class="wp-block-paragraph">Notice the grid stayed on page 2 after sorting. In a real app, you&#8217;d send those values to your API with <a href="https://aspdotnethelp.com/httpclient-in-csharp/">HttpClient in C#</a> and return the rows plus the total count.</p>



<h2 class="wp-block-heading">Use QuickGrid with EF Core and the EntityFrameworkAdapter</h2>



<p class="wp-block-paragraph">With EF Core, QuickGrid can page and sort in the database. Add the adapter and a provider (SQLite here, both 10.0.14):</p>



<pre class="wp-block-code"><code>dotnet add package Microsoft.AspNetCore.Components.QuickGrid.EntityFrameworkAdapter --version 10.0.14
dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 10.0.14</code></pre>



<p class="wp-block-paragraph">Here&#8217;s the project file after all three packages:</p>



<pre class="wp-block-code"><code>&lt;Project Sdk=&quot;Microsoft.NET.Sdk.Web&quot;&gt;

  &lt;PropertyGroup&gt;
    &lt;TargetFramework&gt;net10.0&lt;/TargetFramework&gt;
    &lt;Nullable&gt;enable&lt;/Nullable&gt;
    &lt;ImplicitUsings&gt;enable&lt;/ImplicitUsings&gt;
    &lt;BlazorDisableThrowNavigationException&gt;true&lt;/BlazorDisableThrowNavigationException&gt;
  &lt;/PropertyGroup&gt;

  &lt;ItemGroup&gt;
    &lt;PackageReference Include=&quot;Microsoft.AspNetCore.Components.QuickGrid&quot; Version=&quot;10.0.14&quot; /&gt;
    &lt;PackageReference Include=&quot;Microsoft.AspNetCore.Components.QuickGrid.EntityFrameworkAdapter&quot; Version=&quot;10.0.14&quot; /&gt;
    &lt;PackageReference Include=&quot;Microsoft.EntityFrameworkCore.Sqlite&quot; Version=&quot;10.0.14&quot; /&gt;
  &lt;/ItemGroup&gt;

&lt;/Project&gt;</code></pre>



<p class="wp-block-paragraph">The context is one <code>DbSet</code> in <code>Data/SalesDbContext.cs</code>:</p>



<pre class="wp-block-code"><code>using Microsoft.EntityFrameworkCore;
using QuickGridDemo.Models;

namespace QuickGridDemo.Data;

public class SalesDbContext(DbContextOptions&lt;SalesDbContext&gt; options) : DbContext(options)
{
    public DbSet&lt;Order&gt; Orders =&gt; Set&lt;Order&gt;();
}</code></pre>



<p class="wp-block-paragraph"><code>AddQuickGridEntityFrameworkAdapter</code> registers the async query executor. Here&#8217;s the final <code>Program.cs</code>, which also seeds <code>sales.db</code> with the same 200 orders:</p>



<pre class="wp-block-code"><code>using System.Globalization;
using QuickGridDemo.Components;
using Microsoft.EntityFrameworkCore;
using QuickGridDemo.Data;
using QuickGridDemo.Services;

// Show prices and dates the US way, whatever the server&#x27;s regional settings are
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(&quot;en-US&quot;);
CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo(&quot;en-US&quot;);

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

builder.Services.AddDbContextFactory&lt;SalesDbContext&gt;(options =&gt;
    options.UseSqlite(&quot;Data Source=sales.db&quot;));
builder.Services.AddQuickGridEntityFrameworkAdapter();

var app = builder.Build();

// Create sales.db with the same 200 orders the first time the app starts
using (var db = app.Services.GetRequiredService&lt;IDbContextFactory&lt;SalesDbContext&gt;&gt;().CreateDbContext())
{
    if (db.Database.EnsureCreated())
    {
        db.Orders.AddRange(OrderData.Create(200));
        db.SaveChanges();
    }
}

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler(&quot;/Error&quot;, createScopeForErrors: true);
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}
app.UseStatusCodePagesWithReExecute(&quot;/not-found&quot;, createScopeForStatusCodePages: true);
app.UseHttpsRedirection();

app.UseAntiforgery();

app.MapStaticAssets();
app.MapRazorComponents&lt;App&gt;()
    .AddInteractiveServerRenderMode();

app.Run();</code></pre>



<p class="wp-block-paragraph">The page uses a context factory, as Learn recommends, and disposes the context with the component. <code>EF.Functions.Like</code> keeps the search in SQL:</p>



<pre class="wp-block-code"><code>@page &quot;/orders-db&quot;
@rendermode InteractiveServer
@implements IAsyncDisposable
@using Microsoft.AspNetCore.Components.QuickGrid
@using Microsoft.EntityFrameworkCore
@using QuickGridDemo.Data
@using QuickGridDemo.Models
@inject IDbContextFactory&lt;SalesDbContext&gt; DbFactory

&lt;PageTitle&gt;Orders (EF Core)&lt;/PageTitle&gt;

&lt;h1&gt;Orders from SQLite&lt;/h1&gt;

&lt;input id=&quot;search&quot; class=&quot;form-control mb-2&quot; style=&quot;max-width: 18rem;&quot; placeholder=&quot;Search customer&quot;
       @bind=&quot;search&quot; @bind:event=&quot;oninput&quot; /&gt;

&lt;QuickGrid Items=&quot;Filtered&quot; Pagination=&quot;pagination&quot; Class=&quot;table table-sm&quot;&gt;
    &lt;PropertyColumn Property=&quot;@(o =&gt; o.Id)&quot; Title=&quot;Order #&quot; Sortable=&quot;true&quot; IsDefaultSortColumn=&quot;true&quot; /&gt;
    &lt;PropertyColumn Property=&quot;@(o =&gt; o.Customer)&quot; Sortable=&quot;true&quot; /&gt;
    &lt;PropertyColumn Property=&quot;@(o =&gt; o.State)&quot; Sortable=&quot;true&quot; /&gt;
    &lt;PropertyColumn Property=&quot;@(o =&gt; o.Total)&quot; Format=&quot;C&quot; Sortable=&quot;true&quot; Align=&quot;Align.Right&quot; Class=&quot;total&quot; /&gt;
&lt;/QuickGrid&gt;

&lt;Paginator State=&quot;pagination&quot; /&gt;

@code {
    private SalesDbContext context = default!;
    private readonly PaginationState pagination = new() { ItemsPerPage = 10 };
    private string search = &quot;&quot;;

    private IQueryable&lt;Order&gt; Filtered =&gt;
        context.Orders.Where(o =&gt; EF.Functions.Like(o.Customer, $&quot;%{search}%&quot;));

    protected override void OnInitialized() =&gt; context = DbFactory.CreateDbContext();

    public async ValueTask DisposeAsync() =&gt; await context.DisposeAsync();
}</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/vs-code-blazor-quickgrid-dborders-razor-ef-adapter-build.png" alt="VS Code with DbOrders.razor open, showing IDbContextFactory, a QuickGrid over context.Orders and EF.Functions.Like, and a successful dotnet build" class="wp-image-2778" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/vs-code-blazor-quickgrid-dborders-razor-ef-adapter-build.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/vs-code-blazor-quickgrid-dborders-razor-ef-adapter-build-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/vs-code-blazor-quickgrid-dborders-razor-ef-adapter-build-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/vs-code-blazor-quickgrid-dborders-razor-ef-adapter-build-768x674.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">DbOrders.razor in VS Code</figcaption></figure>
</div>


<p class="wp-block-paragraph">After two clicks on Total and one on next, EF Core logged this query. Only 10 rows leave the database:</p>



<pre class="wp-block-code"><code>SELECT &quot;o&quot;.&quot;Id&quot;, &quot;o&quot;.&quot;Customer&quot;, &quot;o&quot;.&quot;OrderDate&quot;, &quot;o&quot;.&quot;State&quot;, &quot;o&quot;.&quot;Status&quot;, &quot;o&quot;.&quot;Total&quot;
FROM &quot;Orders&quot; AS &quot;o&quot;
WHERE &quot;o&quot;.&quot;Customer&quot; LIKE @Format
ORDER BY &quot;o&quot;.&quot;Total&quot; COLLATE EF_DECIMAL DESC
LIMIT @p OFFSET @p</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1102" height="702" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-entity-framework-adapter-sqlite.png" alt="Blazor QuickGrid with the EF Core adapter and SQLite, sorted by Total descending on page 2: 1086 | Ashley Morales | FL | $2,304.04" class="wp-image-2767" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-entity-framework-adapter-sqlite.png 1102w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-entity-framework-adapter-sqlite-300x191.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-entity-framework-adapter-sqlite-1024x652.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/blazor-quickgrid-entity-framework-adapter-sqlite-768x489.png 768w" sizes="auto, (max-width: 1102px) 100vw, 1102px" /><figcaption class="wp-element-caption">EF Core + SQLite: 200 items Page 2 of 20, sorted in the database</figcaption></figure>
</div>


<p class="wp-block-paragraph">Two things surprised me. EF Core 10 sorted the <code>decimal</code> column in SQLite with <code>COLLATE EF_DECIMAL</code>, no value converter needed. And my first version, without <code>IsDefaultSortColumn</code> on Order #, made EF Core warn on every page:</p>



<pre class="wp-block-code"><code>The query uses a row limiting operator (&#x27;Skip&#x27;/&#x27;Take&#x27;) without an &#x27;OrderBy&#x27; operator. This may lead to unpredictable results. If the &#x27;Distinct&#x27; operator is used after &#x27;OrderBy&#x27;, then make sure to use the &#x27;OrderBy&#x27; operator after &#x27;Distinct&#x27; as the ordering would otherwise get erased.</code></pre>



<p class="wp-block-paragraph">Without an <code>ORDER BY</code>, page 2 can repeat rows from page 1, so always give a database grid a default sort. For the full EF Core setup, see my <a href="https://aspdotnethelp.com/aspnet-core-web-api-crud-ef-core/">ASP.NET Core Web API CRUD with EF Core</a> guide.</p>



<p class="wp-block-paragraph"><strong>Note:</strong> Learn also documents a QuickGrid scaffolder that builds CRUD pages from a model: <code>dotnet aspnet-codegenerator blazor CRUD</code> with <code>-dbProvider</code>, <code>-dc</code>, <code>-m</code> and <code>-outDir</code>. I didn&#8217;t run it here.</p>



<h2 class="wp-block-heading">QuickGrid vs a Plain HTML Table: Which Should You Use?</h2>



<figure class="wp-block-table"><table><thead><tr><th>You need</th><th>Use</th></tr></thead><tbody><tr><td>A short, read-only list (under ~20 rows)</td><td>A plain <code>&lt;table&gt;</code> with <code>@foreach</code></td></tr><tr><td>Sorting, paging or a filter on hundreds of rows</td><td>QuickGrid</td></tr><tr><td>Thousands of rows in one scroll box</td><td>QuickGrid with <code>Virtualize</code></td></tr><tr><td>Hierarchical rows, column drag-reorder, Excel-style range selection</td><td>A third-party grid (Learn says QuickGrid won&#8217;t add these)</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">I start every admin page with QuickGrid now. It&#8217;s free, it&#8217;s maintained by the ASP.NET Core team, and the whole grid fits in one Razor file.</p>



<h2 class="wp-block-heading">Troubleshooting Blazor QuickGrid</h2>



<p class="wp-block-paragraph"><strong>Headers don&#8217;t sort and the Paginator does nothing.</strong> The page is static SSR. Add <code>@rendermode InteractiveServer</code>.</p>



<p class="wp-block-paragraph"><strong>&#8220;The type or namespace name &#8216;QuickGrid&#8217; could not be found&#8221;.</strong> The package is missing, or the page lacks <code>@using Microsoft.AspNetCore.Components.QuickGrid</code>. Put that line in <code>_Imports.razor</code> to cover every page.</p>



<p class="wp-block-paragraph"><strong>Prices show the wrong currency symbol.</strong> <code>Format=&quot;C&quot;</code> uses the server&#8217;s culture. Set en-US as in Step 1.</p>



<p class="wp-block-paragraph"><strong>&#8220;A second operation was started on this context instance&#8221;.</strong> Learn lists this for concurrent EF Core queries. Create the context per component with <code>IDbContextFactory</code>, as <code>DbOrders.razor</code> does.</p>



<p class="wp-block-paragraph"><strong>Note:</strong> before you put a grid like this online, lock the page down with <code>[Authorize]</code>. My <a href="https://aspdotnethelp.com/jwt-authentication-aspnet-core/">JWT authentication guide</a> covers the API side.</p>



<h2 class="wp-block-heading">Where to Take It Next with Blazor QuickGrid</h2>



<p class="wp-block-paragraph">Add an Edit button in the Action column that opens a form, or export the filtered rows to CSV. If you&#8217;re moving an older grid app over, start with <a href="https://aspdotnethelp.com/upgrade-dotnet-8-to-dotnet-10/">upgrading .NET 8 to .NET 10</a> to get <code>RowClass</code>.</p>



<p class="wp-block-paragraph">A few more places to keep going:</p>



<ul class="wp-block-list">
<li><a href="https://aspdotnethelp.com/blazor-tutorials/">All Blazor tutorials</a></li>

<li><a href="https://aspdotnethelp.com/asp-net-core-tutorials/">ASP.NET Core tutorials for the back end</a></li>

<li><a href="https://aspdotnethelp.com/whats-new-in-csharp-14/">C# 14 features you can use in components</a></li>

<li><a href="https://aspdotnethelp.com/csharp-tutorials/">C# tutorials from the basics up</a></li>
</ul>



<p class="wp-block-paragraph">QuickGrid is itself a component that takes parameters like Items and Pagination. To build your own reusable pieces the same way, with required parameters, ChildContent and @key, see my tutorial on <a href="https://aspdotnethelp.com/blazor-components/">Blazor components and parameters in .NET 10</a>.</p>



<h2 class="wp-block-heading">Frequently Asked Questions</h2>


<div id="rank-math-faq" class="rank-math-block">
<div class="rank-math-list ">
<div id="faq-question-qg0000001" class="rank-math-list-item">
<h3 class="rank-math-question ">Does Blazor QuickGrid support filtering?</h3>
<div class="rank-math-answer ">

<p>Not as a built-in filter row. You filter the IQueryable you pass to Items, for example with a bound search box and a LINQ Where clause, and the grid re-renders with the result. A column&#8217;s ColumnOptions can host the filter input too.</p>

</div>
</div>
<div id="faq-question-qg0000002" class="rank-math-list-item">
<h3 class="rank-math-question ">How do I change the page size in QuickGrid pagination?</h3>
<div class="rank-math-answer ">

<p>Set ItemsPerPage on the PaginationState, for example new PaginationState { ItemsPerPage = 25 }, and pass it to both the grid&#8217;s Pagination parameter and the Paginator&#8217;s State.</p>

</div>
</div>
<div id="faq-question-qg0000003" class="rank-math-list-item">
<h3 class="rank-math-question ">How do I select a row in Blazor QuickGrid?</h3>
<div class="rank-math-answer ">

<p>QuickGrid in .NET 10 has no selection API. Add a TemplateColumn with a checkbox, store the selected IDs in a HashSet, and use RowClass to highlight the selected rows.</p>

</div>
</div>
<div id="faq-question-qg0000004" class="rank-math-list-item">
<h3 class="rank-math-question ">Which package do I need for QuickGrid?</h3>
<div class="rank-math-answer ">

<p>Microsoft.AspNetCore.Components.QuickGrid (10.0.14 for .NET 10 as of November 2026). For EF Core data, also add Microsoft.AspNetCore.Components.QuickGrid.EntityFrameworkAdapter and call AddQuickGridEntityFrameworkAdapter.</p>

</div>
</div>
<div id="faq-question-qg0000005" class="rank-math-list-item">
<h3 class="rank-math-question ">Can I use QuickGrid in Blazor WebAssembly?</h3>
<div class="rank-math-answer ">

<p>Yes. It works in any interactive render mode. For data behind a web API, use ItemsProvider so only the current page is requested.</p>

</div>
</div>
<div id="faq-question-qg0000006" class="rank-math-list-item">
<h3 class="rank-math-question ">Where is the official QuickGrid documentation and demo?</h3>
<div class="rank-math-answer ">

<p>See <a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/components/quickgrid?view=aspnetcore-10.0" target="_blank" rel="noopener">ASP.NET Core Blazor QuickGrid component</a> on Microsoft Learn and the <a href="https://aspnet.github.io/quickgridsamples/" target="_blank" rel="noopener">QuickGrid for Blazor sample app</a> it links to.</p>

</div>
</div>
</div>
</div>]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>HttpClient in C#: GET, POST JSON, Headers, Timeouts and IHttpClientFactory (.NET 10)</title>
		<link>https://aspdotnethelp.com/httpclient-in-csharp/</link>
		
		<dc:creator><![CDATA[Bijay Kumar]]></dc:creator>
		<pubDate>Sun, 08 Nov 2026 15:15:00 +0000</pubDate>
				<category><![CDATA[C#]]></category>
		<guid isPermaLink="false">https://aspdotnethelp.com/?p=2736</guid>

					<description><![CDATA[To make a C# HttpClient call, create one shared HttpClient with a BaseAddress, then call GetFromJsonAsync&#60;T&#62;. It sends the GET request and turns the JSON response into a typed object: Every example below ran on .NET 10 against a small orders API I started on localhost, from POST and headers to timeouts, IHttpClientFactory and automatic ... <a title="HttpClient in C#: GET, POST JSON, Headers, Timeouts and IHttpClientFactory (.NET 10)" class="read-more" href="https://aspdotnethelp.com/httpclient-in-csharp/" aria-label="Read more about HttpClient in C#: GET, POST JSON, Headers, Timeouts and IHttpClientFactory (.NET 10)">Read more &#62;&#62;</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">To make a <strong>C# HttpClient</strong> call, create one shared <code>HttpClient</code> with a <code>BaseAddress</code>, then call <code>GetFromJsonAsync&lt;T&gt;</code>. It sends the GET request and turns the JSON response into a typed object:</p>



<pre class="wp-block-code"><code>using System.Net.Http.Json;

using var http = new HttpClient { BaseAddress = new(&quot;http://localhost:5736/&quot;) };
Order? order = await http.GetFromJsonAsync&lt;Order&gt;(&quot;orders/1001&quot;);
Console.WriteLine($&quot;{order!.Customer}, {order.City} {order.State}: ${order.Total}&quot;);

record Order(int Id, string Customer, string City, string State, decimal Total);
// Jenna Morales, Austin TX: $249.99</code></pre>



<p class="wp-block-paragraph">Every example below ran on .NET 10 against a small orders API I started on localhost, from POST and headers to timeouts, IHttpClientFactory and automatic retries.</p>



<h2 class="wp-block-heading">The Sample Orders API Behind Every HttpClient Example</h2>



<p class="wp-block-paragraph">I didn&#8217;t want to call a public site you can&#8217;t control, so every program talks to a tiny local API. It holds three US orders and a few test endpoints. Here&#8217;s the record both sides share:</p>



<pre class="wp-block-code"><code>public record Order(int Id, string Customer, string City, string State, decimal Total);</code></pre>



<p class="wp-block-paragraph">And here&#8217;s the whole server, a minimal API in one file. The routes at the bottom (<code>/slow</code>, <code>/flaky</code>, <code>/connection</code>) exist only so the timeout, retry and reuse demos have something real to hit:</p>



<pre class="wp-block-code"><code>#!/usr/bin/env dotnet
#:sdk Microsoft.NET.Sdk.Web
#:property PublishAot=false
#:include Models.cs
using System.Collections.Concurrent;

var builder = WebApplication.CreateBuilder();
builder.WebHost.UseUrls(&quot;http://localhost:5736&quot;);
var app = builder.Build();

List&lt;Order&gt; orders =
[
    new(1001, &quot;Jenna Morales&quot;, &quot;Austin&quot;, &quot;TX&quot;, 249.99m),
    new(1002, &quot;Tyler Brooks&quot;, &quot;Denver&quot;, &quot;CO&quot;, 89.50m),
    new(1003, &quot;Ava Kim&quot;, &quot;Seattle&quot;, &quot;WA&quot;, 412.00m)
];
var attempts = new ConcurrentDictionary&lt;string, int&gt;();

app.MapGet(&quot;/orders&quot;, () =&gt; orders);
app.MapGet(&quot;/orders/{id:int}&quot;, (int id) =&gt;
    orders.Find(o =&gt; o.Id == id) is { } o ? Results.Ok(o) : Results.NotFound());
app.MapPost(&quot;/orders&quot;, (Order o) =&gt;
{
    if (o.Total &lt;= 0)
        return Results.Problem(title: &quot;Invalid order&quot;, detail: &quot;Total must be greater than $0.&quot;, statusCode: 400);
    orders.Add(o);
    return Results.Created($&quot;/orders/{o.Id}&quot;, o);
});
app.MapPut(&quot;/orders/{id:int}&quot;, (int id, Order o) =&gt;
{
    int i = orders.FindIndex(x =&gt; x.Id == id);
    if (i &lt; 0) return Results.NotFound();
    orders[i] = o with { Id = id };
    return Results.NoContent();
});
app.MapDelete(&quot;/orders/{id:int}&quot;, (int id) =&gt;
    orders.RemoveAll(o =&gt; o.Id == id) &gt; 0 ? Results.NoContent() : Results.NotFound());
app.MapGet(&quot;/orders/search&quot;, (string? customer, string? state) =&gt; new { customer, state });
app.MapGet(&quot;/orders/export&quot;, () =&gt; Export(50_000));
app.MapGet(&quot;/headers&quot;, (HttpRequest r) =&gt; new
{
    userAgent = r.Headers.UserAgent.ToString(),
    accept = r.Headers.Accept.ToString(),
    requestId = r.Headers[&quot;X-Request-Id&quot;].ToString()
});
app.MapGet(&quot;/secure/orders&quot;, (HttpRequest r) =&gt;
    r.Headers.Authorization == &quot;Bearer demo-token-2026&quot; ? Results.Ok(orders) : Results.Unauthorized());
app.MapGet(&quot;/slow&quot;, async (CancellationToken ct) =&gt; { await Task.Delay(5000, ct); return &quot;done&quot;; });
app.MapGet(&quot;/flaky/{id}&quot;, (string id) =&gt;
{
    int n = attempts.AddOrUpdate(id, 1, (_, count) =&gt; count + 1);
    return n &lt;= 2 ? Results.StatusCode(503) : Results.Ok(new { attempt = n });
});
app.MapGet(&quot;/connection&quot;, (HttpContext ctx) =&gt; ctx.Connection.Id);

app.Run();

static async IAsyncEnumerable&lt;Order&gt; Export(int count)
{
    for (int i = 1; i &lt;= count; i++)
    {
        yield return new(100_000 + i, $&quot;Customer {i}&quot;, &quot;Phoenix&quot;, &quot;AZ&quot;, 10m + i % 90);
        if (i % 1_000 == 0) await Task.Yield();
    }
}</code></pre>



<p class="wp-block-paragraph">Start it with <code>dotnet run ApiServer.cs</code> and leave that terminal open. It printed <code>Now listening on: http://localhost:5736</code> for me. These are <a href="https://aspdotnethelp.com/dotnet-run-file-based-apps/">.NET 10 file-based apps</a>, so there&#8217;s no project file to create.</p>



<p class="wp-block-paragraph">Want a real database behind the same routes? I built that in <a href="https://aspdotnethelp.com/aspnet-core-web-api-crud-ef-core/">this ASP.NET Core Web API CRUD tutorial with EF Core</a>. Every client below is a separate file in the same folder.</p>



<h2 class="wp-block-heading">Method 1: HttpClient GET Request with GetStringAsync and GetAsync</h2>



<p class="wp-block-paragraph">Start here when you just need the raw response text, or when you want to look at the status code and headers before you read the body. Same orders API, two calls:</p>



<pre class="wp-block-code"><code>using var http = new HttpClient { BaseAddress = new(&quot;http://localhost:5736/&quot;) };

string json = await http.GetStringAsync(&quot;orders/1002&quot;);
Console.WriteLine(json);

using HttpResponseMessage res = await http.GetAsync(&quot;orders/1003&quot;);
res.EnsureSuccessStatusCode();
Console.WriteLine($&quot;{(int)res.StatusCode} {res.StatusCode}, {res.Content.Headers.ContentType}&quot;);
Console.WriteLine(await res.Content.ReadAsStringAsync());</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>{&quot;id&quot;:1002,&quot;customer&quot;:&quot;Tyler Brooks&quot;,&quot;city&quot;:&quot;Denver&quot;,&quot;state&quot;:&quot;CO&quot;,&quot;total&quot;:89.50}
200 OK, application/json; charset=utf-8
{&quot;id&quot;:1003,&quot;customer&quot;:&quot;Ava Kim&quot;,&quot;city&quot;:&quot;Seattle&quot;,&quot;state&quot;:&quot;WA&quot;,&quot;total&quot;:412.00}</code></pre>



<p class="wp-block-paragraph">The first line is order 1002 as raw JSON. The second call returns <code>200 OK</code> with <code>application/json; charset=utf-8</code>, then order 1003.</p>



<p class="wp-block-paragraph"><strong>How does this code work?</strong></p>



<p class="wp-block-paragraph"><code>GetStringAsync</code> sends a GET and hands back the body as a string. It throws if the status isn&#8217;t 2xx, so you never see the response object.</p>



<p class="wp-block-paragraph"><code>GetAsync</code> returns the full <code>HttpResponseMessage</code>. <code>EnsureSuccessStatusCode()</code> throws an <code>HttpRequestException</code> for anything outside 200-299, and <code>Content.ReadAsStringAsync()</code> reads the body.</p>



<p class="wp-block-paragraph">Every method here is async, so the calls use <code>await</code>. If that&#8217;s new, read <a href="https://aspdotnethelp.com/async-await-in-csharp/">async and await in C#</a> first; never call <code>.Result</code> on these tasks.</p>



<p class="wp-block-paragraph"><strong>Important:</strong> the <code>BaseAddress</code> needs a trailing slash, and the relative paths don&#8217;t start with one. Otherwise the last segment of the base gets dropped.</p>



<h2 class="wp-block-heading">Method 2: Read JSON with GetFromJsonAsync (Recommended)</h2>



<p class="wp-block-paragraph">Most APIs return JSON, and you want objects, not strings. This is the HttpClient method I reach for first. Here&#8217;s the same orders endpoint read straight into a list:</p>



<pre class="wp-block-code"><code>#!/usr/bin/env dotnet
#:property PublishAot=false
#:include Models.cs
using System.Net.Http.Json;

using var http = new HttpClient { BaseAddress = new(&quot;http://localhost:5736/&quot;) };
List&lt;Order&gt; orders = (await http.GetFromJsonAsync&lt;List&lt;Order&gt;&gt;(&quot;orders&quot;))!;

foreach (Order o in orders)
    Console.WriteLine($&quot;{o.Id} {o.Customer,-14} {o.City}, {o.State}  ${o.Total}&quot;);
Console.WriteLine($&quot;Total: ${orders.Sum(o =&gt; o.Total)}&quot;);</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>1001 Jenna Morales  Austin, TX  $249.99
1002 Tyler Brooks   Denver, CO  $89.50
1003 Ava Kim        Seattle, WA  $412.00
Total: $751.49</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-httpclient-getfromjsonasync-list-output.png" alt="C# HttpClient GetFromJsonAsync reading three US orders from a local minimal API into a List&lt;Order&gt; in VS Code, total $751.49" class="wp-image-2745" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-httpclient-getfromjsonasync-list-output.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-httpclient-getfromjsonasync-list-output-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-httpclient-getfromjsonasync-list-output-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-httpclient-getfromjsonasync-list-output-768x674.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">GetFromJsonAsync turns the JSON array into a List&lt;Order&gt; in one call</figcaption></figure>
</div>


<p class="wp-block-paragraph">You get Jenna Morales in Austin, Tyler Brooks in Denver and Ava Kim in Seattle, with a total of $751.49.</p>



<p class="wp-block-paragraph"><strong>How does this code work?</strong></p>



<p class="wp-block-paragraph"><code>GetFromJsonAsync&lt;T&gt;</code> lives in <code>System.Net.Http.Json</code>. It sends the GET, checks the status and deserializes the body with <a href="https://aspdotnethelp.com/system-text-json-in-csharp/">System.Text.Json</a> using the Web defaults.</p>



<p class="wp-block-paragraph">That&#8217;s why the camelCase JSON (<code>customer</code>, <code>total</code>) fills the PascalCase record without any attributes. The result is a <a href="https://aspdotnethelp.com/list-in-csharp/">List in C#</a>, so the <a href="https://aspdotnethelp.com/linq-in-csharp/">LINQ</a> <code>Sum</code> works right away.</p>



<p class="wp-block-paragraph">It returns <code>T?</code>, because the body could be the literal <code>null</code>. I use <code>!</code> here since I control the API.</p>



<h2 class="wp-block-heading">Method 3: HttpClient POST JSON with PostAsJsonAsync, then PUT and DELETE</h2>



<p class="wp-block-paragraph">Now let&#8217;s change data. Same sample orders, and this file adds order 1004, updates it, deletes it and then confirms it&#8217;s gone:</p>



<pre class="wp-block-code"><code>#!/usr/bin/env dotnet
#:property PublishAot=false
#:include Models.cs
using System.Net.Http.Json;

using var http = new HttpClient { BaseAddress = new(&quot;http://localhost:5736/&quot;) };
var order = new Order(1004, &quot;Marcus Reed&quot;, &quot;Chicago&quot;, &quot;IL&quot;, 129.95m);

using var post = await http.PostAsJsonAsync(&quot;orders&quot;, order);
Console.WriteLine($&quot;POST   {(int)post.StatusCode} {post.Headers.Location}&quot;);
Console.WriteLine($&quot;       {await post.Content.ReadFromJsonAsync&lt;Order&gt;()}&quot;);

var updated = order with { Total = 139.95m };
using var put = await http.PutAsJsonAsync(&quot;orders/1004&quot;, updated);
Console.WriteLine($&quot;PUT    {(int)put.StatusCode} {put.StatusCode}&quot;);
using var del = await http.DeleteAsync(&quot;orders/1004&quot;);
Console.WriteLine($&quot;DELETE {(int)del.StatusCode} {del.StatusCode}&quot;);
using var get = await http.GetAsync(&quot;orders/1004&quot;);
Console.WriteLine($&quot;GET    {(int)get.StatusCode} {get.StatusCode}&quot;);</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>POST   201 /orders/1004
       Order { Id = 1004, Customer = Marcus Reed, City = Chicago, State = IL, Total = 129.95 }
PUT    204 NoContent
DELETE 204 NoContent
GET    404 NotFound</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/httpclient-postasjsonasync-put-delete-status-codes.png" alt="HttpClient PostAsJsonAsync returning 201 with a Location header, then PutAsJsonAsync 204, DeleteAsync 204 and GET 404 in the VS Code terminal" class="wp-image-2749" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/httpclient-postasjsonasync-put-delete-status-codes.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/httpclient-postasjsonasync-put-delete-status-codes-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/httpclient-postasjsonasync-put-delete-status-codes-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/httpclient-postasjsonasync-put-delete-status-codes-768x674.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">POST 201 with Location /orders/1004, PUT 204, DELETE 204, then GET 404</figcaption></figure>
</div>


<p class="wp-block-paragraph">The POST answered <code>201</code> with <code>Location: /orders/1004</code> and echoed Marcus Reed&#8217;s order. PUT and DELETE gave <code>204 NoContent</code>, and the final GET gave <code>404</code>.</p>



<p class="wp-block-paragraph"><strong>How does this code work?</strong></p>



<p class="wp-block-paragraph"><code>PostAsJsonAsync</code> serializes the record to JSON, sets <code>Content-Type: application/json</code> and sends it. <code>post.Headers.Location</code> reads the URL the server put in the 201 response.</p>



<p class="wp-block-paragraph"><code>ReadFromJsonAsync&lt;Order&gt;()</code> turns the response body back into a record. The <code>order with { Total = 139.95m }</code> copy is a record feature; see <a href="https://aspdotnethelp.com/csharp-record-vs-class-vs-struct/">record vs class vs struct in C#</a>.</p>



<p class="wp-block-paragraph"><code>DeleteAsync</code> has no body parameter, and none of these methods throw on 404. They hand you the response, so check the status yourself.</p>



<h3 class="wp-block-heading">PostAsync with StringContent (when you already have a JSON string)</h3>



<p class="wp-block-paragraph">If the JSON comes from a file or another system, there&#8217;s nothing to serialize. Wrap the string in <code>StringContent</code> and call <code>PostAsync</code>:</p>



<pre class="wp-block-code"><code>using System.Text;

using var http = new HttpClient { BaseAddress = new(&quot;http://localhost:5736/&quot;) };
string json = &quot;&quot;&quot;{&quot;id&quot;:1006,&quot;customer&quot;:&quot;Ava Kim&quot;,&quot;city&quot;:&quot;Seattle&quot;,&quot;state&quot;:&quot;WA&quot;,&quot;total&quot;:58.25}&quot;&quot;&quot;;

using var content = new StringContent(json, Encoding.UTF8, &quot;application/json&quot;);
using HttpResponseMessage res = await http.PostAsync(&quot;orders&quot;, content);
Console.WriteLine($&quot;{(int)res.StatusCode} {res.StatusCode} {res.Headers.Location}&quot;);

await http.DeleteAsync(&quot;orders/1006&quot;);   // keep the sample API at three orders</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>201 Created /orders/1006</code></pre>



<p class="wp-block-paragraph">It returned <code>201 Created</code> and <code>/orders/1006</code>; the last line deletes the order so the sample API stays at three orders.</p>



<p class="wp-block-paragraph">The third <code>StringContent</code> argument sets the media type. Leave it out and the content type defaults to <code>text/plain</code>, and most APIs reject the request.</p>



<h2 class="wp-block-heading">Method 4: How to Add Headers to HttpClient and Use SendAsync</h2>



<p class="wp-block-paragraph">Headers come in two flavors: ones every request needs, and ones that change per call. Same API, this time the <code>/headers</code> route, which echoes what it received:</p>



<pre class="wp-block-code"><code>using var http = new HttpClient { BaseAddress = new(&quot;http://localhost:5736/&quot;) };
http.DefaultRequestHeaders.UserAgent.ParseAdd(&quot;OrderSync/1.0&quot;);
http.DefaultRequestHeaders.Accept.ParseAdd(&quot;application/json&quot;);

using var request = new HttpRequestMessage(HttpMethod.Get, &quot;headers&quot;);
request.Headers.Add(&quot;X-Request-Id&quot;, &quot;req-20261108-001&quot;);

using HttpResponseMessage res = await http.SendAsync(request);
Console.WriteLine(await res.Content.ReadAsStringAsync());</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>{&quot;userAgent&quot;:&quot;OrderSync/1.0&quot;,&quot;accept&quot;:&quot;application/json&quot;,&quot;requestId&quot;:&quot;req-20261108-001&quot;}</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/httpclient-sendasync-httprequestmessage-headers.png" alt="HttpClient DefaultRequestHeaders User-Agent and Accept plus a per-request X-Request-Id header sent with SendAsync and HttpRequestMessage, echoed as JSON" class="wp-image-2750" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/httpclient-sendasync-httprequestmessage-headers.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/httpclient-sendasync-httprequestmessage-headers-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/httpclient-sendasync-httprequestmessage-headers-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/httpclient-sendasync-httprequestmessage-headers-768x674.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">Default headers and the per-request X-Request-Id all reach the server</figcaption></figure>
</div>


<p class="wp-block-paragraph">The server saw <code>OrderSync/1.0</code>, <code>application/json</code> and <code>req-20261108-001</code>.</p>



<p class="wp-block-paragraph"><strong>How does this code work?</strong></p>



<p class="wp-block-paragraph"><code>DefaultRequestHeaders</code> applies to every request this client sends. Set them once, right after you create the client: Microsoft lists the send methods as thread safe, not the header collection.</p>



<p class="wp-block-paragraph">For one call only, build an <code>HttpRequestMessage</code>, add headers to it and pass it to <code>SendAsync</code>. That&#8217;s also how you send an unusual verb like <code>HttpMethod.Patch</code> or <code>HttpMethod.Head</code>.</p>



<p class="wp-block-paragraph">A request message can be sent once only, which is why both objects sit in <code>using</code> declarations. More on that in <a href="https://aspdotnethelp.com/dispose-and-finalize-in-csharp/">Dispose and Finalize in C#</a>.</p>



<h2 class="wp-block-heading">Method 5: Send an HttpClient Bearer Token with AuthenticationHeaderValue</h2>



<p class="wp-block-paragraph">Most real APIs want a token. The sample&#8217;s <code>/secure/orders</code> route returns the same orders only when the right bearer token arrives:</p>



<pre class="wp-block-code"><code>using System.Net.Http.Headers;

using var http = new HttpClient { BaseAddress = new(&quot;http://localhost:5736/&quot;) };
var res = await http.GetAsync(&quot;secure/orders&quot;);
Console.WriteLine($&quot;No token:   {(int)res.StatusCode} {res.StatusCode}&quot;);

string token = &quot;demo-token-2026&quot;;   // in a real app this comes from your identity provider
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(&quot;Bearer&quot;, token);

res = await http.GetAsync(&quot;secure/orders&quot;);
Console.WriteLine($&quot;With token: {(int)res.StatusCode} {res.StatusCode}&quot;);</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>No token:   401 Unauthorized
With token: 200 OK</code></pre>



<p class="wp-block-paragraph">Without the header the API answered <code>401 Unauthorized</code>; with it, <code>200 OK</code>.</p>



<p class="wp-block-paragraph"><strong>How does this code work?</strong></p>



<p class="wp-block-paragraph"><code>new AuthenticationHeaderValue("Bearer", token)</code> writes <code>Authorization: Bearer demo-token-2026</code>. Setting it on <code>DefaultRequestHeaders</code> adds it to every later call from this client.</p>



<p class="wp-block-paragraph">If different users share one client, put the header on each <code>HttpRequestMessage</code> instead. For the server side, see <a href="https://aspdotnethelp.com/jwt-authentication-aspnet-core/">JWT authentication in ASP.NET Core</a>.</p>



<p class="wp-block-paragraph"><strong>Note:</strong> never hard-code real tokens. Read them from your identity provider, user secrets or a key vault.</p>



<h2 class="wp-block-heading">Method 6: Build HttpClient Query Strings with Uri.EscapeDataString</h2>



<p class="wp-block-paragraph">Query strings look like plain string building, until a value contains <code>&amp;</code>. Here&#8217;s a search for a business customer on the same API, done the naive way first:</p>



<pre class="wp-block-code"><code>using var http = new HttpClient { BaseAddress = new(&quot;http://localhost:5736/&quot;) };
string customer = &quot;Brooks &amp; Lee LLC&quot;;

Console.WriteLine(await http.GetStringAsync($&quot;orders/search?customer={customer}&amp;state=CO&quot;));

string url = $&quot;orders/search?customer={Uri.EscapeDataString(customer)}&amp;state=CO&quot;;
Console.WriteLine(url);
Console.WriteLine(await http.GetStringAsync(url));</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>{&quot;customer&quot;:&quot;Brooks &quot;,&quot;state&quot;:&quot;CO&quot;}
orders/search?customer=Brooks%20%26%20Lee%20LLC&amp;state=CO
{&quot;customer&quot;:&quot;Brooks &amp; Lee LLC&quot;,&quot;state&quot;:&quot;CO&quot;}</code></pre>



<p class="wp-block-paragraph">The naive call sent <code>customer</code> as <code>"Brooks "</code>. The encoded URL sent <code>Brooks%20%26%20Lee%20LLC</code>, and the server got the full name back.</p>



<p class="wp-block-paragraph"><strong>How does this code work?</strong></p>



<p class="wp-block-paragraph">An unescaped <code>&amp;</code> starts a new parameter, so the server split the name in two. <code>Uri.EscapeDataString</code> turns spaces into <code>%20</code> and <code>&amp;</code> into <code>%26</code>.</p>



<p class="wp-block-paragraph">Escape every value that comes from a user or a database. In ASP.NET Core, <code>QueryHelpers.AddQueryString</code> builds and encodes the whole string for you.</p>



<h2 class="wp-block-heading">Method 7: Handle HttpClient Errors with HttpRequestException and ProblemDetails</h2>



<p class="wp-block-paragraph">Things go wrong in three different ways: a 4xx/5xx status, a validation error with details, or no server at all. This file hits all three against the sample API:</p>



<pre class="wp-block-code"><code>#!/usr/bin/env dotnet
#:property PublishAot=false
#:include Models.cs
using System.Net.Http.Json;

using var http = new HttpClient { BaseAddress = new(&quot;http://localhost:5736/&quot;) };
try { await http.GetFromJsonAsync&lt;Order&gt;(&quot;orders/9999&quot;); }
catch (HttpRequestException ex) { Console.WriteLine($&quot;{(int?)ex.StatusCode}: {ex.Message}&quot;); }

var res = await http.PostAsJsonAsync(&quot;orders&quot;, new Order(1005, &quot;Ava Kim&quot;, &quot;Seattle&quot;, &quot;WA&quot;, 0m));
if (!res.IsSuccessStatusCode)
{
    Problem p = (await res.Content.ReadFromJsonAsync&lt;Problem&gt;())!;
    Console.WriteLine($&quot;{p.Status} {p.Title}: {p.Detail}&quot;);
}

try { await http.GetStringAsync(&quot;http://localhost:5799/orders&quot;); }
catch (HttpRequestException ex)
{
    Console.WriteLine($&quot;{ex.HttpRequestError}, StatusCode is null: {ex.StatusCode is null}&quot;);
}

record Problem(string Title, int Status, string Detail);</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>404: Response status code does not indicate success: 404 (Not Found).
400 Invalid order: Total must be greater than $0.
ConnectionError, StatusCode is null: True</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/httpclient-httprequestexception-statuscode-problemdetails.png" alt="HttpClient error handling: HttpRequestException StatusCode 404, a 400 ProblemDetails response read with ReadFromJsonAsync, and HttpRequestError ConnectionError" class="wp-image-2747" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/httpclient-httprequestexception-statuscode-problemdetails.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/httpclient-httprequestexception-statuscode-problemdetails-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/httpclient-httprequestexception-statuscode-problemdetails-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/httpclient-httprequestexception-statuscode-problemdetails-768x674.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">404 from GetFromJsonAsync, a 400 ProblemDetails body, and a refused connection</figcaption></figure>
</div>


<p class="wp-block-paragraph">Order 9999 gave a 404 exception, the $0 order came back as <code>400 Invalid order</code> with the server&#8217;s message, and port 5799 failed with <code>ConnectionError</code>.</p>



<p class="wp-block-paragraph"><strong>How does this code work?</strong></p>



<p class="wp-block-paragraph"><code>GetFromJsonAsync</code> throws <code>HttpRequestException</code> on a non-success status, and <code>ex.StatusCode</code> tells you which one. Catch it in a <a href="https://aspdotnethelp.com/try-catch-in-csharp/">try-catch block</a> where the call happens.</p>



<p class="wp-block-paragraph"><code>PostAsJsonAsync</code> doesn&#8217;t throw on 400, so I check <code>IsSuccessStatusCode</code> and read the problem details JSON the API sent. ASP.NET Core&#8217;s <code>Results.Problem</code> writes <code>title</code>, <code>status</code> and <code>detail</code>.</p>



<p class="wp-block-paragraph">When there&#8217;s no response at all, <code>StatusCode</code> is <code>null</code>, so <a href="https://aspdotnethelp.com/how-to-check-for-null-values-in-c-sharp/">check it for null</a>. <code>HttpRequestError</code> (.NET 8+) tells you why: <code>ConnectionError</code>, <code>NameResolutionError</code> and so on.</p>



<h2 class="wp-block-heading">Method 8: HttpClient Timeout and CancellationToken</h2>



<p class="wp-block-paragraph">The <code>/slow</code> route takes 5 seconds to answer. Here&#8217;s one client with a 2-second <code>Timeout</code>, and the same call canceled by a token after 1 second:</p>



<pre class="wp-block-code"><code>Console.WriteLine($&quot;Default timeout: {new HttpClient().Timeout}&quot;);

using var http = new HttpClient
{
    BaseAddress = new(&quot;http://localhost:5736/&quot;),
    Timeout = TimeSpan.FromSeconds(2)
};
try { await http.GetStringAsync(&quot;slow&quot;); }
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{ Console.WriteLine($&quot;Timeout: {ex.Message}&quot;); }

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1));
try { await http.GetStringAsync(&quot;slow&quot;, cts.Token); }
catch (TaskCanceledException ex)
{ Console.WriteLine($&quot;Canceled: {ex.Message} TimeoutException? {ex.InnerException is TimeoutException}&quot;); }</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>Default timeout: 00:01:40
Timeout: The request was canceled due to the configured HttpClient.Timeout of 2 seconds elapsing.
Canceled: The operation was canceled. TimeoutException? False</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/httpclient-timeout-vs-cancellationtoken-output.png" alt="C# HttpClient default timeout of 00:01:40, a 2-second HttpClient.Timeout throwing TaskCanceledException with TimeoutException, and a CancellationToken cancel" class="wp-image-2751" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/httpclient-timeout-vs-cancellationtoken-output.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/httpclient-timeout-vs-cancellationtoken-output-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/httpclient-timeout-vs-cancellationtoken-output-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/httpclient-timeout-vs-cancellationtoken-output-768x674.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">Default 100 seconds, then a timeout and a cancel that look alike but aren&#8217;t</figcaption></figure>
</div>


<p class="wp-block-paragraph">The default prints <code>00:01:40</code>. The timeout message names <code>HttpClient.Timeout of 2 seconds</code>, and the token cancel says <code>The operation was canceled.</code> with no <code>TimeoutException</code>.</p>



<p class="wp-block-paragraph"><strong>How does this code work?</strong></p>



<p class="wp-block-paragraph">The default <code>HttpClient.Timeout</code> is 100 seconds. It covers the whole request on that client, and you can only set it before the first request.</p>



<p class="wp-block-paragraph">Both cases throw <code>TaskCanceledException</code>. On .NET 5 and later, a timeout carries a <code>TimeoutException</code> as <code>InnerException</code>, and the <code>when</code> filter uses that to tell them apart.</p>



<p class="wp-block-paragraph"><strong>Pro Tip:</strong> keep <code>Timeout</code> as a safety net and pass a <code>CancellationToken</code> for per-call limits or a user&#8217;s Cancel button. Learn notes that only the shorter of the two applies.</p>



<h2 class="wp-block-heading">Why Shouldn&#8217;t You Create a New HttpClient for Every Request?</h2>



<p class="wp-block-paragraph">This is the mistake I see most in code reviews. <code>HttpClient</code> implements <code>IDisposable</code>, so people wrap each call in <code>using</code>. The sample API&#8217;s <code>/connection</code> route returns the TCP connection ID, so we can count connections:</p>



<pre class="wp-block-code"><code>const string Url = &quot;http://localhost:5736/connection&quot;;

var perCall = new HashSet&lt;string&gt;();
for (int i = 0; i &lt; 5; i++)
{
    using var client = new HttpClient();          // the mistake
    perCall.Add(await client.GetStringAsync(Url));
}
Console.WriteLine($&quot;New HttpClient per call: {perCall.Count} TCP connections&quot;);

var shared = new HttpClient(new SocketsHttpHandler
{
    PooledConnectionLifetime = TimeSpan.FromMinutes(2)
});
var reused = new HashSet&lt;string&gt;();
for (int i = 0; i &lt; 5; i++) reused.Add(await shared.GetStringAsync(Url));
Console.WriteLine($&quot;One shared HttpClient:   {reused.Count} TCP connection&quot;);</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>New HttpClient per call: 5 TCP connections
One shared HttpClient:   1 TCP connection</code></pre>



<p class="wp-block-paragraph">Five calls through five new clients opened 5 TCP connections. The shared client sent all five over 1 connection.</p>



<p class="wp-block-paragraph"><strong>How does this code work?</strong></p>



<p class="wp-block-paragraph">Each <code>HttpClient</code> owns its own connection pool, so disposing it closes the socket. A closed socket sits in <code>TIME_WAIT</code> for a while, and a busy app running that <a href="https://aspdotnethelp.com/for-loop-in-csharp/">for loop</a> at scale runs out of ports.</p>



<p class="wp-block-paragraph">Microsoft Learn&#8217;s guidance for .NET Core and .NET 5+ is either a long-lived <code>static</code> or singleton client with <code>PooledConnectionLifetime</code> set, or short-lived clients from <code>IHttpClientFactory</code>.</p>



<p class="wp-block-paragraph"><code>PooledConnectionLifetime</code> recycles connections every 2 minutes here, so DNS changes get picked up. Without it, a shared client keeps the old IP address forever.</p>



<h2 class="wp-block-heading">Method 9: IHttpClientFactory with Named and Typed Clients</h2>



<p class="wp-block-paragraph">In ASP.NET Core or a worker service, let the container hand out clients. Same orders API, now through a generic host with a named client and a typed client:</p>



<pre class="wp-block-code"><code>#!/usr/bin/env dotnet
#:property PublishAot=false
#:package Microsoft.Extensions.Hosting@10.0.14
#:package Microsoft.Extensions.Http@10.0.14
#:include Models.cs
using System.Net.Http.Json;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

var builder = Host.CreateApplicationBuilder();
builder.Logging.ClearProviders();
builder.Services.AddHttpClient(&quot;orders&quot;, c =&gt; c.BaseAddress = new(&quot;http://localhost:5736/&quot;));
builder.Services.AddHttpClient&lt;OrdersClient&gt;(c =&gt; c.BaseAddress = new(&quot;http://localhost:5736/&quot;));
using IHost host = builder.Build();

var factory = host.Services.GetRequiredService&lt;IHttpClientFactory&gt;();
HttpClient named = factory.CreateClient(&quot;orders&quot;);
var all = await named.GetFromJsonAsync&lt;List&lt;Order&gt;&gt;(&quot;orders&quot;);
Console.WriteLine($&quot;Named client: {all!.Count} orders&quot;);

var typed = host.Services.GetRequiredService&lt;OrdersClient&gt;();
Order o = await typed.GetAsync(1002);
Console.WriteLine($&quot;Typed client: {o.Customer}, {o.City} {o.State}&quot;);

class OrdersClient(HttpClient http)
{
    public async Task&lt;Order&gt; GetAsync(int id) =&gt;
        (await http.GetFromJsonAsync&lt;Order&gt;($&quot;orders/{id}&quot;))!;
}</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>Named client: 3 orders
Typed client: Tyler Brooks, Denver CO</code></pre>



<p class="wp-block-paragraph">The named client read 3 orders, and the typed client returned Tyler Brooks in Denver, CO. Packages: <code>Microsoft.Extensions.Hosting</code> 10.0.14 and <code>Microsoft.Extensions.Http</code> 10.0.14.</p>



<p class="wp-block-paragraph"><strong>How does this code work?</strong></p>



<p class="wp-block-paragraph"><code>AddHttpClient("orders", ...)</code> registers a named client; <code>factory.CreateClient("orders")</code> builds a new <code>HttpClient</code> with that base address. The factory pools the handlers underneath, so creating clients is cheap.</p>



<p class="wp-block-paragraph"><code>AddHttpClient&lt;OrdersClient&gt;</code> registers a typed client. The <a href="https://aspdotnethelp.com/constructor-in-csharp/">primary constructor</a> receives a configured <code>HttpClient</code>, and the class wraps the API in real methods like <code>GetAsync(1002)</code>.</p>



<p class="wp-block-paragraph">Typed clients are registered as transient, and handlers are recycled every 2 minutes by default. Don&#8217;t capture a typed client in a singleton. In tests, put an <a href="https://aspdotnethelp.com/interface-in-csharp/">interface</a> in front of <code>OrdersClient</code>.</p>



<p class="wp-block-paragraph">In ASP.NET Core, the same <code>builder.Services.AddHttpClient</code> lines go in <code>Program.cs</code>, and you inject <code>OrdersClient</code> into an endpoint or controller.</p>



<h2 class="wp-block-heading">Method 10: HttpClient Retries with AddStandardResilienceHandler</h2>



<p class="wp-block-paragraph">Networks fail for a second and then recover. The <code>/flaky/{id}</code> route on the same API returns <code>503</code> for the first two calls with a given ID and succeeds on the third:</p>



<pre class="wp-block-code"><code>#!/usr/bin/env dotnet
#:package Microsoft.Extensions.Hosting@10.0.14
#:package Microsoft.Extensions.Http.Resilience@10.11.0
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

const string Api = &quot;http://localhost:5736/&quot;;
var builder = Host.CreateApplicationBuilder();
builder.Logging.ClearProviders();
builder.Services.AddHttpClient(&quot;orders&quot;, c =&gt; c.BaseAddress = new(Api))
    .AddStandardResilienceHandler();
using IHost host = builder.Build();

using var plain = new HttpClient { BaseAddress = new(Api) };
var first = await plain.GetAsync($&quot;flaky/{Guid.NewGuid():N}&quot;);
Console.WriteLine($&quot;Plain HttpClient: {(int)first.StatusCode} {first.StatusCode}&quot;);

var factory = host.Services.GetRequiredService&lt;IHttpClientFactory&gt;();
HttpClient http = factory.CreateClient(&quot;orders&quot;);
var res = await http.GetAsync($&quot;flaky/{Guid.NewGuid():N}&quot;);
Console.WriteLine($&quot;With resilience:  {(int)res.StatusCode} {await res.Content.ReadAsStringAsync()}&quot;);</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>Plain HttpClient: 503 ServiceUnavailable
With resilience:  200 {&quot;attempt&quot;:3}</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/httpclient-addstandardresiliencehandler-retry-503.png" alt="HttpClient AddStandardResilienceHandler from Microsoft.Extensions.Http.Resilience retrying two 503 responses and succeeding on attempt 3 in VS Code" class="wp-image-2746" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/httpclient-addstandardresiliencehandler-retry-503.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/httpclient-addstandardresiliencehandler-retry-503-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/httpclient-addstandardresiliencehandler-retry-503-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/httpclient-addstandardresiliencehandler-retry-503-768x674.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">A plain client stops at 503; the resilient client retries and succeeds on attempt 3</figcaption></figure>
</div>


<p class="wp-block-paragraph">The plain client got <code>503 ServiceUnavailable</code>. The resilient client returned <code>200</code> with <code>{"attempt":3}</code>: two failures, then success, with no retry code of mine.</p>



<p class="wp-block-paragraph"><strong>How does this code work?</strong></p>



<p class="wp-block-paragraph"><code>AddStandardResilienceHandler()</code> comes from <code>Microsoft.Extensions.Http.Resilience</code> (10.11.0 here). It stacks a rate limiter, a 30-second total timeout, retries, a circuit breaker and a 10-second per-attempt timeout.</p>



<p class="wp-block-paragraph">The retry part makes up to 3 retries with exponential backoff and jitter, starting at 2 seconds. It retries 5xx, 408 and 429 responses plus <code>HttpRequestException</code>. Each test uses <a href="https://aspdotnethelp.com/how-to-create-a-guid-in-c-net/">a new GUID</a> so its counter starts fresh.</p>



<p class="wp-block-paragraph"><strong>Important:</strong> it retries every HTTP method by default, including POST. Call <code>options.Retry.DisableForUnsafeHttpMethods()</code> if a repeated POST could create duplicate orders.</p>



<h2 class="wp-block-heading">Method 11: Stream a Large Response with ResponseHeadersRead</h2>



<p class="wp-block-paragraph">By default, <code>GetAsync</code> buffers the whole body in memory before it returns. For a big export that&#8217;s wasteful. The sample API&#8217;s <code>/orders/export</code> route streams 50,000 orders:</p>



<pre class="wp-block-code"><code>#!/usr/bin/env dotnet
#:property PublishAot=false
#:include Models.cs
using System.Globalization;
using System.Text.Json;

CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(&quot;en-US&quot;);
using var http = new HttpClient { BaseAddress = new(&quot;http://localhost:5736/&quot;) };

using HttpResponseMessage res = await http.GetAsync(&quot;orders/export&quot;, HttpCompletionOption.ResponseHeadersRead);
res.EnsureSuccessStatusCode();
await using Stream body = await res.Content.ReadAsStreamAsync();

int count = 0; decimal total = 0;
await foreach (Order? o in JsonSerializer.DeserializeAsyncEnumerable&lt;Order&gt;(body, JsonSerializerOptions.Web))
{
    count++; total += o!.Total;
}
Console.WriteLine($&quot;Streamed {count:N0} orders worth {total:C}&quot;);</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>Streamed 50,000 orders worth $2,724,050.00</code></pre>



<p class="wp-block-paragraph">It streamed 50,000 orders worth $2,724,050.00 without ever holding the full JSON in memory.</p>



<p class="wp-block-paragraph"><strong>How does this code work?</strong></p>



<p class="wp-block-paragraph"><code>HttpCompletionOption.ResponseHeadersRead</code> makes <code>GetAsync</code> return as soon as the headers arrive. <code>ReadAsStreamAsync</code> then gives you the body as it downloads.</p>



<p class="wp-block-paragraph"><code>DeserializeAsyncEnumerable</code> yields one order at a time to the <a href="https://aspdotnethelp.com/foreach-loop-in-csharp/">await foreach loop</a>. <code>GetFromJsonAsAsyncEnumerable</code> does the same in one call. The culture line keeps the <a href="https://aspdotnethelp.com/format-number-with-commas-and-decimal-in-csharp/">number and currency formatting</a> in US style.</p>



<p class="wp-block-paragraph">Dispose the response when you&#8217;re done. With <code>ResponseHeadersRead</code>, the connection stays busy until you read or dispose the body.</p>



<h2 class="wp-block-heading">HttpClient vs IHttpClientFactory vs WebClient vs RestSharp</h2>



<p class="wp-block-paragraph">These come up in every code review and in .NET interviews. Here&#8217;s how I&#8217;d pick in 2026:</p>



<figure class="wp-block-table"><table><thead><tr><th>Option</th><th>What it is</th><th>Use it when</th></tr></thead><tbody><tr><td>Shared <code>HttpClient</code></td><td>Built in; one static instance with <code>PooledConnectionLifetime</code></td><td>Console apps, tools, libraries</td></tr><tr><td><code>IHttpClientFactory</code></td><td><code>Microsoft.Extensions.Http</code>; pooled handlers, named/typed clients</td><td>ASP.NET Core, worker services, anything with DI</td></tr><tr><td><code>WebClient</code></td><td>Old API, marked obsolete (SYSLIB0014) since .NET 6</td><td>Never in new code</td></tr><tr><td>RestSharp</td><td>Third-party wrapper built on <code>HttpClient</code></td><td>Only if your team already standardizes on it</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">HttpClient vs IHttpClientFactory isn&#8217;t really a contest: the factory creates <code>HttpClient</code> objects. If you&#8217;re prepping for interviews, these <a href="https://aspdotnethelp.com/dot-net-core-interview-questions-and-answers/">.NET Core interview questions</a> cover the follow-ups.</p>



<h2 class="wp-block-heading">Which HttpClient Method Should You Use?</h2>



<figure class="wp-block-table"><table><thead><tr><th>Task</th><th>Use</th><th>Status on the sample API</th></tr></thead><tbody><tr><td>Read JSON</td><td><code>GetFromJsonAsync&lt;T&gt;</code></td><td>200</td></tr><tr><td>Create</td><td><code>PostAsJsonAsync</code></td><td>201 + Location</td></tr><tr><td>Update / delete</td><td><code>PutAsJsonAsync</code> / <code>DeleteAsync</code></td><td>204</td></tr><tr><td>Custom verb or per-call headers</td><td><code>SendAsync</code> + <code>HttpRequestMessage</code></td><td>200</td></tr><tr><td>Flaky dependency</td><td><code>AddStandardResilienceHandler</code></td><td>503, 503, 200</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Things to Keep in Mind</h2>



<ul class="wp-block-list">
<li><strong>Reuse the client.</strong> One shared instance with <code>PooledConnectionLifetime</code>, or <code>IHttpClientFactory</code>; never <code>new HttpClient()</code> per call.</li>

<li><strong>Only some methods throw on errors.</strong> <code>GetStringAsync</code> and <code>GetFromJsonAsync</code> throw; <code>GetAsync</code>, <code>PostAsJsonAsync</code> and <code>DeleteAsync</code> return the response.</li>

<li><strong>Timeouts throw TaskCanceledException.</strong> Check <code>InnerException is TimeoutException</code> to tell a timeout from a cancel.</li>

<li><strong>Escape query values.</strong> <code>Uri.EscapeDataString</code> for anything a user typed.</li>

<li><strong>Don&#8217;t retry POST blindly.</strong> Turn off retries for unsafe methods unless the API is idempotent.</li>
</ul>



<p class="wp-block-paragraph">Here&#8217;s what I&#8217;d read next:</p>



<ul class="wp-block-list">
<li><a href="https://aspdotnethelp.com/whats-new-in-csharp-14/">C# 14 features worth using in your API clients</a></li>

<li><a href="https://aspdotnethelp.com/upgrade-dotnet-8-to-dotnet-10/">Upgrade a .NET 8 app to .NET 10</a></li>

<li><a href="https://aspdotnethelp.com/check-dotnet-version/">Check which .NET version your app runs on</a></li>

<li><a href="https://aspdotnethelp.com/csharp-tutorials/">Browse every C# tutorial on aspdotnethelp.com</a></li>
</ul>



<h2 class="wp-block-heading">Frequently Asked Questions</h2>


<div id="rank-math-faq" class="rank-math-block">
<div class="rank-math-list ">
<div id="faq-question-hc0000001" class="rank-math-list-item">
<h3 class="rank-math-question ">What is the default timeout of HttpClient in C#?</h3>
<div class="rank-math-answer ">

<p>100 seconds (100,000 milliseconds). Change it with <code>client.Timeout = TimeSpan.FromSeconds(30)</code> before the first request, or set <code>Timeout.InfiniteTimeSpan</code> and use a <code>CancellationToken</code> instead.</p>

</div>
</div>
<div id="faq-question-hc0000002" class="rank-math-list-item">
<h3 class="rank-math-question ">How do I add a header to HttpClient?</h3>
<div class="rank-math-answer ">

<p>Use <code>client.DefaultRequestHeaders</code> for headers every request needs, like <code>UserAgent.ParseAdd("MyApp/1.0")</code>. For one request, add it to an <code>HttpRequestMessage</code> and send it with <code>SendAsync</code>.</p>

</div>
</div>
<div id="faq-question-hc0000003" class="rank-math-list-item">
<h3 class="rank-math-question ">How do I send a bearer token with HttpClient?</h3>
<div class="rank-math-answer ">

<p>Set <code>client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token)</code>, or put the same value on <code>request.Headers.Authorization</code> when the token changes per user.</p>

</div>
</div>
<div id="faq-question-hc0000004" class="rank-math-list-item">
<h3 class="rank-math-question ">What&#8217;s the difference between PostAsync and PostAsJsonAsync?</h3>
<div class="rank-math-answer ">

<p><code>PostAsync</code> takes any <code>HttpContent</code>, such as <code>StringContent</code> you built yourself. <code>PostAsJsonAsync</code> serializes an object to JSON with System.Text.Json and sets the content type for you.</p>

</div>
</div>
<div id="faq-question-hc0000005" class="rank-math-list-item">
<h3 class="rank-math-question ">Should I dispose HttpClient?</h3>
<div class="rank-math-answer ">

<p>Dispose clients you create when the app shuts down, not after each call. Clients from <code>IHttpClientFactory</code> are fine to dispose because the factory owns the handlers.</p>

</div>
</div>
<div id="faq-question-hc0000006" class="rank-math-list-item">
<h3 class="rank-math-question ">Is HttpClient thread-safe?</h3>
<div class="rank-math-answer ">

<p>The send methods are: <code>GetAsync</code>, <code>PostAsync</code>, <code>PutAsync</code>, <code>DeleteAsync</code>, <code>SendAsync</code> and the <code>Get*Async</code> helpers can run from many threads on one client. Set <code>DefaultRequestHeaders</code>, <code>BaseAddress</code> and <code>Timeout</code> once, before sharing it (<a href="https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient" target="_blank" rel="noopener">HttpClient class on Microsoft Learn</a>).</p>

</div>
</div>
</div>
</div>]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>JWT Authentication in ASP.NET Core Web API (.NET 10): Step by Step</title>
		<link>https://aspdotnethelp.com/jwt-authentication-aspnet-core/</link>
		
		<dc:creator><![CDATA[Bijay Kumar]]></dc:creator>
		<pubDate>Sat, 07 Nov 2026 21:20:00 +0000</pubDate>
				<category><![CDATA[ASP.NET Core]]></category>
		<guid isPermaLink="false">https://aspdotnethelp.com/?p=2735</guid>

					<description><![CDATA[Most first tries at ASP.NET Core JWT authentication sign tokens with a short string like my-secret-key-16. It compiles fine, then the first token you generate throws this: HS256 needs a signing key of at least 256 bits, and that string is only 128. The fix is a long random key kept in user secrets, plus ... <a title="JWT Authentication in ASP.NET Core Web API (.NET 10): Step by Step" class="read-more" href="https://aspdotnethelp.com/jwt-authentication-aspnet-core/" aria-label="Read more about JWT Authentication in ASP.NET Core Web API (.NET 10): Step by Step">Read more &#62;&#62;</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Most first tries at <strong>ASP.NET Core JWT authentication</strong> sign tokens with a short string like <code>my-secret-key-16</code>. It compiles fine, then the first token you generate throws this:</p>



<pre class="wp-block-code"><code>ArgumentOutOfRangeException: IDX10720: Unable to create KeyedHashAlgorithm for algorithm &#x27;HS256&#x27;, the key size must be greater than: &#x27;256&#x27; bits, key has &#x27;128&#x27; bits. (Parameter &#x27;keyBytes&#x27;)</code></pre>



<p class="wp-block-paragraph">HS256 needs a signing key of at least 256 bits, and that string is only 128. The fix is a long random key kept in user secrets, plus AddJwtBearer checking the issuer, audience, signature and lifetime of every token.</p>



<p class="wp-block-paragraph">Here&#8217;s what we&#8217;re building, a .NET 10 payroll API answering a real HttpClient test run:</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/asp-net-core-jwt-authentication-test-401-200-403.png" alt="ASP.NET Core JWT authentication test in VS Code: TestApi.cs calls a .NET 10 minimal API and prints 401 Unauthorized with no token, 200 OK with a valid token, 403 Forbidden for the wrong role and 401 for a tampered token" class="wp-image-2754" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/asp-net-core-jwt-authentication-test-401-200-403.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/asp-net-core-jwt-authentication-test-401-200-403-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/asp-net-core-jwt-authentication-test-401-200-403-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/asp-net-core-jwt-authentication-test-401-200-403-768x674.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">The finished API: no token gets 401, the wrong role gets 403, a valid JWT gets 200</figcaption></figure>
</div>


<h2 class="wp-block-heading">How Does JWT Bearer Authentication Work in ASP.NET Core?</h2>



<p class="wp-block-paragraph">A JSON Web Token is three Base64Url parts joined by dots: a header, a payload of claims and a signature. Your API signs it at login, and the client sends it back on every call in an <code>Authorization: Bearer</code> header.</p>



<figure class="wp-block-table"><table><thead><tr><th>Step</th><th>Who does it</th><th>In this build</th></tr></thead><tbody><tr><td>1. Sign in</td><td>Client</td><td>POST /login with a username and password</td></tr><tr><td>2. Issue the token</td><td>API</td><td>JsonWebTokenHandler.CreateToken signs it with HS256</td></tr><tr><td>3. Send it</td><td>Client</td><td>Authorization: Bearer &lt;token&gt; on each request</td></tr><tr><td>4. Validate it</td><td>JwtBearer handler</td><td>Signature, issuer, audience and lifetime</td></tr><tr><td>5. Authorize</td><td>Authorization middleware</td><td>RequireAuthorization, a role policy, a claim policy</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Failing step 4 gives a 401, and passing it but failing step 5 gives a 403. That split is what the test run above shows. If minimal APIs are new to you, the <a href="https://aspdotnethelp.com/aspnet-core-web-api-crud-ef-core/">ASP.NET Core Web API CRUD tutorial</a> builds one from scratch.</p>



<h2 class="wp-block-heading">Before You Start the JWT Build</h2>



<p class="wp-block-paragraph">You need the .NET 10 SDK (I used 10.0.402) and any editor; I used VS Code. Not sure which SDK you have? <a href="https://aspdotnethelp.com/check-dotnet-version/">Check your .NET version</a> first. Here&#8217;s the NuGet package and the library it brings in:</p>



<figure class="wp-block-table"><table><thead><tr><th>Package</th><th>Version</th><th>Used for</th></tr></thead><tbody><tr><td>Microsoft.AspNetCore.Authentication.JwtBearer</td><td>10.0.14</td><td>AddJwtBearer, validates incoming tokens</td></tr><tr><td>Microsoft.IdentityModel.JsonWebTokens</td><td>8.19.2 (comes with JwtBearer)</td><td>JsonWebTokenHandler, creates tokens</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">The sample API has two demo users and four endpoints. Tyler works in Operations, and Jenna runs payroll in Finance:</p>



<figure class="wp-block-table"><table><thead><tr><th>Username</th><th>Name</th><th>Department</th><th>Roles</th></tr></thead><tbody><tr><td>tbrooks</td><td>Tyler Brooks</td><td>Operations</td><td>Employee</td></tr><tr><td>jmorales</td><td>Jenna Morales</td><td>Finance</td><td>Employee, Payroll</td></tr></tbody></table></figure>



<figure class="wp-block-table"><table><thead><tr><th>Endpoint</th><th>Rule</th><th>Tyler</th><th>Jenna</th></tr></thead><tbody><tr><td>GET /health</td><td>Public</td><td>200</td><td>200</td></tr><tr><td>GET /me</td><td>Any valid token</td><td>200</td><td>200</td></tr><tr><td>GET /payroll</td><td>Role Payroll</td><td>403</td><td>200</td></tr><tr><td>GET /budget</td><td>Claim department = Finance</td><td>403</td><td>200</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Step 1: Create the Minimal API and Add the JwtBearer Package</h2>



<p class="wp-block-paragraph">Start from the empty web template and add the JWT bearer handler at the exact version I tested:</p>



<pre class="wp-block-code"><code>dotnet new web -n PayrollApi -o PayrollApi
cd PayrollApi
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 10.0.14</code></pre>



<p class="wp-block-paragraph">I trimmed <code>Properties/launchSettings.json</code> to one http profile on port 5735. It also sets the Development environment, which matters in Step 2:</p>



<pre class="wp-block-code"><code>{
  &quot;$schema&quot;: &quot;https://json.schemastore.org/launchsettings.json&quot;,
  &quot;profiles&quot;: {
    &quot;http&quot;: {
      &quot;commandName&quot;: &quot;Project&quot;,
      &quot;dotnetRunMessages&quot;: true,
      &quot;launchBrowser&quot;: false,
      &quot;applicationUrl&quot;: &quot;http://localhost:5735&quot;,
      &quot;environmentVariables&quot;: {
        &quot;ASPNETCORE_ENVIRONMENT&quot;: &quot;Development&quot;
      }
    }
  }
}</code></pre>



<p class="wp-block-paragraph">I deleted <code>appsettings.Development.json</code> since nothing here needs it. Here&#8217;s the project file after Step 2 adds the user secrets ID:</p>



<pre class="wp-block-code"><code>&lt;Project Sdk=&quot;Microsoft.NET.Sdk.Web&quot;&gt;

  &lt;PropertyGroup&gt;
    &lt;TargetFramework&gt;net10.0&lt;/TargetFramework&gt;
    &lt;Nullable&gt;enable&lt;/Nullable&gt;
    &lt;ImplicitUsings&gt;enable&lt;/ImplicitUsings&gt;
    &lt;UserSecretsId&gt;a911f57d-d8b7-444f-b815-6f906b67e6e9&lt;/UserSecretsId&gt;
  &lt;/PropertyGroup&gt;

  &lt;ItemGroup&gt;
    &lt;PackageReference Include=&quot;Microsoft.AspNetCore.Authentication.JwtBearer&quot; Version=&quot;10.0.14&quot; /&gt;
  &lt;/ItemGroup&gt;

&lt;/Project&gt;</code></pre>



<p class="wp-block-paragraph"><strong>Checkpoint:</strong> <code>dotnet build</code> succeeds and the csproj references JwtBearer <code>10.0.14</code>.</p>



<h2 class="wp-block-heading">Step 2: Store the JWT Signing Key with dotnet user-secrets</h2>



<p class="wp-block-paragraph">The issuer, audience and token lifetime aren&#8217;t secret, so they go in <code>appsettings.json</code>. The signing key is, so it doesn&#8217;t:</p>



<pre class="wp-block-code"><code>{
  &quot;Logging&quot;: {
    &quot;LogLevel&quot;: {
      &quot;Default&quot;: &quot;Information&quot;,
      &quot;Microsoft.AspNetCore&quot;: &quot;Warning&quot;
    }
  },
  &quot;AllowedHosts&quot;: &quot;*&quot;,
  &quot;Jwt&quot;: {
    &quot;Issuer&quot;: &quot;payroll-api-demo&quot;,
    &quot;Audience&quot;: &quot;payroll-web&quot;,
    &quot;AccessTokenMinutes&quot;: 15
  }
}</code></pre>



<p class="wp-block-paragraph">Anyone holding the key can mint a valid token for any user. Generate 48 random bytes and save them as a user secret without ever printing them (PowerShell 7):</p>



<pre class="wp-block-code"><code>dotnet user-secrets init
$key = [Convert]::ToBase64String([System.Security.Cryptography.RandomNumberGenerator]::GetBytes(48))
dotnet user-secrets set &quot;Jwt:SigningKey&quot; $key</code></pre>



<pre class="wp-block-code"><code>Successfully saved Jwt:SigningKey to the secret store.</code></pre>



<p class="wp-block-paragraph">User secrets live in your Windows profile, outside the repo, and <code>WebApplication.CreateBuilder</code> loads them only in Development. In production, set an environment variable named <code>Jwt__SigningKey</code> (double underscore) or read it from Azure Key Vault.</p>



<p class="wp-block-paragraph">Why 48 bytes? The 64-character Base64 text becomes the key bytes through <a href="https://aspdotnethelp.com/convert-string-to-byte-array-in-csharp/">Encoding.UTF8.GetBytes</a>, so that&#8217;s 512 bits. Here&#8217;s the short key from the intro, run on its own so you can see the real exception:</p>



<pre class="wp-block-code"><code>#:package Microsoft.IdentityModel.JsonWebTokens@8.19.2
using System.Text;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;

var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(&quot;my-secret-key-16&quot;));
Console.WriteLine($&quot;Key size: {key.KeySize} bits&quot;);
try
{
    new JsonWebTokenHandler().CreateToken(new SecurityTokenDescriptor
    {
        Issuer = &quot;payroll-api-demo&quot;,
        SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
    });
}
catch (Exception ex) { Console.WriteLine($&quot;{ex.GetType().Name}: {ex.Message}&quot;); }</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>Key size: 128 bits
ArgumentOutOfRangeException: IDX10720: Unable to create KeyedHashAlgorithm for algorithm &#x27;HS256&#x27;, the key size must be greater than: &#x27;256&#x27; bits, key has &#x27;128&#x27; bits. (Parameter &#x27;keyBytes&#x27;)</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/idx10720-jwt-signing-key-must-be-256-bits-aspnet-core.png" alt="IDX10720 in ASP.NET Core JWT: ShortKey.cs in VS Code creates a 128-bit SymmetricSecurityKey and JsonWebTokenHandler.CreateToken throws ArgumentOutOfRangeException, the key size must be greater than 256 bits" class="wp-image-2757" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/idx10720-jwt-signing-key-must-be-256-bits-aspnet-core.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/idx10720-jwt-signing-key-must-be-256-bits-aspnet-core-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/idx10720-jwt-signing-key-must-be-256-bits-aspnet-core-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/idx10720-jwt-signing-key-must-be-256-bits-aspnet-core-768x674.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">A 16-character key is 128 bits, and HS256 refuses it with IDX10720</figcaption></figure>
</div>


<p class="wp-block-paragraph">It&#8217;s a <a href="https://aspdotnethelp.com/dotnet-run-file-based-apps/">file-based app</a>, so the <code>#:package</code> line pulls the same JsonWebTokens version the API uses. The API&#8217;s Program.cs checks <code>KeySize</code> at startup, so a weak key fails before the first request.</p>



<p class="wp-block-paragraph"><strong>Checkpoint:</strong> <code>dotnet user-secrets list</code> shows a <code>Jwt:SigningKey</code> entry, and the value never lands in a file inside the project.</p>



<h2 class="wp-block-heading">Step 3: Configure AddJwtBearer with TokenValidationParameters</h2>



<p class="wp-block-paragraph">This block tells the JWT bearer handler what a good token looks like. Every check is on, and the values come from configuration:</p>



<pre class="wp-block-code"><code>builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =&gt;
    {
        options.MapInboundClaims = false;          // keep &quot;name&quot; and &quot;role&quot; as they are in the token
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = jwt[&quot;Issuer&quot;],
            ValidateAudience = true,
            ValidAudience = jwt[&quot;Audience&quot;],
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = signingKey,
            ValidateLifetime = true,
            ClockSkew = TimeSpan.Zero,             // the default is 5 minutes
            NameClaimType = &quot;name&quot;,
            RoleClaimType = &quot;role&quot;
        };
    });</code></pre>



<figure class="wp-block-table"><table><thead><tr><th>Setting</th><th>What it rejects</th><th>Default</th></tr></thead><tbody><tr><td>ValidateIssuer + ValidIssuer</td><td>Tokens from another issuer</td><td>On</td></tr><tr><td>ValidateAudience + ValidAudience</td><td>Tokens minted for another API</td><td>On</td></tr><tr><td>IssuerSigningKey</td><td>Forged or edited tokens</td><td>Required</td></tr><tr><td>ValidateLifetime</td><td>Expired or not-yet-valid tokens</td><td>On</td></tr><tr><td>ClockSkew</td><td>How late an expired token still passes</td><td>5 minutes</td></tr></tbody></table></figure>



<p class="wp-block-paragraph"><code>MapInboundClaims = false</code> keeps short claim names like <code>role</code> instead of the long WS-Federation URIs, so <code>NameClaimType</code> and <code>RoleClaimType</code> point at the names I issue.</p>



<p class="wp-block-paragraph">Since ASP.NET Core 8, the handler validates with the newer <code>JsonWebTokenHandler</code> by default (Microsoft quotes about 30% faster). Set <code>UseSecurityTokenValidators = true</code> only if old code casts to <code>JwtSecurityToken</code>.</p>



<p class="wp-block-paragraph">The explicit <code>app.UseAuthentication()</code> and <code>app.UseAuthorization()</code> calls are optional on .NET 10, because <code>WebApplication</code> adds both when the services exist. I keep them so the order is visible, and the Troubleshooting section shows what happens when it&#8217;s wrong.</p>



<p class="wp-block-paragraph"><strong>Checkpoint:</strong> the API starts and logs the Development environment on port 5735:</p>



<pre class="wp-block-code"><code>Building...
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5735
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: C:\DotNetDemo\JwtAuthDemo\PayrollApi</code></pre>



<h2 class="wp-block-heading">Step 4: Generate the JWT Token in a /login Endpoint</h2>



<p class="wp-block-paragraph">This is the generate-a-JWT-token part. I use <code>JsonWebTokenHandler.CreateToken</code> with a <code>SecurityTokenDescriptor</code>. The older <code>JwtSecurityTokenHandler</code> from System.IdentityModel.Tokens.Jwt still works, but it&#8217;s the previous generation:</p>



<pre class="wp-block-code"><code>app.MapPost(&quot;/login&quot;, (LoginRequest login) =&gt;
{
    var user = DemoUsers.Find(login.Username, login.Password);
    if (user is null) return Results.Unauthorized();

    var expires = DateTime.UtcNow.AddMinutes(jwt.GetValue&lt;double&gt;(&quot;AccessTokenMinutes&quot;));
    var token = new JsonWebTokenHandler().CreateToken(new SecurityTokenDescriptor
    {
        Issuer = jwt[&quot;Issuer&quot;],
        Audience = jwt[&quot;Audience&quot;],
        Expires = expires,
        Subject = new ClaimsIdentity(
        [
            new Claim(JwtRegisteredClaimNames.Sub, user.Username),
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
            new Claim(&quot;name&quot;, user.FullName),
            new Claim(&quot;department&quot;, user.Department),
            .. user.Roles.Select(role =&gt; new Claim(&quot;role&quot;, role))
        ]),
        SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256)
    });
    return Results.Ok(new TokenResponse(token, expires));
});</code></pre>



<p class="wp-block-paragraph">Each token gets a <a href="https://aspdotnethelp.com/how-to-create-a-guid-in-c-net/">GUID</a> as its <code>jti</code> (token ID), one <code>role</code> claim per role, and a 15-minute expiry from configuration. The request and response types are <a href="https://aspdotnethelp.com/csharp-record-vs-class-vs-struct/">C# records</a>.</p>



<p class="wp-block-paragraph">Calling it with Tyler&#8217;s demo login returns the token (I cut it to 24 characters here) and its expiry:</p>



<pre class="wp-block-code"><code>curl.exe -s -X POST http://localhost:5735/login -H &quot;Content-Type: application/json&quot; -d &quot;{\&quot;username\&quot;:\&quot;tbrooks\&quot;,\&quot;password\&quot;:\&quot;demo-tyler-2026\&quot;}&quot;</code></pre>



<pre class="wp-block-code"><code>{&quot;accessToken&quot;:&quot;eyJhbGciOiJIUzI1NiIsInR5...&quot;,&quot;expiresAt&quot;:&quot;2026-11-01T13:53:20.2627547Z&quot;}</code></pre>



<p class="wp-block-paragraph"><strong>Checkpoint:</strong> a wrong password returns 401 and no token, and a correct one returns a token starting with <code>eyJ</code>.</p>



<h2 class="wp-block-heading">Step 5: Protect Endpoints with RequireAuthorization, JWT Roles and Claims</h2>



<p class="wp-block-paragraph">Authentication only tells you who&#8217;s calling. Nothing is locked until an endpoint asks for it. Two named policies cover the role rule and the claim rule:</p>



<pre class="wp-block-code"><code>builder.Services.AddAuthorizationBuilder()
    .AddPolicy(&quot;PayrollOnly&quot;, policy =&gt; policy.RequireRole(&quot;Payroll&quot;))
    .AddPolicy(&quot;FinanceOnly&quot;, policy =&gt; policy.RequireClaim(&quot;department&quot;, &quot;Finance&quot;));</code></pre>



<pre class="wp-block-code"><code>app.MapGet(&quot;/me&quot;, (ClaimsPrincipal user) =&gt; new
{
    Name = user.Identity?.Name,
    Department = user.FindFirstValue(&quot;department&quot;),
    Roles = user.FindAll(&quot;role&quot;).Select(r =&gt; r.Value)
}).RequireAuthorization();

app.MapGet(&quot;/payroll&quot;, () =&gt; DemoUsers.Payroll).RequireAuthorization(&quot;PayrollOnly&quot;);

app.MapGet(&quot;/budget&quot;, () =&gt; new { Quarter = &quot;Q4 2026&quot;, Budget = 184_500m })
    .RequireAuthorization(&quot;FinanceOnly&quot;);</code></pre>



<p class="wp-block-paragraph"><code>RequireAuthorization()</code> with no name means any valid token. In controllers, the same rule is <code>[Authorize]</code> or <code>[Authorize(Policy = &quot;PayrollOnly&quot;)]</code>. Here&#8217;s the whole <code>Program.cs</code> at this point:</p>



<pre class="wp-block-code"><code>using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;

var builder = WebApplication.CreateBuilder(args);

var jwt = builder.Configuration.GetSection(&quot;Jwt&quot;);
var keyText = jwt[&quot;SigningKey&quot;]
    ?? throw new InvalidOperationException(&quot;Jwt:SigningKey is missing. Set it with dotnet user-secrets.&quot;);
var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(keyText));
if (signingKey.KeySize &lt; 256)
    throw new InvalidOperationException($&quot;Jwt:SigningKey has {signingKey.KeySize} bits; HS256 needs 256 or more.&quot;);

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =&gt;
    {
        options.MapInboundClaims = false;          // keep &quot;name&quot; and &quot;role&quot; as they are in the token
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = jwt[&quot;Issuer&quot;],
            ValidateAudience = true,
            ValidAudience = jwt[&quot;Audience&quot;],
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = signingKey,
            ValidateLifetime = true,
            ClockSkew = TimeSpan.Zero,             // the default is 5 minutes
            NameClaimType = &quot;name&quot;,
            RoleClaimType = &quot;role&quot;
        };
    });

builder.Services.AddAuthorizationBuilder()
    .AddPolicy(&quot;PayrollOnly&quot;, policy =&gt; policy.RequireRole(&quot;Payroll&quot;))
    .AddPolicy(&quot;FinanceOnly&quot;, policy =&gt; policy.RequireClaim(&quot;department&quot;, &quot;Finance&quot;));

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapGet(&quot;/health&quot;, () =&gt; &quot;OK&quot;);

app.MapPost(&quot;/login&quot;, (LoginRequest login) =&gt;
{
    var user = DemoUsers.Find(login.Username, login.Password);
    if (user is null) return Results.Unauthorized();

    var expires = DateTime.UtcNow.AddMinutes(jwt.GetValue&lt;double&gt;(&quot;AccessTokenMinutes&quot;));
    var token = new JsonWebTokenHandler().CreateToken(new SecurityTokenDescriptor
    {
        Issuer = jwt[&quot;Issuer&quot;],
        Audience = jwt[&quot;Audience&quot;],
        Expires = expires,
        Subject = new ClaimsIdentity(
        [
            new Claim(JwtRegisteredClaimNames.Sub, user.Username),
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
            new Claim(&quot;name&quot;, user.FullName),
            new Claim(&quot;department&quot;, user.Department),
            .. user.Roles.Select(role =&gt; new Claim(&quot;role&quot;, role))
        ]),
        SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256)
    });
    return Results.Ok(new TokenResponse(token, expires));
});

app.MapGet(&quot;/me&quot;, (ClaimsPrincipal user) =&gt; new
{
    Name = user.Identity?.Name,
    Department = user.FindFirstValue(&quot;department&quot;),
    Roles = user.FindAll(&quot;role&quot;).Select(r =&gt; r.Value)
}).RequireAuthorization();

app.MapGet(&quot;/payroll&quot;, () =&gt; DemoUsers.Payroll).RequireAuthorization(&quot;PayrollOnly&quot;);

app.MapGet(&quot;/budget&quot;, () =&gt; new { Quarter = &quot;Q4 2026&quot;, Budget = 184_500m })
    .RequireAuthorization(&quot;FinanceOnly&quot;);

app.Run();

record LoginRequest(string Username, string Password);
record TokenResponse(string AccessToken, DateTime ExpiresAt);
record DemoUser(string Username, string Password, string FullName, string Department, string[] Roles);
record PayrollRow(string Employee, string City, string State, decimal AnnualSalary);

static class DemoUsers
{
    // Demo only. A real app checks hashed passwords (ASP.NET Core Identity) or uses an identity provider.
    static readonly DemoUser[] Users =
    [
        new(&quot;tbrooks&quot;, &quot;demo-tyler-2026&quot;, &quot;Tyler Brooks&quot;, &quot;Operations&quot;, [&quot;Employee&quot;]),
        new(&quot;jmorales&quot;, &quot;demo-jenna-2026&quot;, &quot;Jenna Morales&quot;, &quot;Finance&quot;, [&quot;Employee&quot;, &quot;Payroll&quot;]),
    ];

    public static DemoUser? Find(string username, string password) =&gt;
        Users.FirstOrDefault(u =&gt; u.Username == username &amp;&amp; u.Password == password);

    public static readonly PayrollRow[] Payroll =
    [
        new(&quot;Tyler Brooks&quot;, &quot;Columbus&quot;, &quot;OH&quot;, 58_400m),
        new(&quot;Jenna Morales&quot;, &quot;Austin&quot;, &quot;TX&quot;, 91_250m),
        new(&quot;Rachel Kim&quot;, &quot;Seattle&quot;, &quot;WA&quot;, 104_800m),
    ];
}</code></pre>



<p class="wp-block-paragraph">Calling /me with no token proves the lock and shows the challenge header the HTTP spec requires:</p>



<pre class="wp-block-code"><code>HTTP/1.1 401 Unauthorized
Content-Length: 0
Server: Kestrel
WWW-Authenticate: Bearer</code></pre>



<p class="wp-block-paragraph"><strong>Checkpoint:</strong> no token gives <code>401</code> with <code>WWW-Authenticate: Bearer</code>, and Jenna&#8217;s token on /me returns <code>{"name":"Jenna Morales","department":"Finance","roles":["Employee","Payroll"]}</code>.</p>



<h2 class="wp-block-heading">Step 6: Decode the JWT and Read Its Claims</h2>



<p class="wp-block-paragraph">A JWT is signed, not encrypted. Anyone with the token can read the payload, which this little client proves. It uses <a href="https://aspdotnethelp.com/system-text-json-in-csharp/">System.Text.Json</a> and the <code>Base64Url</code> class from .NET 9:</p>



<pre class="wp-block-code"><code>#:property PublishAot=false
using System.Buffers.Text;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;

var http = new HttpClient { BaseAddress = new Uri(&quot;http://localhost:5735&quot;) };
var login = await http.PostAsJsonAsync(&quot;/login&quot;, new { username = &quot;jmorales&quot;, password = &quot;demo-jenna-2026&quot; });
string token = (await login.Content.ReadFromJsonAsync&lt;JsonElement&gt;()).GetProperty(&quot;accessToken&quot;).GetString()!;

string[] parts = token.Split(&#x27;.&#x27;);
Console.WriteLine($&quot;{parts.Length} parts, {token.Length} characters: {token[..20]}...&quot;);
var pretty = new JsonSerializerOptions { WriteIndented = true };
foreach (var (label, part) in new[] { (&quot;Header&quot;, parts[0]), (&quot;Payload&quot;, parts[1]) })
{
    using var doc = JsonDocument.Parse(Encoding.UTF8.GetString(Base64Url.DecodeFromChars(part)));
    Console.WriteLine($&quot;{label}: {JsonSerializer.Serialize(doc.RootElement, pretty)}&quot;);
}</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>3 parts, 395 characters: eyJhbGciOiJIUzI1NiIs...
Header: {
  &quot;alg&quot;: &quot;HS256&quot;,
  &quot;typ&quot;: &quot;JWT&quot;
}
Payload: {
  &quot;aud&quot;: &quot;payroll-web&quot;,
  &quot;iss&quot;: &quot;payroll-api-demo&quot;,
  &quot;exp&quot;: 1793541200,
  &quot;sub&quot;: &quot;jmorales&quot;,
  &quot;jti&quot;: &quot;dc270880-8f35-4dc4-a159-c811c1751823&quot;,
  &quot;name&quot;: &quot;Jenna Morales&quot;,
  &quot;department&quot;: &quot;Finance&quot;,
  &quot;role&quot;: [
    &quot;Employee&quot;,
    &quot;Payroll&quot;
  ],
  &quot;iat&quot;: 1793540300,
  &quot;nbf&quot;: 1793540300
}</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/decode-jwt-token-header-payload-claims-csharp.png" alt="Decode a JWT token in C#: DecodeJwt.cs in VS Code logs in, splits the token into 3 parts and prints the decoded payload claims exp, sub, jti, name, department, role, iat and nbf" class="wp-image-2755" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/decode-jwt-token-header-payload-claims-csharp.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/decode-jwt-token-header-payload-claims-csharp-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/decode-jwt-token-header-payload-claims-csharp-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/decode-jwt-token-header-payload-claims-csharp-768x674.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">The header and payload of a real token from the /login endpoint</figcaption></figure>
</div>


<p class="wp-block-paragraph">The claims match the descriptor. <code>exp</code>, <code>iat</code> and <code>nbf</code> are Unix seconds, and <code>role</code> became a JSON array because Jenna has two roles. Never put a password, SSN or anything private in a claim.</p>



<p class="wp-block-paragraph"><strong>Checkpoint:</strong> the payload shows <code>&quot;iss&quot;: &quot;payroll-api-demo&quot;</code> and <code>&quot;aud&quot;: &quot;payroll-web&quot;</code>, the same values the API validates.</p>



<h2 class="wp-block-heading">Test the JWT Bearer Authentication with HttpClient</h2>



<p class="wp-block-paragraph">This client logs in both users and calls every endpoint. It&#8217;s plain <a href="https://aspdotnethelp.com/async-await-in-csharp/">async/await in C#</a> with one <code>HttpClient</code>:</p>



<pre class="wp-block-code"><code>#:property PublishAot=false
using System.Net.Http.Json;

var http = new HttpClient { BaseAddress = new Uri(&quot;http://localhost:5735&quot;) };

async Task&lt;string&gt; Login(string user, string pass) =&gt;
    (await (await http.PostAsJsonAsync(&quot;/login&quot;, new { username = user, password = pass }))
        .Content.ReadFromJsonAsync&lt;Token&gt;())!.AccessToken;

async Task Call(string path, string? token, string who)
{
    var req = new HttpRequestMessage(HttpMethod.Get, path);
    if (token is not null) req.Headers.Authorization = new(&quot;Bearer&quot;, token);
    var res = await http.SendAsync(req);
    string hint = string.Join(&quot; &quot;, res.Headers.WwwAuthenticate.Select(h =&gt; h.Scheme));
    Console.WriteLine($&quot;{path,-9}{who,-20}{(int)res.StatusCode} {res.StatusCode} {hint}&quot;);
}

string tyler = await Login(&quot;tbrooks&quot;, &quot;demo-tyler-2026&quot;);
string jenna = await Login(&quot;jmorales&quot;, &quot;demo-jenna-2026&quot;);
await Call(&quot;/health&quot;, null, &quot;no token&quot;);
await Call(&quot;/me&quot;, null, &quot;no token&quot;);
await Call(&quot;/me&quot;, tyler, &quot;Tyler (Employee)&quot;);
await Call(&quot;/payroll&quot;, tyler, &quot;Tyler (Employee)&quot;);
await Call(&quot;/budget&quot;, tyler, &quot;Tyler (Operations)&quot;);
await Call(&quot;/payroll&quot;, jenna, &quot;Jenna (Payroll)&quot;);
await Call(&quot;/budget&quot;, jenna, &quot;Jenna (Finance)&quot;);
await Call(&quot;/me&quot;, tyler[..^4] + &quot;AAAA&quot;, &quot;tampered token&quot;);

record Token(string AccessToken, DateTime ExpiresAt);</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>/health  no token            200 OK 
/me      no token            401 Unauthorized Bearer
/me      Tyler (Employee)    200 OK 
/payroll Tyler (Employee)    403 Forbidden 
/budget  Tyler (Operations)  403 Forbidden 
/payroll Jenna (Payroll)     200 OK 
/budget  Jenna (Finance)     200 OK 
/me      tampered token      401 Unauthorized Bearer</code></pre>



<p class="wp-block-paragraph"><strong>Note:</strong> file-based apps publish as Native AOT by default, which turns off reflection-based JSON. My first run of <code>PostAsJsonAsync</code> died with &#8220;Reflection-based serialization has been disabled&#8221;, so the clients start with <code>#:property PublishAot=false</code>.</p>



<h3 class="wp-block-heading">What each JWT test result means</h3>



<figure class="wp-block-table"><table><thead><tr><th>Call</th><th>Status</th><th>Why</th></tr></thead><tbody><tr><td>/health, no token</td><td>200</td><td>No RequireAuthorization, so it&#8217;s public</td></tr><tr><td>/me, no token</td><td>401</td><td>Challenge with WWW-Authenticate: Bearer</td></tr><tr><td>/payroll, Tyler</td><td>403</td><td>Valid token, but no Payroll role</td></tr><tr><td>/budget, Tyler</td><td>403</td><td>department is Operations, not Finance</td></tr><tr><td>/payroll and /budget, Jenna</td><td>200</td><td>Role and claim both match</td></tr><tr><td>/me, tampered token</td><td>401</td><td>Signature check fails</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">The tampered call changed the last four characters of the signature. The full response header says why:</p>



<pre class="wp-block-code"><code>HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error=&quot;invalid_token&quot;, error_description=&quot;The signature key was not found&quot;</code></pre>



<h3 class="wp-block-heading">Test an expired JWT token (lifetime and ClockSkew)</h3>



<p class="wp-block-paragraph">For this run I started the API with <code>Jwt__AccessTokenMinutes=0.1</code>, a six-second token, and waited eight seconds between calls:</p>



<pre class="wp-block-code"><code>#:property PublishAot=false
using System.Net.Http.Json;

var http = new HttpClient { BaseAddress = new Uri(&quot;http://localhost:5735&quot;) };
var login = await http.PostAsJsonAsync(&quot;/login&quot;, new { username = &quot;tbrooks&quot;, password = &quot;demo-tyler-2026&quot; });
var token = (await login.Content.ReadFromJsonAsync&lt;Token&gt;())!;
http.DefaultRequestHeaders.Authorization = new(&quot;Bearer&quot;, token.AccessToken);
Console.WriteLine($&quot;Token expires at {token.ExpiresAt:HH:mm:ss} UTC&quot;);

for (int i = 0; i &lt; 2; i++)
{
    var res = await http.GetAsync(&quot;/me&quot;);
    Console.WriteLine($&quot;{DateTime.UtcNow:HH:mm:ss}  GET /me -&gt; {(int)res.StatusCode} {res.StatusCode}&quot;);
    foreach (var h in res.Headers.WwwAuthenticate) Console.WriteLine($&quot;  WWW-Authenticate: {h}&quot;);
    if (i == 0) await Task.Delay(TimeSpan.FromSeconds(8));
}

record Token(string AccessToken, DateTime ExpiresAt);</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>Token expires at 13:39:33 UTC
13:39:27  GET /me -&gt; 200 OK
13:39:35  GET /me -&gt; 401 Unauthorized
  WWW-Authenticate: Bearer error=&quot;invalid_token&quot;, error_description=&quot;The token expired at &#x27;11/01/2026 13:39:33&#x27;&quot;</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/jwt-token-expired-401-clockskew-aspnet-core.png" alt="Expired JWT in ASP.NET Core: ExpiryTest.cs output shows GET /me 200 OK, then 401 Unauthorized after the token expired, with WWW-Authenticate Bearer error invalid_token and The token expired at" class="wp-image-2758" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/jwt-token-expired-401-clockskew-aspnet-core.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/jwt-token-expired-401-clockskew-aspnet-core-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/jwt-token-expired-401-clockskew-aspnet-core-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/jwt-token-expired-401-clockskew-aspnet-core-768x674.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">Eight seconds later the same token gets 401 invalid_token</figcaption></figure>
</div>


<p class="wp-block-paragraph">It failed right on time because <code>ClockSkew</code> is zero. With the default five minutes, that token would have kept working for five more minutes.</p>



<h2 class="wp-block-heading">Generate a Dev JWT Token with dotnet user-jwts</h2>



<p class="wp-block-paragraph">Don&#8217;t want a /login endpoint while you build? The <code>dotnet user-jwts</code> tool mints local tokens and keeps its signing key in user secrets. It works when <code>AddJwtBearer()</code> reads its options from configuration, so I used a second small project:</p>



<pre class="wp-block-code"><code>using System.Security.Claims;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(&quot;Bearer&quot;).AddJwtBearer();   // options come from appsettings
builder.Services.AddAuthorization();

var app = builder.Build();

app.MapGet(&quot;/secret&quot;, (ClaimsPrincipal user) =&gt; $&quot;Hello {user.Identity?.Name}, you&#x27;re in.&quot;)
    .RequireAuthorization();
app.MapGet(&quot;/payroll&quot;, () =&gt; &quot;Payroll data for Q4 2026&quot;)
    .RequireAuthorization(p =&gt; p.RequireRole(&quot;Payroll&quot;));

app.Run();</code></pre>



<p class="wp-block-paragraph">It has the same launchSettings.json as PayrollApi. Create a token for Jenna with the Payroll role:</p>



<pre class="wp-block-code"><code>dotnet user-jwts create --name &quot;Jenna Morales&quot; --role Payroll</code></pre>



<pre class="wp-block-code"><code>New JWT saved with ID &#x27;ca875434&#x27;.
Name: Jenna Morales
Roles: [Payroll]

Token: eyJhbGciOiJIUzI1NiIsInR5...</code></pre>



<p class="wp-block-paragraph">The tool also wrote the matching issuer and audience into <code>appsettings.Development.json</code>, and it added a <code>UserSecretsId</code> to the csproj:</p>



<pre class="wp-block-code"><code>{
  &quot;Logging&quot;: {
    &quot;LogLevel&quot;: {
      &quot;Default&quot;: &quot;Information&quot;,
      &quot;Microsoft.AspNetCore&quot;: &quot;Warning&quot;
    }
  },
  &quot;Authentication&quot;: {
    &quot;Schemes&quot;: {
      &quot;Bearer&quot;: {
        &quot;ValidAudiences&quot;: [
          &quot;http://localhost:5735&quot;
        ],
        &quot;ValidIssuer&quot;: &quot;dotnet-user-jwts&quot;
      }
    }
  }
}</code></pre>



<pre class="wp-block-code"><code>&lt;Project Sdk=&quot;Microsoft.NET.Sdk.Web&quot;&gt;

  &lt;PropertyGroup&gt;
    &lt;TargetFramework&gt;net10.0&lt;/TargetFramework&gt;
    &lt;Nullable&gt;enable&lt;/Nullable&gt;
    &lt;ImplicitUsings&gt;enable&lt;/ImplicitUsings&gt;
    &lt;UserSecretsId&gt;1acd2c24-eb31-4a71-a13c-3a7ab23a1a79&lt;/UserSecretsId&gt;
  &lt;/PropertyGroup&gt;

  &lt;ItemGroup&gt;
    &lt;PackageReference Include=&quot;Microsoft.AspNetCore.Authentication.JwtBearer&quot; Version=&quot;10.0.14&quot; /&gt;
  &lt;/ItemGroup&gt;

&lt;/Project&gt;</code></pre>



<p class="wp-block-paragraph"><code>dotnet user-jwts print</code> with the ID shows everything in the token, including the claims and the expiry:</p>



<pre class="wp-block-code"><code>dotnet user-jwts print ca875434 --show-all</code></pre>



<pre class="wp-block-code"><code>Found JWT with ID &#x27;ca875434&#x27;.
ID: ca875434
Name: Jenna Morales
Scheme: Bearer
Audience(s): http://localhost:5735
Not Before: 2026-11-01T13:41:42.0000000+00:00
Expires On: 2027-02-01T13:41:42.0000000+00:00
Issued On: 2026-11-01T13:41:43.0000000+00:00
Scopes: none
Roles: [Payroll]
Custom Claims: [none]
Token Header: {&quot;alg&quot;:&quot;HS256&quot;,&quot;typ&quot;:&quot;JWT&quot;}
Token Payload: {&quot;unique_name&quot;:&quot;Jenna Morales&quot;,&quot;sub&quot;:&quot;Jenna Morales&quot;,&quot;jti&quot;:&quot;ca875434&quot;,&quot;role&quot;:&quot;Payroll&quot;,&quot;aud&quot;:&quot;http://localhost:5735&quot;,&quot;nbf&quot;:1793540502,&quot;exp&quot;:1801489302,&quot;iat&quot;:1793540503,&quot;iss&quot;:&quot;dotnet-user-jwts&quot;}
Compact Token: eyJhbGciOiJIUzI1NiIsInR5...</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/dotnet-user-jwts-create-print-token-aspnet-core.png" alt="dotnet user-jwts print --show-all in the VS Code terminal shows the JWT for Jenna Morales with role Payroll, audience http://localhost:5735, issuer dotnet-user-jwts and the token header and payload" class="wp-image-2756" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/dotnet-user-jwts-create-print-token-aspnet-core.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/dotnet-user-jwts-create-print-token-aspnet-core-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/dotnet-user-jwts-create-print-token-aspnet-core-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/dotnet-user-jwts-create-print-token-aspnet-core-768x674.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">dotnet user-jwts print shows the token&#8217;s claims, audience and expiry</figcaption></figure>
</div>


<p class="wp-block-paragraph">With that token, <code>/secret</code> answered <code>Hello Jenna Morales, you're in.</code> and <code>/payroll</code> returned 200. Without it, <code>/secret</code> returned 401. My token expires three months out, as the tool&#8217;s help says.</p>



<h2 class="wp-block-heading">JWT Refresh Tokens: What They Do (Not Built Here)</h2>



<p class="wp-block-paragraph">A short access token is safer, but nobody wants to sign in every 15 minutes. A refresh token is a long random value, stored on the server, that the client trades for a new access token.</p>



<p class="wp-block-paragraph">The server rotates it on every use, revokes it on sign-out, and never puts it in browser storage. That&#8217;s real work, which is why I&#8217;d lean on ASP.NET Core Identity or an identity provider before writing my own.</p>



<h2 class="wp-block-heading">ASP.NET Core Identity vs JWT, and Cookie vs JWT: Which One Do You Need?</h2>



<p class="wp-block-paragraph">These aren&#8217;t really rivals. Identity stores users and passwords, while cookies and JWTs are two ways to carry the signed-in user on each request:</p>



<figure class="wp-block-table"><table><thead><tr><th>Option</th><th>Best for</th><th>Watch out for</th></tr></thead><tbody><tr><td>Cookie authentication</td><td>Server-rendered sites, Blazor, a backend for your SPA</td><td>CSRF protection, same-site only</td></tr><tr><td>JWT bearer</td><td>APIs called by mobile apps, other services, other domains</td><td>Can&#8217;t be revoked before it expires</td></tr><tr><td>ASP.NET Core Identity</td><td>Storing users, hashed passwords, MFA</td><td>It&#8217;s a user store, not a token format</td></tr><tr><td>Identity provider (Entra ID, Auth0)</td><td>Production sign-in with OAuth and OpenID Connect</td><td>Setup and cost</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Identity can issue bearer tokens too, through <code>MapIdentityApi</code>. If a code-based sign-in fits your users better than passwords, my <a href="https://aspdotnethelp.com/generate-otp-asp-net/">OTP sign-in example in ASP.NET Core</a> covers that half.</p>



<h2 class="wp-block-heading">Make the ASP.NET Core JWT Setup Production-Ready</h2>



<ul class="wp-block-list">
<li><strong>HTTPS only.</strong> A bearer token works like a password until it expires, so never send it over plain HTTP outside localhost.</li>

<li><strong>Short lifetimes.</strong> 5 to 15 minutes for access tokens, with ClockSkew of 30 seconds or less.</li>

<li><strong>Keep the key out of the repo.</strong> User secrets in development, Key Vault or an environment variable in production, and rotate it.</li>

<li><strong>Store tokens safely.</strong> Don&#8217;t keep them in browser localStorage; use a backend with an HttpOnly cookie for browser apps.</li>

<li><strong>Use an identity provider for real users.</strong> Microsoft recommends asymmetric keys and OAuth or OpenID Connect flows over issuing tokens from a username and password.</li>
</ul>



<p class="wp-block-paragraph">Want a token box in your API docs? My <a href="https://aspdotnethelp.com/openapi-scalar-aspnet-core/">OpenAPI and Scalar guide for ASP.NET Core</a> adds the Bearer security scheme so you can paste a JWT and try the locked endpoints.</p>



<h2 class="wp-block-heading">Troubleshooting ASP.NET Core JWT Authentication Errors</h2>



<p class="wp-block-paragraph"><strong>IDX10720: the key size must be greater than &#8216;256&#8217; bits.</strong> Your HS256 key is shorter than 32 bytes. Generate a new one as in Step 2.</p>



<p class="wp-block-paragraph"><strong>A valid token still gets 401.</strong> I swapped the two middleware lines so UseAuthorization ran first, and Tyler&#8217;s good token on /me returned 401. Authentication must run before authorization.</p>



<p class="wp-block-paragraph"><strong>Everyone gets in.</strong> The endpoint has no <code>RequireAuthorization</code>, exactly like /health, which returned 200 with no token. To lock every endpoint by default, set a fallback policy with <code>SetFallbackPolicy</code>.</p>



<p class="wp-block-paragraph"><strong>Roles never match, so every call is 403.</strong> With <code>MapInboundClaims = false</code> you must set <code>RoleClaimType = &quot;role&quot;</code>. Leave mapping on and the handler renames <code>role</code> to the long ClaimTypes.Role URI instead.</p>



<p class="wp-block-paragraph"><strong>Expired tokens still work for a few minutes.</strong> That&#8217;s the 5-minute default <code>ClockSkew</code>. Lower it, as in Step 3.</p>



<p class="wp-block-paragraph"><strong>dotnet user-jwts says it could not find appsettings.Development.json.</strong> I hit this after deleting that file. The tool edits it rather than creating it, so put back a file with just the Logging section.</p>



<p class="wp-block-paragraph"><strong>Note:</strong> a 401 with no <code>error_description</code> usually means no token arrived at all. Check the header is exactly <code>Bearer</code> plus a space, and wrap client calls in a <a href="https://aspdotnethelp.com/try-catch-in-csharp/">try-catch block</a> while you debug.</p>



<h2 class="wp-block-heading">Where to Take It Next with JWT in ASP.NET Core</h2>



<p class="wp-block-paragraph">Swap the in-memory payroll list for a database with the <a href="https://aspdotnethelp.com/linq-in-csharp/">LINQ queries</a> you already know, and move the demo users into ASP.NET Core Identity. Then put an identity provider in front of the API when real customers sign in.</p>



<p class="wp-block-paragraph">Every API here targets .NET 10. The <a href="https://aspdotnethelp.com/whats-new-in-dotnet-10/">What is new in .NET 10 roundup</a> lists the other runtime and ASP.NET Core changes worth knowing.</p>



<p class="wp-block-paragraph">A few more pages that fit this one:</p>



<ul class="wp-block-list">
<li><a href="https://aspdotnethelp.com/asp-net-core-tutorials/">Every ASP.NET Core tutorial on AspDotnetHelp</a></li>

<li><a href="https://aspdotnethelp.com/upgrade-dotnet-8-to-dotnet-10/">Move an older API from .NET 8 to .NET 10</a></li>

<li><a href="https://aspdotnethelp.com/dotnet-8-9-end-of-support/">.NET 8 and .NET 9 end-of-support dates</a></li>

<li><a href="https://aspdotnethelp.com/interface-in-csharp/">Interfaces in C# for a token service</a></li>
</ul>



<p class="wp-block-paragraph">JWT only works when UseAuthentication runs before UseAuthorization, and that order trips up a lot of people. I explain the whole pipeline, including the recommended order and how to write your own, in <a href="https://aspdotnethelp.com/aspnet-core-middleware/">middleware in ASP.NET Core</a>.</p>



<h2 class="wp-block-heading">Frequently Asked Questions</h2>


<div id="rank-math-faq" class="rank-math-block">
<div class="rank-math-list ">
<div id="faq-question-jwt0000001" class="rank-math-list-item">
<h3 class="rank-math-question ">How do I generate a JWT token in .NET?</h3>
<div class="rank-math-answer ">

<p>Build a <code>SecurityTokenDescriptor</code> with the issuer, audience, expiry, claims and <code>SigningCredentials</code>, then call <code>new JsonWebTokenHandler().CreateToken(descriptor)</code>. Use a key of at least 256 bits for HS256.</p>

</div>
</div>
<div id="faq-question-jwt0000002" class="rank-math-list-item">
<h3 class="rank-math-question ">How do I validate a JWT token manually in C#?</h3>
<div class="rank-math-answer ">

<p>Call <code>JsonWebTokenHandler.ValidateTokenAsync(token, parameters)</code> with the same <code>TokenValidationParameters</code> the API uses, then check <code>result.IsValid</code> and <code>result.Exception</code>. In an API, let AddJwtBearer do it for you.</p>

</div>
</div>
<div id="faq-question-jwt0000003" class="rank-math-list-item">
<h3 class="rank-math-question ">What is the difference between 401 and 403 with JWT?</h3>
<div class="rank-math-answer ">

<p>401 means the token is missing or failed validation (signature, issuer, audience or expiry). 403 means the token is valid but the user lacks the role or claim the policy needs.</p>

</div>
</div>
<div id="faq-question-jwt0000004" class="rank-math-list-item">
<h3 class="rank-math-question ">Where should I store the JWT signing key?</h3>
<div class="rank-math-answer ">

<p>Use dotnet user-secrets during development and Azure Key Vault or an environment variable in production. Never commit it to appsettings.json or source control.</p>

</div>
</div>
<div id="faq-question-jwt0000005" class="rank-math-list-item">
<h3 class="rank-math-question ">Is a JWT encrypted?</h3>
<div class="rank-math-answer ">

<p>No. A signed JWT is only Base64Url-encoded, so anyone holding it can read the claims. The signature stops edits, not reading, so keep secrets out of the payload.</p>

</div>
</div>
<div id="faq-question-jwt0000006" class="rank-math-list-item">
<h3 class="rank-math-question ">Should I issue my own JWTs in production?</h3>
<div class="rank-math-answer ">

<p>Microsoft advises against creating access tokens from a username and password request and recommends OpenID Connect or OAuth flows with asymmetric keys. See <a href="https://learn.microsoft.com/en-us/aspnet/core/security/authentication/configure-jwt-bearer-authentication?view=aspnetcore-10.0" target="_blank" rel="noopener">Configure JWT bearer authentication on Microsoft Learn</a>.</p>

</div>
</div>
</div>
</div>]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Record vs Class vs Struct in C#: Differences and When to Use Each (.NET 10)</title>
		<link>https://aspdotnethelp.com/csharp-record-vs-class-vs-struct/</link>
		
		<dc:creator><![CDATA[Bijay Kumar]]></dc:creator>
		<pubDate>Sat, 07 Nov 2026 15:15:00 +0000</pubDate>
				<category><![CDATA[C#]]></category>
		<guid isPermaLink="false">https://aspdotnethelp.com/?p=2734</guid>

					<description><![CDATA[If you&#8217;re modeling a shipping address or an API payload, here&#8217;s my rule for C# record vs class: use a record when two objects with the same values should be equal. Use a class for objects with identity and changing state, and a struct only for tiny values. I ran all three on one Austin, ... <a title="Record vs Class vs Struct in C#: Differences and When to Use Each (.NET 10)" class="read-more" href="https://aspdotnethelp.com/csharp-record-vs-class-vs-struct/" aria-label="Read more about Record vs Class vs Struct in C#: Differences and When to Use Each (.NET 10)">Read more &#62;&#62;</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">If you&#8217;re modeling a shipping address or an API payload, here&#8217;s my rule for <strong>C# record vs class</strong>: use a record when two objects with the same values should be equal.</p>



<p class="wp-block-paragraph">Use a class for objects with identity and changing state, and a struct only for tiny values.</p>



<p class="wp-block-paragraph">I ran all three on one Austin, TX address on .NET 10. The class called two identical addresses unequal, the record and struct didn&#8217;t, and a million structs allocated 15.3 MB versus 38.1 MB.</p>



<h2 class="wp-block-heading">Record vs Class vs Struct in C# at a Glance</h2>



<p class="wp-block-paragraph">A <code>record</code> isn&#8217;t a third kind of type. It&#8217;s a modifier: <code>record</code> (or <code>record class</code>) is a reference type, and <code>record struct</code> is a value type. The compiler adds value equality, <code>ToString</code>, <code>with</code> and deconstruction on top.</p>



<figure class="wp-block-table"><table><thead><tr><th>Feature</th><th>record (class)</th><th>class</th><th>struct</th></tr></thead><tbody><tr><td>Kind of type</td><td>Reference</td><td>Reference</td><td>Value</td></tr><tr><td><code>==</code> on two identical addresses (my run)</td><td>True</td><td>False</td><td>Doesn&#8217;t compile (CS0019)</td></tr><tr><td><code>Equals</code> on the same two</td><td>True</td><td>False</td><td>True (reflection-based)</td></tr><tr><td>Positional syntax + <code>Deconstruct</code></td><td>Generated</td><td>No</td><td>Only as <code>record struct</code></td></tr><tr><td><code>ToString()</code> output</td><td>Type name + every property</td><td>Type name only</td><td>Type name only</td></tr><tr><td><code>with</code> expressions</td><td>Yes</td><td>No</td><td>Yes (C# 10+)</td></tr><tr><td>Inheritance</td><td>From another record only</td><td>Yes</td><td>No</td></tr><tr><td>Copy on assignment</td><td>Reference</td><td>Reference</td><td>Whole value (16 bytes here)</td></tr><tr><td>EF Core entity</td><td>No</td><td>Yes</td><td>No</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Every test below uses one sample: a shipping address in Austin, TX 78701, declared four ways. If plain structs are new to you, start with <a href="https://aspdotnethelp.com/structure-in-csharp/">structures in C#</a>.</p>



<h2 class="wp-block-heading">Record vs Class Equality: Does == Compare Values?</h2>



<p class="wp-block-paragraph">Only on records. Here are two identical addresses per type, compared with == and Equals:</p>



<pre class="wp-block-code"><code>var c1 = new AddressClass(&quot;Austin&quot;, &quot;TX&quot;, &quot;78701&quot;);
var c2 = new AddressClass(&quot;Austin&quot;, &quot;TX&quot;, &quot;78701&quot;);
var r1 = new AddressRecord(&quot;Austin&quot;, &quot;TX&quot;, &quot;78701&quot;);
var r2 = new AddressRecord(&quot;Austin&quot;, &quot;TX&quot;, &quot;78701&quot;);
var s1 = new AddressStruct(&quot;Austin&quot;, &quot;TX&quot;, &quot;78701&quot;);
var s2 = new AddressStruct(&quot;Austin&quot;, &quot;TX&quot;, &quot;78701&quot;);
var rs1 = new AddressRecordStruct(&quot;Austin&quot;, &quot;TX&quot;, &quot;78701&quot;);
var rs2 = new AddressRecordStruct(&quot;Austin&quot;, &quot;TX&quot;, &quot;78701&quot;);

Console.WriteLine($&quot;class         == {c1 == c2,-5}  Equals {c1.Equals(c2)}&quot;);
Console.WriteLine($&quot;record        == {r1 == r2,-5}  Equals {r1.Equals(r2)}&quot;);
Console.WriteLine($&quot;struct        == n/a    Equals {s1.Equals(s2)}&quot;);
Console.WriteLine($&quot;record struct == {rs1 == rs2,-5}  Equals {rs1.Equals(rs2)}&quot;);

class AddressClass(string city, string state, string zip)
{
    public string City =&gt; city; public string State =&gt; state; public string Zip =&gt; zip;
}
record AddressRecord(string City, string State, string Zip);
struct AddressStruct(string city, string state, string zip)
{
    public string City = city, State = state, Zip = zip;
}
record struct AddressRecordStruct(string City, string State, string Zip);</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>class         == False  Equals False
record        == True   Equals True
struct        == n/a    Equals True
record struct == True   Equals True</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-record-vs-class-vs-struct-equality-vscode.png" alt="VS Code running Equality.cs: two identical Austin TX addresses compare False as a class and True as a record, struct and record struct" class="wp-image-2740" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-record-vs-class-vs-struct-equality-vscode.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-record-vs-class-vs-struct-equality-vscode-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-record-vs-class-vs-struct-equality-vscode-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-record-vs-class-vs-struct-equality-vscode-768x674.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">Same address values, four types: only the class says they&#8217;re different</figcaption></figure>
</div>


<p class="wp-block-paragraph">The class printed False because it compares references, and c1 and c2 are two objects. The record compared City, State and Zip and printed True.</p>



<p class="wp-block-paragraph">The plain struct&#8217;s <code>Equals</code> also said True, but <code>==</code> doesn&#8217;t exist on a struct until you write it, and <code>ValueType.Equals</code> uses reflection. A <code>record struct</code> gets a compiler-generated <code>==</code> and <code>IEquatable&lt;T&gt;</code> for free.</p>



<h2 class="wp-block-heading">Record Class vs Record Struct vs Readonly Record Struct</h2>



<p class="wp-block-paragraph">Same address values, now as the three record flavors. Watch what assignment copies and what you&#8217;re allowed to change:</p>



<pre class="wp-block-code"><code>var rc = new AddressRecord(&quot;Austin&quot;, &quot;TX&quot;, &quot;78701&quot;);
var rcAlias = rc;                       // copies the reference
Console.WriteLine($&quot;record class:    same object {ReferenceEquals(rc, rcAlias)}&quot;);

var rs = new AddressRecordStruct(&quot;Austin&quot;, &quot;TX&quot;, &quot;78701&quot;);
var rsCopy = rs;                        // copies the whole value
rsCopy.City = &quot;Dallas&quot;;                 // allowed: positional props are get; set;
Console.WriteLine($&quot;record struct:   {rs.City} / {rsCopy.City}&quot;);

var ro = new AddressReadonly(&quot;Austin&quot;, &quot;TX&quot;, &quot;78701&quot;);
// ro.City = &quot;Dallas&quot;;                  // CS8852: init-only property
Console.WriteLine($&quot;readonly record struct: {ro}&quot;);

record AddressRecord(string City, string State, string Zip);
record struct AddressRecordStruct(string City, string State, string Zip);
readonly record struct AddressReadonly(string City, string State, string Zip);</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>record class:    same object True
record struct:   Austin / Dallas
readonly record struct: AddressReadonly { City = Austin, State = TX, Zip = 78701 }</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-record-class-vs-record-struct-vs-readonly-record-struct.png" alt="VS Code running RecordKinds.cs: a record class assignment shares the object, a record struct copy can change City to Dallas, a readonly record struct is init-only" class="wp-image-2739" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-record-class-vs-record-struct-vs-readonly-record-struct.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-record-class-vs-record-struct-vs-readonly-record-struct-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-record-class-vs-record-struct-vs-readonly-record-struct-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-record-class-vs-record-struct-vs-readonly-record-struct-768x674.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">record struct properties are settable; readonly record struct and record class are init-only</figcaption></figure>
</div>


<p class="wp-block-paragraph">Assigning the record class copied the reference, so both variables point at one object. Assigning the record struct copied the value, so changing the copy to Dallas left the original in Austin.</p>



<p class="wp-block-paragraph">Positional properties are <code>init</code>-only in a record class and a <code>readonly record struct</code> (the <code>ro.City</code> line fails with CS8852), but read-write in a plain <code>record struct</code>. I default to <code>readonly record struct</code>.</p>



<h2 class="wp-block-heading">Record ToString Output and Positional Deconstruction</h2>



<p class="wp-block-paragraph">Positional syntax (<code>record AddressRecord(string City, string State, string Zip)</code>) writes the <a href="https://aspdotnethelp.com/constructor-in-csharp/">primary constructor</a>, the properties and a <code>Deconstruct</code> method for you:</p>



<pre class="wp-block-code"><code>var home = new AddressRecord(&quot;Austin&quot;, &quot;TX&quot;, &quot;78701&quot;);
Console.WriteLine(home);
Console.WriteLine(new AddressClass { City = &quot;Austin&quot; });

var (city, state, zip) = home;          // positional records get Deconstruct
Console.WriteLine($&quot;{city}, {state} {zip}&quot;);

record AddressRecord(string City, string State, string Zip);
class AddressClass { public string? City; }</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>AddressRecord { City = Austin, State = TX, Zip = 78701 }
AddressClass
Austin, TX 78701</code></pre>



<p class="wp-block-paragraph">The record printed every property in <code>Type { Name = Value }</code> form; the class printed only its type name, so you&#8217;d <a href="https://aspdotnethelp.com/tostring-in-csharp/">override ToString in C#</a> yourself for readable logs.</p>



<h2 class="wp-block-heading">With Expressions: Copying a Record (and the Shallow Copy Gotcha)</h2>



<p class="wp-block-paragraph">Records are meant to be changed by copying. A <code>with</code> expression clones the record and sets only the properties you list. Same address values, plus a shipment that holds a list:</p>



<pre class="wp-block-code"><code>var home = new AddressRecord(&quot;Austin&quot;, &quot;TX&quot;, &quot;78701&quot;);
var office = home with { City = &quot;Dallas&quot;, Zip = &quot;75201&quot; };
Console.WriteLine(home);
Console.WriteLine(office);
Console.WriteLine($&quot;Equal: {home == office}&quot;);

var order = new Shipment(1001, [&quot;Tent&quot;, &quot;Lantern&quot;]);
var reship = order with { Id = 1002 };          // shallow copy
reship.Items.Add(&quot;Cooler&quot;);
Console.WriteLine($&quot;Order 1001 items: {string.Join(&quot;, &quot;, order.Items)}&quot;);

var safe = order with { Id = 1003, Items = [.. order.Items] };
safe.Items.Add(&quot;Stove&quot;);
Console.WriteLine($&quot;Order 1001 items: {order.Items.Count}, 1003: {safe.Items.Count}&quot;);

record AddressRecord(string City, string State, string Zip);
record Shipment(int Id, List&lt;string&gt; Items);</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>AddressRecord { City = Austin, State = TX, Zip = 78701 }
AddressRecord { City = Dallas, State = TX, Zip = 75201 }
Equal: False
Order 1001 items: Tent, Lantern, Cooler
Order 1001 items: 3, 1003: 4</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-record-with-expression-shallow-copy-list.png" alt="VS Code running WithExpression.cs: with creates a Dallas copy of the Austin address, and adding Cooler to the copied shipment also changes order 1001" class="wp-image-2743" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-record-with-expression-shallow-copy-list.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-record-with-expression-shallow-copy-list-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-record-with-expression-shallow-copy-list-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-record-with-expression-shallow-copy-list-768x674.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">with is a shallow copy: both shipments shared one list until I copied it</figcaption></figure>
</div>


<p class="wp-block-paragraph">The Dallas copy left Austin alone. The shipment is the trap: order 1001 suddenly had a Cooler it never ordered.</p>



<p class="wp-block-paragraph"><code>with</code> copies references, not the objects behind them, so both records shared one <code>List&lt;string&gt;</code>. Copying the list in the same expression (<code>Items = [.. order.Items]</code>) gave order 1003 its own four items.</p>



<h2 class="wp-block-heading">Record Inheritance: Records Can&#8217;t Inherit From Classes</h2>



<p class="wp-block-paragraph">Record classes support <a href="https://aspdotnethelp.com/inheritance-in-csharp/">inheritance in C#</a>, but only with other records. Same Austin data, now with a business address that adds a company name:</p>



<pre class="wp-block-code"><code>Address home = new Address(&quot;Austin&quot;, &quot;TX&quot;, &quot;78701&quot;);
Address dock = new BusinessAddress(&quot;Austin&quot;, &quot;TX&quot;, &quot;78701&quot;, &quot;Lone Star Outfitters&quot;);

Console.WriteLine(dock);
Console.WriteLine($&quot;Same values, different types: {home == dock}&quot;);
Console.WriteLine($&quot;dock is Address: {dock is Address}&quot;);

record Address(string City, string State, string Zip);
record BusinessAddress(string City, string State, string Zip, string Company)
    : Address(City, State, Zip);</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>BusinessAddress { City = Austin, State = TX, Zip = 78701, Company = Lone Star Outfitters }
Same values, different types: False
dock is Address: True</code></pre>



<p class="wp-block-paragraph">The derived <code>ToString</code> included the base properties. Equality also checks the runtime type, so two addresses with the same city, state and ZIP weren&#8217;t equal.</p>



<p class="wp-block-paragraph">A record can&#8217;t inherit from a class, and a class can&#8217;t inherit from a record. I tried it and got CS8864: Records may only inherit from object or another record. Structs and record structs can&#8217;t inherit at all.</p>



<h2 class="wp-block-heading">Record vs Class vs Struct Performance and Memory</h2>



<p class="wp-block-paragraph">This test fills an array with 1,000,000 addresses per type. <code>GC.GetAllocatedBytesForCurrentThread</code> counts the bytes and <code>Stopwatch</code> the time:</p>



<pre class="wp-block-code"><code>using System.Diagnostics;
using System.Runtime.CompilerServices;

const int N = 1_000_000;
Console.WriteLine($&quot;struct size: {Unsafe.SizeOf&lt;AddrStruct&gt;()} bytes&quot;);
Run(&quot;class &quot;, () =&gt; { var a = new AddrClass[N]; for (int i = 0; i &lt; N; i++) a[i] = new(&quot;Austin&quot;, i); });
Run(&quot;record&quot;, () =&gt; { var a = new AddrRecord[N]; for (int i = 0; i &lt; N; i++) a[i] = new(&quot;Austin&quot;, i); });
Run(&quot;struct&quot;, () =&gt; { var a = new AddrStruct[N]; for (int i = 0; i &lt; N; i++) a[i] = new(&quot;Austin&quot;, i); });
Run(&quot;boxed &quot;, () =&gt; { var a = new object[N]; for (int i = 0; i &lt; N; i++) a[i] = new AddrStruct(&quot;Austin&quot;, i); });

static void Run(string name, Action fill)
{
    fill();                                           // warm-up
    long start = GC.GetAllocatedBytesForCurrentThread();
    var sw = Stopwatch.StartNew(); fill();
    double mb = (GC.GetAllocatedBytesForCurrentThread() - start) / 1048576.0;
    Console.WriteLine($&quot;{name}  {mb,6:F1} MB  {sw.ElapsedMilliseconds,3} ms&quot;);
}
class AddrClass(string city, int zip) { public string City = city; public int Zip = zip; }
record AddrRecord(string City, int Zip);
readonly record struct AddrStruct(string City, int Zip);</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>struct size: 16 bytes
class     38.1 MB  173 ms
record    38.1 MB  179 ms
struct    15.3 MB   76 ms
boxed     38.1 MB  175 ms</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1084" height="952" src="https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-record-vs-class-vs-struct-performance-memory.png" alt="VS Code running MemoryCost.cs: one million addresses allocate 38.1 MB as a class or record, 15.3 MB as a struct and 38.1 MB when boxed" class="wp-image-2742" srcset="https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-record-vs-class-vs-struct-performance-memory.png 1084w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-record-vs-class-vs-struct-performance-memory-300x263.png 300w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-record-vs-class-vs-struct-performance-memory-1024x899.png 1024w, https://aspdotnethelp.com/wp-content/uploads/2026/11/csharp-record-vs-class-vs-struct-performance-memory-768x674.png 768w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">A separate run in VS Code: the MB match, the milliseconds shift from run to run</figcaption></figure>
</div>


<p class="wp-block-paragraph">Class and record allocated the same 38.1 MB, so a record costs nothing extra (the millisecond gap between them changes every run). The struct array took 15.3 MB in 76 ms, because the values sit inline with no object headers.</p>



<p class="wp-block-paragraph">Storing the same structs as <code>object</code> boxed every one and allocated 38.1 MB in 175 ms. That&#8217;s why <a href="https://aspdotnethelp.com/boxing-and-unboxing-in-csharp/">boxing and unboxing</a> matters when you pick a struct.</p>



<p class="wp-block-paragraph">This struct is 16 bytes, and every assignment or method call copies all of it, where a class copies an 8-byte reference. Microsoft&#8217;s guideline keeps structs under 16 bytes and immutable, so this one is right at the line.</p>



<h2 class="wp-block-heading">Records as DTOs: ASP.NET Core and System.Text.Json</h2>



<p class="wp-block-paragraph">Records are my default for request and response bodies. ASP.NET Core binds JSON with <a href="https://aspdotnethelp.com/system-text-json-in-csharp/">System.Text.Json</a>, which calls the record&#8217;s constructor by parameter name:</p>



<pre class="wp-block-code"><code>#:property PublishAot=false
using System.Text.Json;

var web = new JsonSerializerOptions(JsonSerializerDefaults.Web);
var dto = new AddressDto(&quot;Austin&quot;, &quot;TX&quot;, &quot;78701&quot;);

string json = JsonSerializer.Serialize(dto, web);
Console.WriteLine(json);

var back = JsonSerializer.Deserialize&lt;AddressDto&gt;(json, web);
Console.WriteLine(back);
Console.WriteLine($&quot;Round trip equal: {dto == back}&quot;);

record AddressDto(string City, string State, string Zip);</code></pre>



<p class="wp-block-paragraph"><strong>Output:</strong></p>



<pre class="wp-block-code"><code>{&quot;city&quot;:&quot;Austin&quot;,&quot;state&quot;:&quot;TX&quot;,&quot;zip&quot;:&quot;78701&quot;}
AddressDto { City = Austin, State = TX, Zip = 78701 }
Round trip equal: True</code></pre>



<p class="wp-block-paragraph">The record serialized to <code>{"city":"Austin","state":"TX","zip":"78701"}</code> and came back equal to the original, which also makes DTOs easy to assert in tests.</p>



<p class="wp-block-paragraph"><strong>Important:</strong> file-based apps publish as native AOT by default, which turns off reflection-based JSON. The <code>#:property PublishAot=false</code> line switches that off.</p>



<h2 class="wp-block-heading">Should EF Core Entities Be Records or Classes?</h2>



<p class="wp-block-paragraph">Classes. Microsoft Learn says EF Core relies on reference equality to keep one instance per entity, so records and record structs aren&#8217;t appropriate as entity types. EF Core also doesn&#8217;t support updating immutable entities.</p>



<p class="wp-block-paragraph">In my <a href="https://aspdotnethelp.com/aspnet-core-web-api-crud-ef-core/">ASP.NET Core Web API CRUD with EF Core</a> build, the entity is a class and the API contracts are records.</p>



<h2 class="wp-block-heading">When to Use a Record, a Class or a Struct</h2>



<figure class="wp-block-table"><table><thead><tr><th>Situation</th><th>Pick</th></tr></thead><tbody><tr><td>DTO, API request/response, message, config snapshot</td><td><code>record</code></td></tr><tr><td>Object with identity and changing state (customer, cart, service)</td><td><code>class</code></td></tr><tr><td>EF Core entity</td><td><code>class</code></td></tr><tr><td>Small value under 16 bytes (point, money, coordinates)</td><td><code>readonly record struct</code></td></tr><tr><td>Value type you mutate in a tight loop</td><td><code>struct</code> or <code>record struct</code></td></tr><tr><td>Throwaway grouping inside one method</td><td>a tuple</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">For that last row, a <a href="https://aspdotnethelp.com/tuple-in-csharp/">tuple in C#</a> is lighter than any declared type. Once the data leaves the method, I promote it to a record.</p>



<p class="wp-block-paragraph">Classes compare identity, records compare values, structs copy values. Pick by how equality and copying should behave; a record and a class cost the same.</p>



<p class="wp-block-paragraph">Keep going with these:</p>



<ul class="wp-block-list">
<li><a href="https://aspdotnethelp.com/linq-in-csharp/">Query lists of records with LINQ</a></li>

<li><a href="https://aspdotnethelp.com/abstract-class-in-csharp/">Abstract classes for shared behavior</a></li>

<li><a href="https://aspdotnethelp.com/whats-new-in-csharp-14/">New C# 14 features for your types</a></li>

<li><a href="https://aspdotnethelp.com/csharp-tutorials/">All C# tutorials on aspdotnethelp.com</a></li>
</ul>



<p class="wp-block-paragraph">Records also shine with positional patterns, since you can deconstruct them right inside a switch expression. I show that, along with property and list patterns, in <a href="https://aspdotnethelp.com/csharp-pattern-matching/">pattern matching in C# with is and switch expressions</a>.</p>



<h2 class="wp-block-heading">Frequently Asked Questions</h2>


<div id="rank-math-faq" class="rank-math-block">
<div class="rank-math-list ">
<div id="faq-question-rc0000001" class="rank-math-list-item">
<h3 class="rank-math-question ">Are C# records immutable?</h3>
<div class="rank-math-answer ">

<p>Positional properties are init-only in a record class and a readonly record struct, but read-write in a record struct. The immutability is shallow: a List property can still be changed.</p>

</div>
</div>
<div id="faq-question-rc0000002" class="rank-math-list-item">
<h3 class="rank-math-question ">Is a record slower than a class?</h3>
<div class="rank-math-answer ">

<p>Not to create: 1,000,000 of each allocated 38.1 MB. Equals and GetHashCode compare every property, so they cost more than a reference check.</p>

</div>
</div>
<div id="faq-question-rc0000003" class="rank-math-list-item">
<h3 class="rank-math-question ">Can a record inherit from a class?</h3>
<div class="rank-math-answer ">

<p>No. A record can inherit only from another record (or object), and a class can&#8217;t inherit from a record; the compiler reports CS8864.</p>

</div>
</div>
<div id="faq-question-rc0000004" class="rank-math-list-item">
<h3 class="rank-math-question ">When should I use a record instead of a class?</h3>
<div class="rank-math-answer ">

<p>Use a record for data whose equality depends on its values: DTOs, messages, settings. See the <a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record" target="_blank" rel="noopener">records reference on Microsoft Learn</a> for every generated member.</p>

</div>
</div>
<div id="faq-question-rc0000005" class="rank-math-list-item">
<h3 class="rank-math-question ">Should I use a struct or a record struct?</h3>
<div class="rank-math-answer ">

<p>A record struct, unless you need full control. It adds ==, IEquatable and ToString without reflection. Keep it small; Microsoft&#8217;s <a href="https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/choosing-between-class-and-struct" target="_blank" rel="noopener">class vs struct guideline</a> suggests under 16 bytes.</p>

</div>
</div>
</div>
</div>]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>

<!--
Performance optimized by W3 Total Cache. Learn more: https://www.boldgrid.com/w3-total-cache/?utm_source=w3tc&utm_medium=footer_comment&utm_campaign=free_plugin

Page Caching using Disk: Enhanced 

Served from: aspdotnethelp.com @ 2026-11-11 22:50:14 by W3 Total Cache
-->