﻿<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
  <channel xmlns:media="http://search.yahoo.com/mrss/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
    <title>Dev Leader</title>
    <link>https://www.devleader.ca/</link>
    <description />
    <atom:link href="https://www.devleader.ca/feed" rel="self" type="application/rss+xml" />
    <item>
      <guid isPermaLink="false">d0a3a457-3db8-40b6-bb6f-db24cecfa096</guid>
      <link>https://www.devleader.ca/2026/06/05/error-handling-in-aspnet-core-web-api-problem-details-and-global-handlers</link>
      <category>asp.net core error handling</category>
      <category>problem details</category>
      <category>global exception handler</category>
      <category>aspnet core exceptions</category>
      <title>Error Handling in ASP.NET Core Web API: Problem Details and Global Handlers</title>
      <pubDate>Fri, 05 Jun 2026 21:00:00 Z</pubDate>
      <description><![CDATA[<h1 id="error-handling-in-asp.net-core-web-api-problem-details-and-global-handlers">Error Handling in ASP.NET Core Web API: Problem Details and Global Handlers</h1>
<p><strong>ASP.NET Core error handling</strong> done well means your API clients never receive a mystery 500 response with no actionable information. Every error -- whether a validation failure, a domain rule violation, or an unexpected crash -- ideally comes back as a structured, consistent response that developers can actually parse and act on. This matters more than most teams realize until they're debugging a production incident and the API just returns <code>Internal Server Error</code> with an empty body.</p>
<p>This guide covers the full picture for .NET 10: the Problem Details standard, built-in middleware, the modern <code>IExceptionHandler</code> interface, and how to layer everything into a production-ready <strong>asp.net core error handling</strong> strategy that scales with your application.</p>
<h2 id="the-problem-details-standard-rfc-9457">The Problem Details Standard (RFC 9457)</h2>
<p>The HTTP ecosystem has long suffered from inconsistent error formats. Every API did its own thing -- some returned <code>{ &quot;error&quot;: &quot;...&quot; }</code>, others <code>{ &quot;message&quot;: &quot;...&quot; }</code>, and plenty returned HTML error pages to JSON clients. RFC 9457 (which superseded RFC 7807) standardizes this once and for all. It defines the <code>application/problem+json</code> content type and a set of well-known fields that error responses commonly include.</p>
<p>The core fields are worth understanding before you write a line of code. The <code>type</code> field is a URI that identifies the error type -- it can be a real URL pointing to documentation, or the placeholder <code>about:blank</code>. The <code>title</code> gives a human-readable summary of the problem type, not the specific occurrence. The <code>status</code> mirrors the HTTP status code. The <code>detail</code> field provides specific information about this particular occurrence, meant to help the developer debug without exposing sensitive internals. The <code>instance</code> is a URI that identifies the specific occurrence -- usually the request path.</p>
<p>In .NET 10, enabling Problem Details support is a single method call. The framework provides <code>AddProblemDetails()</code>, and the middleware hooks in automatically when you call <code>UseExceptionHandler()</code> without arguments. Here is the minimal setup:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails(); // Enables RFC 9457 compliant responses

var app = builder.Build();

// UseExceptionHandler() with no path uses ProblemDetails when AddProblemDetails is registered
app.UseExceptionHandler();

// Converts bare status codes (404 from routing, 405 Method Not Allowed, etc.) to ProblemDetails
app.UseStatusCodePages();

app.MapControllers();
app.Run();
</code></pre>
</div>
<p>When an unhandled exception occurs, this combination produces a Problem Details response by default -- the exact shape can still vary based on environment settings and any customization you apply:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-json">{
  &quot;type&quot;: &quot;https://tools.ietf.org/html/rfc9457&quot;,
  &quot;title&quot;: &quot;An error occurred while processing your request.&quot;,
  &quot;status&quot;: 500,
  &quot;detail&quot;: null,
  &quot;instance&quot;: &quot;/api/orders/42&quot;,
  &quot;traceId&quot;: &quot;00-a1b2c3d4e5f6-b7c8d9e0f1a2-00&quot;
}
</code></pre>
</div>
<blockquote class="blockquote">
<p><strong>Note on <code>type</code> URIs:</strong> In production, consider using your own domain for type URIs (e.g., <code>&quot;https://api.yourdomain.com/errors/internal-error&quot;</code>) rather than pointing directly at the RFC URI. This lets you host human-readable documentation at that URL and gives clients a stable, API-specific reference. The <code>tools.ietf.org</code> URIs shown in examples follow the RFC 9457 pattern (<code>https://tools.ietf.org/html/rfc9457</code>).</p>
</blockquote>
<p>The <code>traceId</code> is automatically added by ASP.NET Core in .NET 10. It is invaluable for correlating errors across distributed systems, and you get it for free.</p>
<h2 id="useexceptionhandler-the-built-in-global-middleware">UseExceptionHandler: The Built-in Global Middleware</h2>
<p><code>UseExceptionHandler</code> is the workhorse of <strong>asp.net core error handling</strong>. It wraps the entire downstream pipeline in a try/catch, catches any unhandled exceptions, and gives you a controlled opportunity to produce a proper response. The simplest form passes a path -- ASP.NET Core re-executes the request at that path to generate the error response. The more powerful form, and the one preferred in .NET 10, uses a lambda that runs inline.</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">app.UseExceptionHandler(exceptionHandlerApp =&gt;
{
    exceptionHandlerApp.Run(async context =&gt;
    {
        var exceptionHandlerFeature = context.Features.Get&lt;IExceptionHandlerFeature&gt;();
        var exception = exceptionHandlerFeature?.Error;

        var status = exception switch
        {
            NotFoundException =&gt; StatusCodes.Status404NotFound,
            UnauthorizedAccessException =&gt; StatusCodes.Status403Forbidden,
            _ =&gt; StatusCodes.Status500InternalServerError
        };

        var problemDetails = new ProblemDetails
        {
            Status = status,
            Title = status == 500 ? &quot;An unexpected error occurred&quot; : exception?.Message,
            Detail = status == 500 ? null : exception?.Message,
            Instance = context.Request.Path
        };

        problemDetails.Extensions[&quot;traceId&quot;] =
            Activity.Current?.Id ?? context.TraceIdentifier;

        context.Response.StatusCode = status;
        context.Response.ContentType = &quot;application/problem+json&quot;;

        await context.Response.WriteAsJsonAsync(problemDetails);
    });
});
</code></pre>
</div>
<p>This approach works well for small applications. But as your domain grows, the exception type switching inside the lambda becomes unwieldy. A wall of <code>if/else</code> or a <code>switch</code> with dozens of cases is hard to maintain and impossible to test in isolation. That is exactly the problem <code>IExceptionHandler</code> solves.</p>
<h2 id="iexceptionhandler-the-di-friendly-approach.net-8">IExceptionHandler: The DI-Friendly Approach (.NET 8+)</h2>
<p><code>IExceptionHandler</code> was introduced in .NET 8 and is a modern, convenient option for <strong>asp.net core error handling</strong> in .NET 10. You implement the interface in a class, register it with DI, and ASP.NET Core calls your handlers in registration order. The first handler that returns <code>true</code> wins -- all others are skipped. Multiple handlers chain together, each responsible for a specific exception type.</p>
<p>This is structurally identical to <a href="https://www.devleader.ca/2026/05/25/chain-of-responsibility-design-pattern-in-c-complete-guide-with-examples">the Chain of Responsibility pattern</a> -- each handler in the chain decides whether to handle the request or pass it along. The pattern keeps each handler focused, independently testable, and easy to add or remove.</p>
<p>Here is a complete example with a domain-specific handler and a generic fallback:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">// Handles domain-specific exceptions
public sealed class DomainExceptionHandler : IExceptionHandler
{
    private readonly ILogger&lt;DomainExceptionHandler&gt; _logger;

    public DomainExceptionHandler(ILogger&lt;DomainExceptionHandler&gt; logger)
    {
        _logger = logger;
    }

    public async ValueTask&lt;bool&gt; TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        if (exception is not DomainException domainException)
        {
            return false; // Not our concern -- let the next handler try
        }

        _logger.LogWarning(
            domainException,
            &quot;Domain rule violated: {ErrorCode} -- {Message}&quot;,
            domainException.ErrorCode,
            domainException.Message);

        var problemDetails = new ProblemDetails
        {
            Status = StatusCodes.Status422UnprocessableEntity,
            Title = &quot;Business rule violation&quot;,
            Detail = domainException.Message,
            Type = &quot;https://api.example.com/errors/domain&quot;
        };

        problemDetails.Extensions[&quot;errorCode&quot;] = domainException.ErrorCode;
        problemDetails.Extensions[&quot;traceId&quot;] =
            Activity.Current?.Id ?? httpContext.TraceIdentifier;

        httpContext.Response.StatusCode = StatusCodes.Status422UnprocessableEntity;
        await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);

        return true; // Handled
    }
}

// Catches everything else
public sealed class GlobalExceptionHandler : IExceptionHandler
{
    private readonly ILogger&lt;GlobalExceptionHandler&gt; _logger;

    public GlobalExceptionHandler(ILogger&lt;GlobalExceptionHandler&gt; logger)
    {
        _logger = logger;
    }

    public async ValueTask&lt;bool&gt; TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        _logger.LogError(
            exception,
            &quot;Unhandled exception: {ExceptionType} on {Method} {Path}&quot;,
            exception.GetType().Name,
            httpContext.Request.Method,
            httpContext.Request.Path);

        var problemDetails = new ProblemDetails
        {
            Status = StatusCodes.Status500InternalServerError,
            Title = &quot;An unexpected error occurred&quot;,
            Type = &quot;https://tools.ietf.org/html/rfc9457&quot;
        };

        problemDetails.Extensions[&quot;traceId&quot;] =
            Activity.Current?.Id ?? httpContext.TraceIdentifier;

        httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
        await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);

        return true;
    }
}
</code></pre>
</div>
<p>Registering both in <code>Program.cs</code>:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">// Register in the order they should be tried
builder.Services.AddExceptionHandler&lt;DomainExceptionHandler&gt;();
builder.Services.AddExceptionHandler&lt;GlobalExceptionHandler&gt;();
builder.Services.AddProblemDetails();

// In the pipeline -- no path argument needed
app.UseExceptionHandler();
</code></pre>
</div>
<p>The registration order is meaningful. <code>DomainExceptionHandler</code> is evaluated first because it was registered first. Only if it returns <code>false</code> does <code>GlobalExceptionHandler</code> get a chance. This is clean, predictable, and mirrors how <a href="https://www.devleader.ca/2026/06/06/mediator-design-pattern-in-c-complete-guide-with-examples">the Mediator pattern</a> dispatches requests to specific handlers -- each handler knows exactly what it is responsible for.</p>
<h2 id="customizing-problem-details-globally">Customizing Problem Details Globally</h2>
<p>Sometimes you want to add custom fields to every problem details response -- not just exception responses, but also 404s, 401s, and validation errors from <code>[ApiController]</code>. The <code>CustomizeProblemDetails</code> callback runs for all of them.</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">builder.Services.AddProblemDetails(options =&gt;
{
    options.CustomizeProblemDetails = context =&gt;
    {
        // Add traceId to every problem response
        context.ProblemDetails.Extensions[&quot;traceId&quot;] =
            Activity.Current?.Id ?? context.HttpContext.TraceIdentifier;

        // Add server node info (helpful in multi-node deployments)
        context.ProblemDetails.Extensions[&quot;nodeId&quot;] = Environment.MachineName;

        // ISO 8601 timestamp for client-side correlation
        context.ProblemDetails.Extensions[&quot;timestamp&quot;] =
            DateTimeOffset.UtcNow.ToString(&quot;o&quot;);
    };
});
</code></pre>
</div>
<p>This approach gives you a uniform response shape across your entire API. Every error -- whether produced by routing, authentication, validation, or your own exception handlers -- will include these fields. Client applications can write a single error-handling routine that works everywhere.</p>
<p>In .NET 10, <code>ProblemDetailsContext</code> gained a <code>SuppressDiagnosticsCallback</code> property, giving you finer control over when diagnostics events are suppressed for handled exceptions. This is useful when you want to suppress diagnostic telemetry for expected exceptions (such as validation errors or not-found responses) while still recording them for unexpected errors.</p>
<p>This technique is conceptually similar to what the <a href="https://www.devleader.ca/2026/06/02/chain-of-responsibility-vs-decorator-pattern-in-c-key-differences-explained">Decorator pattern</a> describes in object-oriented terms: you are wrapping the base behavior (producing a problem details response) with additional cross-cutting concerns (adding trace IDs, timestamps) without modifying the core logic.</p>
<h2 id="validation-error-responses-with-apicontroller">Validation Error Responses with [ApiController]</h2>
<p>The <code>[ApiController]</code> attribute automates a critical part of <strong>asp.net core error handling</strong> for you. When model validation fails, it short-circuits execution before your action method runs and returns a 400 Bad Request with a <code>ValidationProblemDetails</code> body. You never need to write <code>if (!ModelState.IsValid)</code> checks in API controller actions.</p>
<p>The automatic validation response looks like this:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-json">{
  &quot;type&quot;: &quot;https://tools.ietf.org/html/rfc9457&quot;,
  &quot;title&quot;: &quot;One or more validation errors occurred.&quot;,
  &quot;status&quot;: 400,
  &quot;errors&quot;: {
    &quot;Email&quot;: [&quot;The Email field is not a valid e-mail address.&quot;],
    &quot;Name&quot;: [&quot;The Name field is required.&quot;]
  },
  &quot;traceId&quot;: &quot;00-a1b2c3...&quot;
}
</code></pre>
</div>
<p>To customize this response -- for example to return 422 instead of 400 for semantic validation failures, or to add your custom extension fields -- replace <code>InvalidModelStateResponseFactory</code>:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">builder.Services.Configure&lt;ApiBehaviorOptions&gt;(options =&gt;
{
    options.InvalidModelStateResponseFactory = context =&gt;
    {
        var problemDetails = new ValidationProblemDetails(context.ModelState)
        {
            Type = &quot;https://api.example.com/errors/validation&quot;,
            Title = &quot;Validation failed&quot;,
            Status = StatusCodes.Status422UnprocessableEntity
        };

        problemDetails.Extensions[&quot;traceId&quot;] =
            context.HttpContext.TraceIdentifier;

        return new UnprocessableEntityObjectResult(problemDetails)
        {
            ContentTypes = { &quot;application/problem+json&quot; }
        };
    };
});
</code></pre>
</div>
<p>The choice between 400 and 422 depends on how you classify the error and what semantics you want clients to rely on. Both are valid; the key is choosing deliberately and applying consistently. Use 400 for requests with bad syntax or structure, 422 for semantically invalid requests. Either way, document your convention in your API spec so consumers know what to expect.</p>
<h2 id="exception-filters-vs-iexceptionhandler">Exception Filters vs IExceptionHandler</h2>
<p>These two mechanisms both catch exceptions, but they operate at different levels. Understanding the distinction helps you choose the right tool for each scenario.</p>
<p>Exception filters (<code>IExceptionFilter</code>, <code>IAsyncExceptionFilter</code>) are MVC-scoped. They only catch exceptions thrown inside MVC action methods -- not in middleware, not in minimal API handlers, not in background services. You can apply them globally, per-controller, or per-action via attributes, which gives you fine-grained control. They are a reasonable fit when specific exceptions should map to specific responses only within certain controllers.</p>
<p><code>IExceptionHandler</code> is pipeline-wide. It catches anything that bubbles up through the entire middleware stack -- controllers, minimal APIs, and even exceptions in other middleware. In .NET 10, <code>IExceptionHandler</code> is the commonly preferred approach unless you have a very specific reason to scope error handling to individual actions. For pure Web APIs, there is rarely such a reason.</p>
<p>If you are familiar with <a href="https://www.devleader.ca/2026/05/13/proxy-design-pattern-in-c-complete-guide-with-examples">the Proxy pattern</a>, you can think of <code>IExceptionHandler</code> as a pipeline-level proxy that intercepts every request and translates exception language into HTTP language -- a clean separation of concerns.</p>
<h2 id="logging-errors-properly">Logging Errors Properly</h2>
<p><strong>ASP.NET Core error handling</strong> and structured logging are inseparable. Every unhandled exception ideally produces a structured log entry with enough context to diagnose the problem later. The <a href="https://www.devleader.ca/2026/07/07/how-to-set-up-serilog-in-aspnet-core-step-by-step-guide">How to Set Up Serilog in ASP.NET Core guide</a> covers Serilog setup in depth, and the <a href="https://www.devleader.ca/2026/07/05/serilog-in-net-complete-guide-to-structured-logging">Serilog in .NET complete guide</a> covers structured logging patterns -- but a few error-handling-specific practices are worth emphasizing here.</p>
<p>Log the full exception object, not just <code>exception.Message</code>. The message omits the stack trace, inner exceptions, and contextual data that makes the difference between a 5-minute fix and a 2-hour investigation. Use the overload that accepts an <code>Exception</code> as the first parameter:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">_logger.LogError(
    exception,
    &quot;Order processing failed for OrderId {OrderId} -- {ExceptionType}&quot;,
    orderId,
    exception.GetType().Name);
</code></pre>
</div>
<p>Avoid logging and re-throwing the same exception at multiple layers. Log once, at the outermost catch point -- your global exception handler. Multiple log entries for a single exception create noise and make correlation harder. Let the structured log entry carry the full context, including the trace ID.</p>
<h2 id="development-vs-production-errors">Development vs Production Errors</h2>
<p>One of the most practical aspects of <strong>asp.net core error handling</strong> is environment-based configuration. In development, you want full exception details -- stack traces, inner exceptions, query strings. In production, you want the opposite: structured Problem Details with no sensitive internals exposed.</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">if (app.Environment.IsDevelopment())
{
    // Full stack traces and exception details -- never enable in production
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler(); // Clean ProblemDetails via your IExceptionHandler chain
    app.UseHsts();
}
</code></pre>
</div>
<p><code>UseDeveloperExceptionPage</code> returns a rich HTML page (or detailed JSON for API requests that send <code>Accept: application/json</code>) with the full exception details. It is invaluable during development. But in production, stack traces and internal file paths can reveal exploitable information about your system. Ensure <code>ASPNETCORE_ENVIRONMENT</code> is set to <code>Production</code> in your deployment environment, and never conditionally enable developer pages based on anything other than <code>IsDevelopment()</code>.</p>
<h2 id="frequently-asked-questions">Frequently Asked Questions</h2>
<h3 id="what-is-the-difference-between-useexceptionhandler-and-iexceptionhandler-in-asp.net-core">What is the difference between UseExceptionHandler and IExceptionHandler in ASP.NET Core?</h3>
<p><code>UseExceptionHandler</code> is the middleware component you register in the request pipeline. It wraps the downstream pipeline in a try/catch and catches unhandled exceptions. When registered with no arguments and <code>AddProblemDetails</code> has been called, it produces a Problem Details response by default -- the exact shape can still vary based on environment settings and any customization you apply.</p>
<p><code>IExceptionHandler</code> is an interface you implement in your own classes. These implementations are called by <code>UseExceptionHandler</code> when an exception occurs. Multiple handlers can be registered, and they are tried in order -- the first one to return <code>true</code> handles the exception and subsequent handlers are skipped. Think of <code>UseExceptionHandler</code> as the outer shell and <code>IExceptionHandler</code> implementations as the pluggable logic running inside it.</p>
<p>In .NET 10, the commonly preferred approach is to use both together: <code>UseExceptionHandler()</code> in the pipeline and one or more <code>IExceptionHandler</code> implementations registered with <code>AddExceptionHandler&lt;T&gt;()</code>.</p>
<h3 id="should-validation-errors-return-http-400-or-http-422">Should validation errors return HTTP 400 or HTTP 422?</h3>
<p>Both status codes are defensible, and you will find respected APIs using each. RFC 9110 defines 400 Bad Request as appropriate when the server cannot process the request due to a client error -- which includes invalid syntax, missing required fields, and values outside allowed ranges. RFC 9110 defines 422 Unprocessable Content as appropriate when the request is syntactically correct but cannot be processed due to semantic errors.</p>
<p>The practical difference: use 400 for structural problems (missing required field, wrong data type) and 422 for business rule violations (a date range where the end is before the start, an order quantity that exceeds inventory). ASP.NET Core returns 400 by default for model validation failures. You can override this with <code>InvalidModelStateResponseFactory</code> if your team prefers 422. The most important thing is consistency -- pick a convention and document it in your API spec.</p>
<h3 id="how-do-i-add-custom-fields-like-errorcode-to-every-problem-details-response">How do I add custom fields like errorCode to every Problem Details response?</h3>
<p>Use the <code>CustomizeProblemDetails</code> callback in <code>AddProblemDetails()</code>. This callback receives every problem details response before it is written -- including automatic validation errors, routing 404s, and responses from your <code>IExceptionHandler</code> implementations. You can add any key-value pair to <code>ProblemDetails.Extensions</code> and it will be serialized as an additional field in the JSON response.</p>
<p>For exception-specific data like an <code>errorCode</code> that only makes sense when a domain exception is thrown, add the extension in your <code>IExceptionHandler</code> implementation instead. That way the field only appears when the relevant exception type is caught, rather than appearing (with a null or default value) on every response.</p>
<h3 id="how-do-i-handle-notfoundexception-to-return-404-instead-of-500">How do I handle NotFoundException to return 404 instead of 500?</h3>
<p>Create a custom exception class (e.g., <code>NotFoundException : Exception</code>) and implement an <code>IExceptionHandler</code> that checks for it. When the handler receives a <code>NotFoundException</code>, it sets the HTTP status code to 404, writes a problem details body, and returns <code>true</code>.</p>
<p>A cleaner pattern is to add a <code>StatusCode</code> property to a base exception class and have a single <code>IExceptionHandler</code> read it. Your <code>NotFoundException</code> sets <code>StatusCode = 404</code>, your <code>ConflictException</code> sets <code>StatusCode = 409</code>, and so on. The handler maps exception status codes to HTTP status codes without knowing anything about specific exception types. This makes adding new exception types purely additive -- no handler changes needed.</p>
<h3 id="can-i-test-iexceptionhandler-implementations-in-isolation">Can I test IExceptionHandler implementations in isolation?</h3>
<p>Yes, and this is one of the main reasons to prefer <code>IExceptionHandler</code> over inline lambda handlers. Construct your handler, create a <code>DefaultHttpContext</code> with a mock response stream, call <code>TryHandleAsync</code> with a test exception, and assert the response status code and body.</p>
<p>For integration tests, use <code>WebApplicationFactory&lt;Program&gt;</code> to start a real test server and configure endpoints that deliberately throw specific exceptions. Then call those endpoints with an <code>HttpClient</code> and assert the full response -- status code, content type, and parsed JSON body. Integration tests are especially valuable for verifying that your <code>AddProblemDetails</code> customizations apply consistently across all error paths.</p>
<h3 id="what-happens-if-an-iexceptionhandler-implementation-throws-an-exception-itself">What happens if an IExceptionHandler implementation throws an exception itself?</h3>
<p>If your <code>IExceptionHandler</code> throws while trying to handle an exception, ASP.NET Core falls back to its built-in behavior. If <code>AddProblemDetails</code> is registered, the framework will attempt to produce a generic 500 problem details response. If it cannot do that either, it produces a bare 500 with no body.</p>
<p>Your exception handlers need to be defensive. Avoid calling services that could fail -- no database calls, no external HTTP calls, nothing that might throw. If you absolutely need external data in your error handler, wrap it in a try/catch. The error handling layer is not the place for complex orchestration.</p>
<h3 id="how-does-asp.net-core-error-handling-interact-with-middleware-short-circuiting">How does asp.net core error handling interact with middleware short-circuiting?</h3>
<p><code>UseExceptionHandler</code> only catches exceptions that propagate through the pipeline. Middleware that short-circuits by writing a response and not calling <code>next()</code> does not trigger <code>UseExceptionHandler</code> -- it bypasses it entirely. This is intentional. Health check endpoints, rate limiters, and cached response middleware all need to short-circuit without triggering exception handling.</p>
<p>The implication is that middleware registered before <code>UseExceptionHandler</code> in the pipeline is outside the error handling safety net. Keep middleware that might produce errors after <code>UseExceptionHandler</code> in the registration order, which is why the recommended pipeline ordering usually puts <code>UseExceptionHandler</code> first.</p>
]]></description>
      <content:encoded><![CDATA[<h1 id="error-handling-in-asp.net-core-web-api-problem-details-and-global-handlers">Error Handling in ASP.NET Core Web API: Problem Details and Global Handlers</h1>
<p><strong>ASP.NET Core error handling</strong> done well means your API clients never receive a mystery 500 response with no actionable information. Every error -- whether a validation failure, a domain rule violation, or an unexpected crash -- ideally comes back as a structured, consistent response that developers can actually parse and act on. This matters more than most teams realize until they're debugging a production incident and the API just returns <code>Internal Server Error</code> with an empty body.</p>
<p>This guide covers the full picture for .NET 10: the Problem Details standard, built-in middleware, the modern <code>IExceptionHandler</code> interface, and how to layer everything into a production-ready <strong>asp.net core error handling</strong> strategy that scales with your application.</p>
<h2 id="the-problem-details-standard-rfc-9457">The Problem Details Standard (RFC 9457)</h2>
<p>The HTTP ecosystem has long suffered from inconsistent error formats. Every API did its own thing -- some returned <code>{ &quot;error&quot;: &quot;...&quot; }</code>, others <code>{ &quot;message&quot;: &quot;...&quot; }</code>, and plenty returned HTML error pages to JSON clients. RFC 9457 (which superseded RFC 7807) standardizes this once and for all. It defines the <code>application/problem+json</code> content type and a set of well-known fields that error responses commonly include.</p>
<p>The core fields are worth understanding before you write a line of code. The <code>type</code> field is a URI that identifies the error type -- it can be a real URL pointing to documentation, or the placeholder <code>about:blank</code>. The <code>title</code> gives a human-readable summary of the problem type, not the specific occurrence. The <code>status</code> mirrors the HTTP status code. The <code>detail</code> field provides specific information about this particular occurrence, meant to help the developer debug without exposing sensitive internals. The <code>instance</code> is a URI that identifies the specific occurrence -- usually the request path.</p>
<p>In .NET 10, enabling Problem Details support is a single method call. The framework provides <code>AddProblemDetails()</code>, and the middleware hooks in automatically when you call <code>UseExceptionHandler()</code> without arguments. Here is the minimal setup:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails(); // Enables RFC 9457 compliant responses

var app = builder.Build();

// UseExceptionHandler() with no path uses ProblemDetails when AddProblemDetails is registered
app.UseExceptionHandler();

// Converts bare status codes (404 from routing, 405 Method Not Allowed, etc.) to ProblemDetails
app.UseStatusCodePages();

app.MapControllers();
app.Run();
</code></pre>
</div>
<p>When an unhandled exception occurs, this combination produces a Problem Details response by default -- the exact shape can still vary based on environment settings and any customization you apply:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-json">{
  &quot;type&quot;: &quot;https://tools.ietf.org/html/rfc9457&quot;,
  &quot;title&quot;: &quot;An error occurred while processing your request.&quot;,
  &quot;status&quot;: 500,
  &quot;detail&quot;: null,
  &quot;instance&quot;: &quot;/api/orders/42&quot;,
  &quot;traceId&quot;: &quot;00-a1b2c3d4e5f6-b7c8d9e0f1a2-00&quot;
}
</code></pre>
</div>
<blockquote class="blockquote">
<p><strong>Note on <code>type</code> URIs:</strong> In production, consider using your own domain for type URIs (e.g., <code>&quot;https://api.yourdomain.com/errors/internal-error&quot;</code>) rather than pointing directly at the RFC URI. This lets you host human-readable documentation at that URL and gives clients a stable, API-specific reference. The <code>tools.ietf.org</code> URIs shown in examples follow the RFC 9457 pattern (<code>https://tools.ietf.org/html/rfc9457</code>).</p>
</blockquote>
<p>The <code>traceId</code> is automatically added by ASP.NET Core in .NET 10. It is invaluable for correlating errors across distributed systems, and you get it for free.</p>
<h2 id="useexceptionhandler-the-built-in-global-middleware">UseExceptionHandler: The Built-in Global Middleware</h2>
<p><code>UseExceptionHandler</code> is the workhorse of <strong>asp.net core error handling</strong>. It wraps the entire downstream pipeline in a try/catch, catches any unhandled exceptions, and gives you a controlled opportunity to produce a proper response. The simplest form passes a path -- ASP.NET Core re-executes the request at that path to generate the error response. The more powerful form, and the one preferred in .NET 10, uses a lambda that runs inline.</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">app.UseExceptionHandler(exceptionHandlerApp =&gt;
{
    exceptionHandlerApp.Run(async context =&gt;
    {
        var exceptionHandlerFeature = context.Features.Get&lt;IExceptionHandlerFeature&gt;();
        var exception = exceptionHandlerFeature?.Error;

        var status = exception switch
        {
            NotFoundException =&gt; StatusCodes.Status404NotFound,
            UnauthorizedAccessException =&gt; StatusCodes.Status403Forbidden,
            _ =&gt; StatusCodes.Status500InternalServerError
        };

        var problemDetails = new ProblemDetails
        {
            Status = status,
            Title = status == 500 ? &quot;An unexpected error occurred&quot; : exception?.Message,
            Detail = status == 500 ? null : exception?.Message,
            Instance = context.Request.Path
        };

        problemDetails.Extensions[&quot;traceId&quot;] =
            Activity.Current?.Id ?? context.TraceIdentifier;

        context.Response.StatusCode = status;
        context.Response.ContentType = &quot;application/problem+json&quot;;

        await context.Response.WriteAsJsonAsync(problemDetails);
    });
});
</code></pre>
</div>
<p>This approach works well for small applications. But as your domain grows, the exception type switching inside the lambda becomes unwieldy. A wall of <code>if/else</code> or a <code>switch</code> with dozens of cases is hard to maintain and impossible to test in isolation. That is exactly the problem <code>IExceptionHandler</code> solves.</p>
<h2 id="iexceptionhandler-the-di-friendly-approach.net-8">IExceptionHandler: The DI-Friendly Approach (.NET 8+)</h2>
<p><code>IExceptionHandler</code> was introduced in .NET 8 and is a modern, convenient option for <strong>asp.net core error handling</strong> in .NET 10. You implement the interface in a class, register it with DI, and ASP.NET Core calls your handlers in registration order. The first handler that returns <code>true</code> wins -- all others are skipped. Multiple handlers chain together, each responsible for a specific exception type.</p>
<p>This is structurally identical to <a href="https://www.devleader.ca/2026/05/25/chain-of-responsibility-design-pattern-in-c-complete-guide-with-examples">the Chain of Responsibility pattern</a> -- each handler in the chain decides whether to handle the request or pass it along. The pattern keeps each handler focused, independently testable, and easy to add or remove.</p>
<p>Here is a complete example with a domain-specific handler and a generic fallback:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">// Handles domain-specific exceptions
public sealed class DomainExceptionHandler : IExceptionHandler
{
    private readonly ILogger&lt;DomainExceptionHandler&gt; _logger;

    public DomainExceptionHandler(ILogger&lt;DomainExceptionHandler&gt; logger)
    {
        _logger = logger;
    }

    public async ValueTask&lt;bool&gt; TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        if (exception is not DomainException domainException)
        {
            return false; // Not our concern -- let the next handler try
        }

        _logger.LogWarning(
            domainException,
            &quot;Domain rule violated: {ErrorCode} -- {Message}&quot;,
            domainException.ErrorCode,
            domainException.Message);

        var problemDetails = new ProblemDetails
        {
            Status = StatusCodes.Status422UnprocessableEntity,
            Title = &quot;Business rule violation&quot;,
            Detail = domainException.Message,
            Type = &quot;https://api.example.com/errors/domain&quot;
        };

        problemDetails.Extensions[&quot;errorCode&quot;] = domainException.ErrorCode;
        problemDetails.Extensions[&quot;traceId&quot;] =
            Activity.Current?.Id ?? httpContext.TraceIdentifier;

        httpContext.Response.StatusCode = StatusCodes.Status422UnprocessableEntity;
        await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);

        return true; // Handled
    }
}

// Catches everything else
public sealed class GlobalExceptionHandler : IExceptionHandler
{
    private readonly ILogger&lt;GlobalExceptionHandler&gt; _logger;

    public GlobalExceptionHandler(ILogger&lt;GlobalExceptionHandler&gt; logger)
    {
        _logger = logger;
    }

    public async ValueTask&lt;bool&gt; TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        _logger.LogError(
            exception,
            &quot;Unhandled exception: {ExceptionType} on {Method} {Path}&quot;,
            exception.GetType().Name,
            httpContext.Request.Method,
            httpContext.Request.Path);

        var problemDetails = new ProblemDetails
        {
            Status = StatusCodes.Status500InternalServerError,
            Title = &quot;An unexpected error occurred&quot;,
            Type = &quot;https://tools.ietf.org/html/rfc9457&quot;
        };

        problemDetails.Extensions[&quot;traceId&quot;] =
            Activity.Current?.Id ?? httpContext.TraceIdentifier;

        httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
        await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);

        return true;
    }
}
</code></pre>
</div>
<p>Registering both in <code>Program.cs</code>:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">// Register in the order they should be tried
builder.Services.AddExceptionHandler&lt;DomainExceptionHandler&gt;();
builder.Services.AddExceptionHandler&lt;GlobalExceptionHandler&gt;();
builder.Services.AddProblemDetails();

// In the pipeline -- no path argument needed
app.UseExceptionHandler();
</code></pre>
</div>
<p>The registration order is meaningful. <code>DomainExceptionHandler</code> is evaluated first because it was registered first. Only if it returns <code>false</code> does <code>GlobalExceptionHandler</code> get a chance. This is clean, predictable, and mirrors how <a href="https://www.devleader.ca/2026/06/06/mediator-design-pattern-in-c-complete-guide-with-examples">the Mediator pattern</a> dispatches requests to specific handlers -- each handler knows exactly what it is responsible for.</p>
<h2 id="customizing-problem-details-globally">Customizing Problem Details Globally</h2>
<p>Sometimes you want to add custom fields to every problem details response -- not just exception responses, but also 404s, 401s, and validation errors from <code>[ApiController]</code>. The <code>CustomizeProblemDetails</code> callback runs for all of them.</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">builder.Services.AddProblemDetails(options =&gt;
{
    options.CustomizeProblemDetails = context =&gt;
    {
        // Add traceId to every problem response
        context.ProblemDetails.Extensions[&quot;traceId&quot;] =
            Activity.Current?.Id ?? context.HttpContext.TraceIdentifier;

        // Add server node info (helpful in multi-node deployments)
        context.ProblemDetails.Extensions[&quot;nodeId&quot;] = Environment.MachineName;

        // ISO 8601 timestamp for client-side correlation
        context.ProblemDetails.Extensions[&quot;timestamp&quot;] =
            DateTimeOffset.UtcNow.ToString(&quot;o&quot;);
    };
});
</code></pre>
</div>
<p>This approach gives you a uniform response shape across your entire API. Every error -- whether produced by routing, authentication, validation, or your own exception handlers -- will include these fields. Client applications can write a single error-handling routine that works everywhere.</p>
<p>In .NET 10, <code>ProblemDetailsContext</code> gained a <code>SuppressDiagnosticsCallback</code> property, giving you finer control over when diagnostics events are suppressed for handled exceptions. This is useful when you want to suppress diagnostic telemetry for expected exceptions (such as validation errors or not-found responses) while still recording them for unexpected errors.</p>
<p>This technique is conceptually similar to what the <a href="https://www.devleader.ca/2026/06/02/chain-of-responsibility-vs-decorator-pattern-in-c-key-differences-explained">Decorator pattern</a> describes in object-oriented terms: you are wrapping the base behavior (producing a problem details response) with additional cross-cutting concerns (adding trace IDs, timestamps) without modifying the core logic.</p>
<h2 id="validation-error-responses-with-apicontroller">Validation Error Responses with [ApiController]</h2>
<p>The <code>[ApiController]</code> attribute automates a critical part of <strong>asp.net core error handling</strong> for you. When model validation fails, it short-circuits execution before your action method runs and returns a 400 Bad Request with a <code>ValidationProblemDetails</code> body. You never need to write <code>if (!ModelState.IsValid)</code> checks in API controller actions.</p>
<p>The automatic validation response looks like this:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-json">{
  &quot;type&quot;: &quot;https://tools.ietf.org/html/rfc9457&quot;,
  &quot;title&quot;: &quot;One or more validation errors occurred.&quot;,
  &quot;status&quot;: 400,
  &quot;errors&quot;: {
    &quot;Email&quot;: [&quot;The Email field is not a valid e-mail address.&quot;],
    &quot;Name&quot;: [&quot;The Name field is required.&quot;]
  },
  &quot;traceId&quot;: &quot;00-a1b2c3...&quot;
}
</code></pre>
</div>
<p>To customize this response -- for example to return 422 instead of 400 for semantic validation failures, or to add your custom extension fields -- replace <code>InvalidModelStateResponseFactory</code>:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">builder.Services.Configure&lt;ApiBehaviorOptions&gt;(options =&gt;
{
    options.InvalidModelStateResponseFactory = context =&gt;
    {
        var problemDetails = new ValidationProblemDetails(context.ModelState)
        {
            Type = &quot;https://api.example.com/errors/validation&quot;,
            Title = &quot;Validation failed&quot;,
            Status = StatusCodes.Status422UnprocessableEntity
        };

        problemDetails.Extensions[&quot;traceId&quot;] =
            context.HttpContext.TraceIdentifier;

        return new UnprocessableEntityObjectResult(problemDetails)
        {
            ContentTypes = { &quot;application/problem+json&quot; }
        };
    };
});
</code></pre>
</div>
<p>The choice between 400 and 422 depends on how you classify the error and what semantics you want clients to rely on. Both are valid; the key is choosing deliberately and applying consistently. Use 400 for requests with bad syntax or structure, 422 for semantically invalid requests. Either way, document your convention in your API spec so consumers know what to expect.</p>
<h2 id="exception-filters-vs-iexceptionhandler">Exception Filters vs IExceptionHandler</h2>
<p>These two mechanisms both catch exceptions, but they operate at different levels. Understanding the distinction helps you choose the right tool for each scenario.</p>
<p>Exception filters (<code>IExceptionFilter</code>, <code>IAsyncExceptionFilter</code>) are MVC-scoped. They only catch exceptions thrown inside MVC action methods -- not in middleware, not in minimal API handlers, not in background services. You can apply them globally, per-controller, or per-action via attributes, which gives you fine-grained control. They are a reasonable fit when specific exceptions should map to specific responses only within certain controllers.</p>
<p><code>IExceptionHandler</code> is pipeline-wide. It catches anything that bubbles up through the entire middleware stack -- controllers, minimal APIs, and even exceptions in other middleware. In .NET 10, <code>IExceptionHandler</code> is the commonly preferred approach unless you have a very specific reason to scope error handling to individual actions. For pure Web APIs, there is rarely such a reason.</p>
<p>If you are familiar with <a href="https://www.devleader.ca/2026/05/13/proxy-design-pattern-in-c-complete-guide-with-examples">the Proxy pattern</a>, you can think of <code>IExceptionHandler</code> as a pipeline-level proxy that intercepts every request and translates exception language into HTTP language -- a clean separation of concerns.</p>
<h2 id="logging-errors-properly">Logging Errors Properly</h2>
<p><strong>ASP.NET Core error handling</strong> and structured logging are inseparable. Every unhandled exception ideally produces a structured log entry with enough context to diagnose the problem later. The <a href="https://www.devleader.ca/2026/07/07/how-to-set-up-serilog-in-aspnet-core-step-by-step-guide">How to Set Up Serilog in ASP.NET Core guide</a> covers Serilog setup in depth, and the <a href="https://www.devleader.ca/2026/07/05/serilog-in-net-complete-guide-to-structured-logging">Serilog in .NET complete guide</a> covers structured logging patterns -- but a few error-handling-specific practices are worth emphasizing here.</p>
<p>Log the full exception object, not just <code>exception.Message</code>. The message omits the stack trace, inner exceptions, and contextual data that makes the difference between a 5-minute fix and a 2-hour investigation. Use the overload that accepts an <code>Exception</code> as the first parameter:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">_logger.LogError(
    exception,
    &quot;Order processing failed for OrderId {OrderId} -- {ExceptionType}&quot;,
    orderId,
    exception.GetType().Name);
</code></pre>
</div>
<p>Avoid logging and re-throwing the same exception at multiple layers. Log once, at the outermost catch point -- your global exception handler. Multiple log entries for a single exception create noise and make correlation harder. Let the structured log entry carry the full context, including the trace ID.</p>
<h2 id="development-vs-production-errors">Development vs Production Errors</h2>
<p>One of the most practical aspects of <strong>asp.net core error handling</strong> is environment-based configuration. In development, you want full exception details -- stack traces, inner exceptions, query strings. In production, you want the opposite: structured Problem Details with no sensitive internals exposed.</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">if (app.Environment.IsDevelopment())
{
    // Full stack traces and exception details -- never enable in production
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler(); // Clean ProblemDetails via your IExceptionHandler chain
    app.UseHsts();
}
</code></pre>
</div>
<p><code>UseDeveloperExceptionPage</code> returns a rich HTML page (or detailed JSON for API requests that send <code>Accept: application/json</code>) with the full exception details. It is invaluable during development. But in production, stack traces and internal file paths can reveal exploitable information about your system. Ensure <code>ASPNETCORE_ENVIRONMENT</code> is set to <code>Production</code> in your deployment environment, and never conditionally enable developer pages based on anything other than <code>IsDevelopment()</code>.</p>
<h2 id="frequently-asked-questions">Frequently Asked Questions</h2>
<h3 id="what-is-the-difference-between-useexceptionhandler-and-iexceptionhandler-in-asp.net-core">What is the difference between UseExceptionHandler and IExceptionHandler in ASP.NET Core?</h3>
<p><code>UseExceptionHandler</code> is the middleware component you register in the request pipeline. It wraps the downstream pipeline in a try/catch and catches unhandled exceptions. When registered with no arguments and <code>AddProblemDetails</code> has been called, it produces a Problem Details response by default -- the exact shape can still vary based on environment settings and any customization you apply.</p>
<p><code>IExceptionHandler</code> is an interface you implement in your own classes. These implementations are called by <code>UseExceptionHandler</code> when an exception occurs. Multiple handlers can be registered, and they are tried in order -- the first one to return <code>true</code> handles the exception and subsequent handlers are skipped. Think of <code>UseExceptionHandler</code> as the outer shell and <code>IExceptionHandler</code> implementations as the pluggable logic running inside it.</p>
<p>In .NET 10, the commonly preferred approach is to use both together: <code>UseExceptionHandler()</code> in the pipeline and one or more <code>IExceptionHandler</code> implementations registered with <code>AddExceptionHandler&lt;T&gt;()</code>.</p>
<h3 id="should-validation-errors-return-http-400-or-http-422">Should validation errors return HTTP 400 or HTTP 422?</h3>
<p>Both status codes are defensible, and you will find respected APIs using each. RFC 9110 defines 400 Bad Request as appropriate when the server cannot process the request due to a client error -- which includes invalid syntax, missing required fields, and values outside allowed ranges. RFC 9110 defines 422 Unprocessable Content as appropriate when the request is syntactically correct but cannot be processed due to semantic errors.</p>
<p>The practical difference: use 400 for structural problems (missing required field, wrong data type) and 422 for business rule violations (a date range where the end is before the start, an order quantity that exceeds inventory). ASP.NET Core returns 400 by default for model validation failures. You can override this with <code>InvalidModelStateResponseFactory</code> if your team prefers 422. The most important thing is consistency -- pick a convention and document it in your API spec.</p>
<h3 id="how-do-i-add-custom-fields-like-errorcode-to-every-problem-details-response">How do I add custom fields like errorCode to every Problem Details response?</h3>
<p>Use the <code>CustomizeProblemDetails</code> callback in <code>AddProblemDetails()</code>. This callback receives every problem details response before it is written -- including automatic validation errors, routing 404s, and responses from your <code>IExceptionHandler</code> implementations. You can add any key-value pair to <code>ProblemDetails.Extensions</code> and it will be serialized as an additional field in the JSON response.</p>
<p>For exception-specific data like an <code>errorCode</code> that only makes sense when a domain exception is thrown, add the extension in your <code>IExceptionHandler</code> implementation instead. That way the field only appears when the relevant exception type is caught, rather than appearing (with a null or default value) on every response.</p>
<h3 id="how-do-i-handle-notfoundexception-to-return-404-instead-of-500">How do I handle NotFoundException to return 404 instead of 500?</h3>
<p>Create a custom exception class (e.g., <code>NotFoundException : Exception</code>) and implement an <code>IExceptionHandler</code> that checks for it. When the handler receives a <code>NotFoundException</code>, it sets the HTTP status code to 404, writes a problem details body, and returns <code>true</code>.</p>
<p>A cleaner pattern is to add a <code>StatusCode</code> property to a base exception class and have a single <code>IExceptionHandler</code> read it. Your <code>NotFoundException</code> sets <code>StatusCode = 404</code>, your <code>ConflictException</code> sets <code>StatusCode = 409</code>, and so on. The handler maps exception status codes to HTTP status codes without knowing anything about specific exception types. This makes adding new exception types purely additive -- no handler changes needed.</p>
<h3 id="can-i-test-iexceptionhandler-implementations-in-isolation">Can I test IExceptionHandler implementations in isolation?</h3>
<p>Yes, and this is one of the main reasons to prefer <code>IExceptionHandler</code> over inline lambda handlers. Construct your handler, create a <code>DefaultHttpContext</code> with a mock response stream, call <code>TryHandleAsync</code> with a test exception, and assert the response status code and body.</p>
<p>For integration tests, use <code>WebApplicationFactory&lt;Program&gt;</code> to start a real test server and configure endpoints that deliberately throw specific exceptions. Then call those endpoints with an <code>HttpClient</code> and assert the full response -- status code, content type, and parsed JSON body. Integration tests are especially valuable for verifying that your <code>AddProblemDetails</code> customizations apply consistently across all error paths.</p>
<h3 id="what-happens-if-an-iexceptionhandler-implementation-throws-an-exception-itself">What happens if an IExceptionHandler implementation throws an exception itself?</h3>
<p>If your <code>IExceptionHandler</code> throws while trying to handle an exception, ASP.NET Core falls back to its built-in behavior. If <code>AddProblemDetails</code> is registered, the framework will attempt to produce a generic 500 problem details response. If it cannot do that either, it produces a bare 500 with no body.</p>
<p>Your exception handlers need to be defensive. Avoid calling services that could fail -- no database calls, no external HTTP calls, nothing that might throw. If you absolutely need external data in your error handler, wrap it in a try/catch. The error handling layer is not the place for complex orchestration.</p>
<h3 id="how-does-asp.net-core-error-handling-interact-with-middleware-short-circuiting">How does asp.net core error handling interact with middleware short-circuiting?</h3>
<p><code>UseExceptionHandler</code> only catches exceptions that propagate through the pipeline. Middleware that short-circuits by writing a response and not calling <code>next()</code> does not trigger <code>UseExceptionHandler</code> -- it bypasses it entirely. This is intentional. Health check endpoints, rate limiters, and cached response middleware all need to short-circuit without triggering exception handling.</p>
<p>The implication is that middleware registered before <code>UseExceptionHandler</code> in the pipeline is outside the error handling safety net. Keep middleware that might produce errors after <code>UseExceptionHandler</code> in the registration order, which is why the recommended pipeline ordering usually puts <code>UseExceptionHandler</code> first.</p>
]]></content:encoded>
      <media:content url="https://devleader-d2f9ggbjfpdqcka7.z01.azurefd.net/media/aspnet-core-error-handling.webp" />
    </item>
    <item>
      <guid isPermaLink="false">3f107f53-930b-412f-a8e8-9a3c0345b92d</guid>
      <link>https://www.devleader.ca/2026/06/05/when-to-use-iterator-pattern-in-c-decision-guide-with-examples</link>
      <category>iterator pattern</category>
      <category>c#</category>
      <category>design patterns</category>
      <category>when to use</category>
      <category>decision guide</category>
      <title>When to Use Iterator Pattern in C#: Decision Guide with Examples</title>
      <pubDate>Fri, 05 Jun 2026 13:00:00 Z</pubDate>
      <description><![CDATA[<h1 id="when-to-use-iterator-pattern-in-c-decision-guide-with-examples">When to Use Iterator Pattern in C#: Decision Guide with Examples</h1>
<p>You've got <code>foreach</code> loops everywhere in your C# code. Lists, arrays, dictionaries -- they all support enumeration out of the box. So why would you ever need to build a custom iterator? The answer is that built-in collections cover the common case, but <strong>when to use the iterator pattern in C#</strong> becomes a real question once your data doesn't fit neatly into a <code>List&lt;T&gt;</code>. Maybe you're reading millions of lines from a file. Maybe you're walking a tree structure. Maybe you're pulling paginated results from an API and want to hide that complexity behind a simple loop.</p>
<p>This article gives you a structured decision framework for recognizing when the iterator pattern earns its place in your code versus when you should lean on what .NET gives you for free. We'll walk through decision criteria, three practical scenarios with C# code, and the situations where a custom iterator adds unnecessary complexity. If you're building up your design pattern toolkit, consider checking out the <a href="https://www.devleader.ca/2026/03/02/strategy-design-pattern-in-c-complete-guide-with-examples">strategy design pattern</a> for another pattern that helps you swap out behavior cleanly.</p>
<h2 id="decision-criteria-when-custom-iterators-add-value">Decision Criteria: When Custom Iterators Add Value</h2>
<p>Not every data source maps to a <code>List&lt;T&gt;</code> or an array. And not every traversal is a simple front-to-back scan. The question of when to use the iterator pattern in C# boils down to a few clear signals.</p>
<h3 id="custom-data-structures-that-dont-map-to-standard-collections">Custom Data Structures That Don't Map to Standard Collections</h3>
<p>If you've built a tree, a graph, a skip list, or any data structure that isn't a flat sequence, the built-in collection types won't expose it the way consumers expect. A custom iterator lets you present your data structure as an <code>IEnumerable&lt;T&gt;</code> so callers can use <code>foreach</code> without understanding the internal layout. This is especially valuable when your data structure is part of a <a href="https://www.devleader.ca/2026/04/02/composite-design-pattern-in-c-complete-guide-with-examples">composite design pattern</a> where the hierarchy can be arbitrarily deep.</p>
<h3 id="lazy-evaluation-of-expensive-computations">Lazy Evaluation of Expensive Computations</h3>
<p>When each element in a sequence is expensive to compute -- think parsing, transforming, or generating -- materializing the entire collection upfront wastes time and memory if the consumer only needs a few elements. The iterator pattern in C# with <code>yield return</code> lets you compute elements one at a time, on demand. The consumer pulls what it needs, and the rest never gets computed.</p>
<h3 id="streaming-data-from-external-sources">Streaming Data from External Sources</h3>
<p>Files, network streams, database cursors, and API endpoints aren't collections sitting in memory. They're sources you read from incrementally. A custom iterator wraps the read-ahead logic so callers see a clean <code>foreach</code> loop. This is the same principle behind how <code>IAsyncEnumerable&lt;T&gt;</code> works for async data sources in .NET.</p>
<p>Beyond these three signals, there's one more worth considering.</p>
<h3 id="multiple-traversal-strategies-over-the-same-data">Multiple Traversal Strategies Over the Same Data</h3>
<p>A binary tree can be traversed in-order, pre-order, or post-order. A graph can be traversed depth-first or breadth-first. If consumers of your data structure need different traversal orders, the iterator pattern lets you expose multiple <code>IEnumerable&lt;T&gt;</code> methods -- one per traversal strategy -- without forcing the consumer to understand the traversal algorithm. This pairs naturally with the <a href="https://www.devleader.ca/2026/03/02/strategy-design-pattern-in-c-complete-guide-with-examples">strategy design pattern</a>, where the traversal algorithm itself is interchangeable.</p>
<h2 id="scenario-lazy-file-processing">Scenario: Lazy File Processing</h2>
<p>One of the clearest demonstrations of when to use the iterator pattern in C# is processing large files. If you call <code>File.ReadAllLines()</code>, you load the entire file into memory as a string array. For a file with millions of lines, that's a significant allocation. An iterator reads one line at a time.</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">using System.Collections.Generic;
using System.IO;

public static class LazyFileReader
{
    // Reads lines one at a time using yield return
    public static IEnumerable&lt;string&gt; ReadLines(
        string filePath)
    {
        using var reader = new StreamReader(filePath);

        while (!reader.EndOfStream)
        {
            yield return reader.ReadLine()!;
        }
    }
}
</code></pre>
</div>
<p>The <code>yield return</code> keyword is the key. Each time the consumer's <code>foreach</code> asks for the next element, execution resumes inside <code>ReadLines</code>, reads one line, and suspends again. The <code>StreamReader</code> stays open across iterations, and the <code>using</code> statement ensures it gets disposed when enumeration finishes or the consumer breaks out early.</p>
<p>Here's how you'd use it:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">// Only one line is in memory at any given time
foreach (var line in LazyFileReader.ReadLines(&quot;server-log.txt&quot;))
{
    if (line.Contains(&quot;ERROR&quot;))
    {
        Console.WriteLine(line);
    }
}
</code></pre>
</div>
<p>Compare this to the eager approach:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">// Loads the ENTIRE file into memory at once
var allLines = File.ReadAllLines(&quot;server-log.txt&quot;);

foreach (var line in allLines)
{
    if (line.Contains(&quot;ERROR&quot;))
    {
        Console.WriteLine(line);
    }
}
</code></pre>
</div>
<p>With a 2 GB log file containing millions of lines, <code>ReadAllLines</code> allocates a massive string array plus the string data itself. The lazy iterator holds exactly one string in flight at a time. If you're only looking for error lines and breaking early, the iterator may read just a fraction of the file.</p>
<p>Note that .NET does include <code>File.ReadLines()</code> (not <code>ReadAllLines</code>), which uses this exact pattern internally. That's how fundamental the iterator pattern is to the .NET ecosystem -- the framework itself uses it to solve this problem.</p>
<h2 id="scenario-tree-traversal">Scenario: Tree Traversal</h2>
<p>Trees are a natural fit for the iterator pattern in C# because they're not flat sequences. A consumer shouldn't need to understand recursion or maintain a stack just to visit every node. Multiple traversal strategies make this even more compelling.</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">using System.Collections.Generic;

public sealed class BinaryTreeNode&lt;T&gt;
{
    public T Value { get; }
    public BinaryTreeNode&lt;T&gt;? Left { get; }
    public BinaryTreeNode&lt;T&gt;? Right { get; }

    public BinaryTreeNode(
        T value,
        BinaryTreeNode&lt;T&gt;? left = null,
        BinaryTreeNode&lt;T&gt;? right = null)
    {
        Value = value;
        Left = left;
        Right = right;
    }

    // In-order: Left, Root, Right
    public IEnumerable&lt;T&gt; InOrder()
    {
        if (Left is not null)
        {
            foreach (var item in Left.InOrder())
            {
                yield return item;
            }
        }

        yield return Value;

        if (Right is not null)
        {
            foreach (var item in Right.InOrder())
            {
                yield return item;
            }
        }
    }

    // Pre-order: Root, Left, Right
    public IEnumerable&lt;T&gt; PreOrder()
    {
        yield return Value;

        if (Left is not null)
        {
            foreach (var item in Left.PreOrder())
            {
                yield return item;
            }
        }

        if (Right is not null)
        {
            foreach (var item in Right.PreOrder())
            {
                yield return item;
            }
        }
    }

    // Post-order: Left, Right, Root
    public IEnumerable&lt;T&gt; PostOrder()
    {
        if (Left is not null)
        {
            foreach (var item in Left.PostOrder())
            {
                yield return item;
            }
        }

        if (Right is not null)
        {
            foreach (var item in Right.PostOrder())
            {
                yield return item;
            }
        }

        yield return Value;
    }
}
</code></pre>
</div>
<p>Each traversal method returns <code>IEnumerable&lt;T&gt;</code> and uses <code>yield return</code> to lazily produce values. The caller picks the traversal strategy they need:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">//       4
//      / 
//     2   6
//    /  / 
//   1  3 5  7
var tree = new BinaryTreeNode&lt;int&gt;(4,
    new BinaryTreeNode&lt;int&gt;(2,
        new BinaryTreeNode&lt;int&gt;(1),
        new BinaryTreeNode&lt;int&gt;(3)),
    new BinaryTreeNode&lt;int&gt;(6,
        new BinaryTreeNode&lt;int&gt;(5),
        new BinaryTreeNode&lt;int&gt;(7)));

// In-order: 1, 2, 3, 4, 5, 6, 7
foreach (var value in tree.InOrder())
{
    Console.Write($&quot;{value} &quot;);
}

Console.WriteLine();

// Pre-order: 4, 2, 1, 3, 6, 5, 7
foreach (var value in tree.PreOrder())
{
    Console.Write($&quot;{value} &quot;);
}
</code></pre>
</div>
<p>This is where the <a href="https://www.devleader.ca/2026/04/02/composite-design-pattern-in-c-complete-guide-with-examples">composite design pattern</a> and the iterator pattern complement each other. Composite gives you a uniform interface for treating individual objects and compositions the same way. The iterator pattern gives you a uniform way to traverse that composition. When your composite structure represents a file system, an organization chart, or a UI component tree, exposing <code>IEnumerable&lt;T&gt;</code> traversals lets consumers work with the hierarchy without understanding its shape.</p>
<h2 id="scenario-paginated-api-results">Scenario: Paginated API Results</h2>
<p>APIs that return paginated results are a strong case for when to use the iterator pattern in C#. The consumer wants to iterate over all results. The iterator handles fetching page by page behind the scenes. Client code sees a simple <code>foreach</code> and never thinks about page tokens, offsets, or batch sizes.</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;

public sealed class PaginatedApiIterator&lt;T&gt;
{
    private readonly HttpClient _httpClient;
    private readonly string _baseUrl;
    private readonly int _pageSize;

    public PaginatedApiIterator(
        HttpClient httpClient,
        string baseUrl,
        int pageSize = 50)
    {
        _httpClient = httpClient;
        _baseUrl = baseUrl;
        _pageSize = pageSize;
    }

    public IEnumerable&lt;T&gt; GetAll()
    {
        int page = 1;
        bool hasMore = true;

        while (hasMore)
        {
            var url = $&quot;{_baseUrl}?page={page}&quot; +
                      $&quot;&amp;pageSize={_pageSize}&quot;;

            var response = _httpClient
                .GetStringAsync(url)
                .GetAwaiter()
                .GetResult();

            var pageResult = JsonSerializer
                .Deserialize&lt;PageResult&lt;T&gt;&gt;(response);

            if (pageResult?.Items is null ||
                pageResult.Items.Count == 0)
            {
                yield break;
            }

            foreach (var item in pageResult.Items)
            {
                yield return item;
            }

            hasMore = pageResult.Items.Count == _pageSize;
            page++;
        }
    }
}

public sealed class PageResult&lt;T&gt;
{
    public List&lt;T&gt;? Items { get; set; }

    public int TotalCount { get; set; }
}
</code></pre>
</div>
<p>The consumer code is clean and completely unaware of pagination:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">var httpClient = new HttpClient();
var iterator = new PaginatedApiIterator&lt;UserDto&gt;(
    httpClient,
    &quot;https://api.example.com/users&quot;,
    pageSize: 25);

foreach (var user in iterator.GetAll())
{
    Console.WriteLine($&quot;{user.Name} ({user.Email})&quot;);
}
</code></pre>
</div>
<p>There's an important note here. This synchronous example uses <code>GetAwaiter().GetResult()</code> for simplicity. In production, you'd use an async version with <code>IAsyncEnumerable&lt;T&gt;</code> and <code>await foreach</code> instead. The pattern is the same -- the iterator abstracts away the page-fetching mechanics -- but the async variant avoids blocking threads.</p>
<p>For a production-quality async version:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">public async IAsyncEnumerable&lt;T&gt; GetAllAsync()
{
    int page = 1;
    bool hasMore = true;

    while (hasMore)
    {
        var url = $&quot;{_baseUrl}?page={page}&quot; +
                  $&quot;&amp;pageSize={_pageSize}&quot;;

        var response = await _httpClient
            .GetStringAsync(url);

        var pageResult = JsonSerializer
            .Deserialize&lt;PageResult&lt;T&gt;&gt;(response);

        if (pageResult?.Items is null ||
            pageResult.Items.Count == 0)
        {
            yield break;
        }

        foreach (var item in pageResult.Items)
        {
            yield return item;
        }

        hasMore = pageResult.Items.Count == _pageSize;
        page++;
    }
}
</code></pre>
</div>
<p>This pattern works for any paginated source -- REST APIs, database cursors, or even file-based batch processing. The key benefit is encapsulation: the consumer iterates without knowing about the pagination protocol. If the API changes its pagination scheme from page numbers to cursor tokens, you update one method and every consumer continues working unchanged. This kind of encapsulation is a core benefit you'll see across many design patterns, including the <a href="https://www.devleader.ca/2026/05/25/chain-of-responsibility-design-pattern-in-c-complete-guide-with-examples">chain of responsibility pattern</a> where each handler hides processing details from the caller.</p>
<h2 id="when-not-to-use-the-iterator-pattern">When NOT to Use the Iterator Pattern</h2>
<p>Knowing when to use the iterator pattern in C# means also knowing when it's the wrong tool. The pattern adds value when you need lazy evaluation, custom traversal, or data source abstraction. In other cases, it introduces complexity without a payoff.</p>
<h3 id="simple-lists-and-arrays">Simple Lists and Arrays</h3>
<p>If your data is already in a <code>List&lt;T&gt;</code>, an array, or any other built-in collection, there's no reason to use the iterator pattern in C# for a custom implementation. The <code>foreach</code> keyword works out of the box. These types implement <code>IEnumerable&lt;T&gt;</code> and provide efficient enumerators. Writing a custom iterator around an existing collection doesn't add value -- it adds a layer of indirection that makes debugging harder and code longer.</p>
<h3 id="random-access-requirements">Random Access Requirements</h3>
<p>Iterators are sequential by design. You move forward through a sequence, one element at a time. If your consumer needs to jump to element 47, access the last element, or index by position, an iterator is the wrong abstraction. Use an indexer, a <code>List&lt;T&gt;</code>, or an array where <code>collection[index]</code> gives you O(1) access.</p>
<h3 id="all-elements-must-be-available-at-once">All Elements Must Be Available at Once</h3>
<p>Some algorithms need the full dataset in memory before they can proceed -- sorting, grouping, aggregating, or finding the median all require the complete collection. If your consumers consistently call <code>.ToList()</code> or <code>.ToArray()</code> on your <code>IEnumerable&lt;T&gt;</code> before doing any work, using the iterator pattern in C# adds overhead without benefit. You're paying the cost of the iterator state machine and then materializing everything anyway. Return a <code>List&lt;T&gt;</code> or <code>IReadOnlyList&lt;T&gt;</code> instead and make the intent clear.</p>
<p>There's one more situation worth flagging.</p>
<h3 id="deferred-execution-causing-subtle-bugs">Deferred Execution Causing Subtle Bugs</h3>
<p>Deferred execution means your iterator's body doesn't run until someone enumerates it. This is usually a feature, but it can cause problems when the data source changes between when you create the iterator and when you consume it. If you yield from a database connection and the connection gets disposed before enumeration starts, you'll get an exception at enumeration time -- not at creation time. This makes bugs harder to trace. When the lifetime of your data source doesn't align with enumeration timing, the iterator pattern in C# can work against you. Consider materializing the results eagerly or using a pattern like <a href="https://www.devleader.ca/2026/05/08/flyweight-design-pattern-in-c-complete-guide-with-examples">the flyweight pattern</a> to share computed results efficiently.</p>
<h2 id="iterator-pattern-in-the.net-ecosystem">Iterator Pattern in the .NET Ecosystem</h2>
<p>The iterator pattern isn't just a textbook concept in C# -- it's woven into the framework at every level. Understanding the iterator pattern in C# makes you better at using the tools .NET provides.</p>
<p><strong>LINQ is built on iterators.</strong> Every LINQ method like <code>Where</code>, <code>Select</code>, and <code>Take</code> returns an <code>IEnumerable&lt;T&gt;</code> that uses deferred execution. When you chain <code>.Where(x =&gt; x &gt; 5).Select(x =&gt; x * 2).Take(10)</code>, no work happens until you enumerate. Each operator is an iterator that pulls from the previous one. Understanding the iterator pattern explains why LINQ is lazy, why calling <code>.ToList()</code> triggers execution, and why you can compose queries without performance penalties until enumeration begins.</p>
<p><strong><code>IAsyncEnumerable&lt;T&gt;</code> extends the pattern to async.</strong> When you need to iterate over data that arrives asynchronously -- reading from a gRPC stream, consuming messages from a channel, or querying a database with streaming results -- <code>IAsyncEnumerable&lt;T&gt;</code> is the async version of the iterator pattern. The <code>await foreach</code> syntax works identically to <code>foreach</code>, and <code>yield return</code> inside an <code>async</code> method produces the async iterator state machine. If you've worked with <a href="https://www.devleader.ca/2024/02/21/iservicecollection-in-c-simplified-beginners-guide-for-dependency-injection">dependency injection through IServiceCollection</a>, you've seen how .NET wires up services behind a clean API. <code>IAsyncEnumerable&lt;T&gt;</code> does the same for async data streams -- it hides the complexity of async iteration behind a simple loop.</p>
<p><strong>Channels use the pattern for producer-consumer scenarios.</strong> <code>System.Threading.Channels</code> provides bounded and unbounded channels where producers write and consumers read. The <code>ChannelReader&lt;T&gt;.ReadAllAsync()</code> method returns an <code>IAsyncEnumerable&lt;T&gt;</code>, letting you consume channel items with <code>await foreach</code>. Without understanding the iterator pattern, the relationship between channels and async enumeration isn't obvious.</p>
<p><strong>System.IO.Pipelines leverages sequential reading.</strong> While pipelines use a different API than <code>IEnumerable&lt;T&gt;</code>, the conceptual model is the same: you read data incrementally from a source without loading everything into memory. The iterator pattern gives you the mental model to understand why pipelines work the way they do and when to choose them over simpler <code>IEnumerable&lt;T&gt;</code> based approaches.</p>
<p>The takeaway is straightforward. Understanding when to use the iterator pattern in C# isn't just about learning an academic design pattern -- you're learning the foundation that powers LINQ, async streams, channels, and half the APIs in the .NET Base Class Library.</p>
<h2 id="frequently-asked-questions">Frequently Asked Questions</h2>
<h3 id="what-is-the-difference-between-ienumerable-and-ienumerator-in-c">What is the difference between IEnumerable and IEnumerator in C#?</h3>
<p><code>IEnumerable&lt;T&gt;</code> represents a sequence that can be iterated. It has one method: <code>GetEnumerator()</code>, which returns an <code>IEnumerator&lt;T&gt;</code>. The enumerator is the actual cursor that tracks where you are in the sequence. It has <code>Current</code> (the element you're pointing at) and <code>MoveNext()</code> (advance to the next element). Think of <code>IEnumerable&lt;T&gt;</code> as the collection and <code>IEnumerator&lt;T&gt;</code> as the bookmark. When you use <code>yield return</code> in C#, the compiler generates both for you automatically, which is why you rarely implement <code>IEnumerator&lt;T&gt;</code> by hand.</p>
<h3 id="should-i-use-yield-return-or-implement-ienumerator-manually">Should I use yield return or implement IEnumerator manually?</h3>
<p>Use <code>yield return</code> in the vast majority of cases. The C# compiler generates the state machine code that <code>IEnumerator&lt;T&gt;</code> requires, handling <code>MoveNext</code>, <code>Current</code>, <code>Reset</code>, and <code>Dispose</code> for you. Manual implementation is only justified when you need fine-grained control over the state machine -- for example, when building a high-performance enumerator that avoids heap allocations by using a <code>struct</code> enumerator. For everyday scenarios like file processing, tree traversal, and API pagination, <code>yield return</code> produces correct, readable, and maintainable code.</p>
<h3 id="how-does-yield-return-work-under-the-hood-in-c">How does yield return work under the hood in C#?</h3>
<p>When the C# compiler encounters <code>yield return</code>, it transforms your method into a state machine class that implements <code>IEnumerable&lt;T&gt;</code> and <code>IEnumerator&lt;T&gt;</code>. Each <code>yield return</code> becomes a state transition. Local variables become fields on the generated class so their values persist across calls to <code>MoveNext()</code>. When the consumer calls <code>MoveNext()</code>, execution resumes from where the last <code>yield return</code> suspended. This is why iterator methods don't execute when you call them -- they return the state machine object. Execution only begins when the consumer starts enumerating.</p>
<h3 id="can-i-use-the-iterator-pattern-with-async-code-in-c">Can I use the iterator pattern with async code in C#?</h3>
<p>Yes. C# supports <code>async</code> iterator methods that return <code>IAsyncEnumerable&lt;T&gt;</code>. You can use <code>yield return</code> inside an <code>async</code> method, and consumers iterate with <code>await foreach</code>. This is essential for streaming data from async sources -- databases, HTTP APIs, message queues, and gRPC streams all benefit from async iteration. The compiler generates an async state machine that handles both the <code>await</code> suspension points and the <code>yield return</code> suspension points, giving you the same lazy evaluation semantics you get with synchronous iterators.</p>
<h3 id="when-should-i-call-tolist-instead-of-keeping-an-ienumerable">When should I call ToList() instead of keeping an IEnumerable?</h3>
<p>Call <code>ToList()</code> when you need to iterate the sequence more than once, when you need <code>Count</code> or index access without re-enumerating, or when the underlying data source might change or be disposed before you finish enumerating. Keep the <code>IEnumerable&lt;T&gt;</code> when you want lazy evaluation, when you might not need all elements, or when memory is a concern and you're working with large datasets. A common mistake is calling <code>ToList()</code> on a LINQ query deep inside a method and then returning <code>IEnumerable&lt;T&gt;</code> from the method -- you've already paid the materialization cost, so return <code>IReadOnlyList&lt;T&gt;</code> to communicate the intent clearly.</p>
<h3 id="how-does-the-iterator-pattern-relate-to-the-observer-pattern">How does the iterator pattern relate to the observer pattern?</h3>
<p>The iterator pattern and the <a href="https://www.devleader.ca/2026/03/26/observer-design-pattern-in-c-complete-guide-with-examples">observer pattern</a> are duals of each other. The iterator pattern is &quot;pull-based&quot; -- the consumer requests the next element when it's ready. The observer pattern is &quot;push-based&quot; -- the producer sends elements to subscribers when they're available. In .NET, <code>IEnumerable&lt;T&gt;</code> represents the pull model and <code>IObservable&lt;T&gt;</code> represents the push model. Understanding both helps you choose the right approach for your data flow. If the consumer controls the pace, use iterators. If the producer controls the pace, use observers.</p>
<h3 id="is-the-iterator-pattern-thread-safe-in-c">Is the iterator pattern thread-safe in C#?</h3>
<p>Not by default. The enumerator state machine generated by <code>yield return</code> is not thread-safe. If multiple threads enumerate the same <code>IEnumerable&lt;T&gt;</code> instance concurrently, each gets its own enumerator (from <code>GetEnumerator()</code>), so that's fine. But if multiple threads share a single <code>IEnumerator&lt;T&gt;</code> and call <code>MoveNext()</code> concurrently, the behavior is undefined. For concurrent scenarios, use <code>IAsyncEnumerable&lt;T&gt;</code> with proper async coordination, or materialize to a thread-safe collection first. Channels in .NET provide a thread-safe producer-consumer model that integrates with <code>IAsyncEnumerable&lt;T&gt;</code> for safe concurrent iteration.</p>
]]></description>
      <content:encoded><![CDATA[<h1 id="when-to-use-iterator-pattern-in-c-decision-guide-with-examples">When to Use Iterator Pattern in C#: Decision Guide with Examples</h1>
<p>You've got <code>foreach</code> loops everywhere in your C# code. Lists, arrays, dictionaries -- they all support enumeration out of the box. So why would you ever need to build a custom iterator? The answer is that built-in collections cover the common case, but <strong>when to use the iterator pattern in C#</strong> becomes a real question once your data doesn't fit neatly into a <code>List&lt;T&gt;</code>. Maybe you're reading millions of lines from a file. Maybe you're walking a tree structure. Maybe you're pulling paginated results from an API and want to hide that complexity behind a simple loop.</p>
<p>This article gives you a structured decision framework for recognizing when the iterator pattern earns its place in your code versus when you should lean on what .NET gives you for free. We'll walk through decision criteria, three practical scenarios with C# code, and the situations where a custom iterator adds unnecessary complexity. If you're building up your design pattern toolkit, consider checking out the <a href="https://www.devleader.ca/2026/03/02/strategy-design-pattern-in-c-complete-guide-with-examples">strategy design pattern</a> for another pattern that helps you swap out behavior cleanly.</p>
<h2 id="decision-criteria-when-custom-iterators-add-value">Decision Criteria: When Custom Iterators Add Value</h2>
<p>Not every data source maps to a <code>List&lt;T&gt;</code> or an array. And not every traversal is a simple front-to-back scan. The question of when to use the iterator pattern in C# boils down to a few clear signals.</p>
<h3 id="custom-data-structures-that-dont-map-to-standard-collections">Custom Data Structures That Don't Map to Standard Collections</h3>
<p>If you've built a tree, a graph, a skip list, or any data structure that isn't a flat sequence, the built-in collection types won't expose it the way consumers expect. A custom iterator lets you present your data structure as an <code>IEnumerable&lt;T&gt;</code> so callers can use <code>foreach</code> without understanding the internal layout. This is especially valuable when your data structure is part of a <a href="https://www.devleader.ca/2026/04/02/composite-design-pattern-in-c-complete-guide-with-examples">composite design pattern</a> where the hierarchy can be arbitrarily deep.</p>
<h3 id="lazy-evaluation-of-expensive-computations">Lazy Evaluation of Expensive Computations</h3>
<p>When each element in a sequence is expensive to compute -- think parsing, transforming, or generating -- materializing the entire collection upfront wastes time and memory if the consumer only needs a few elements. The iterator pattern in C# with <code>yield return</code> lets you compute elements one at a time, on demand. The consumer pulls what it needs, and the rest never gets computed.</p>
<h3 id="streaming-data-from-external-sources">Streaming Data from External Sources</h3>
<p>Files, network streams, database cursors, and API endpoints aren't collections sitting in memory. They're sources you read from incrementally. A custom iterator wraps the read-ahead logic so callers see a clean <code>foreach</code> loop. This is the same principle behind how <code>IAsyncEnumerable&lt;T&gt;</code> works for async data sources in .NET.</p>
<p>Beyond these three signals, there's one more worth considering.</p>
<h3 id="multiple-traversal-strategies-over-the-same-data">Multiple Traversal Strategies Over the Same Data</h3>
<p>A binary tree can be traversed in-order, pre-order, or post-order. A graph can be traversed depth-first or breadth-first. If consumers of your data structure need different traversal orders, the iterator pattern lets you expose multiple <code>IEnumerable&lt;T&gt;</code> methods -- one per traversal strategy -- without forcing the consumer to understand the traversal algorithm. This pairs naturally with the <a href="https://www.devleader.ca/2026/03/02/strategy-design-pattern-in-c-complete-guide-with-examples">strategy design pattern</a>, where the traversal algorithm itself is interchangeable.</p>
<h2 id="scenario-lazy-file-processing">Scenario: Lazy File Processing</h2>
<p>One of the clearest demonstrations of when to use the iterator pattern in C# is processing large files. If you call <code>File.ReadAllLines()</code>, you load the entire file into memory as a string array. For a file with millions of lines, that's a significant allocation. An iterator reads one line at a time.</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">using System.Collections.Generic;
using System.IO;

public static class LazyFileReader
{
    // Reads lines one at a time using yield return
    public static IEnumerable&lt;string&gt; ReadLines(
        string filePath)
    {
        using var reader = new StreamReader(filePath);

        while (!reader.EndOfStream)
        {
            yield return reader.ReadLine()!;
        }
    }
}
</code></pre>
</div>
<p>The <code>yield return</code> keyword is the key. Each time the consumer's <code>foreach</code> asks for the next element, execution resumes inside <code>ReadLines</code>, reads one line, and suspends again. The <code>StreamReader</code> stays open across iterations, and the <code>using</code> statement ensures it gets disposed when enumeration finishes or the consumer breaks out early.</p>
<p>Here's how you'd use it:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">// Only one line is in memory at any given time
foreach (var line in LazyFileReader.ReadLines(&quot;server-log.txt&quot;))
{
    if (line.Contains(&quot;ERROR&quot;))
    {
        Console.WriteLine(line);
    }
}
</code></pre>
</div>
<p>Compare this to the eager approach:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">// Loads the ENTIRE file into memory at once
var allLines = File.ReadAllLines(&quot;server-log.txt&quot;);

foreach (var line in allLines)
{
    if (line.Contains(&quot;ERROR&quot;))
    {
        Console.WriteLine(line);
    }
}
</code></pre>
</div>
<p>With a 2 GB log file containing millions of lines, <code>ReadAllLines</code> allocates a massive string array plus the string data itself. The lazy iterator holds exactly one string in flight at a time. If you're only looking for error lines and breaking early, the iterator may read just a fraction of the file.</p>
<p>Note that .NET does include <code>File.ReadLines()</code> (not <code>ReadAllLines</code>), which uses this exact pattern internally. That's how fundamental the iterator pattern is to the .NET ecosystem -- the framework itself uses it to solve this problem.</p>
<h2 id="scenario-tree-traversal">Scenario: Tree Traversal</h2>
<p>Trees are a natural fit for the iterator pattern in C# because they're not flat sequences. A consumer shouldn't need to understand recursion or maintain a stack just to visit every node. Multiple traversal strategies make this even more compelling.</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">using System.Collections.Generic;

public sealed class BinaryTreeNode&lt;T&gt;
{
    public T Value { get; }
    public BinaryTreeNode&lt;T&gt;? Left { get; }
    public BinaryTreeNode&lt;T&gt;? Right { get; }

    public BinaryTreeNode(
        T value,
        BinaryTreeNode&lt;T&gt;? left = null,
        BinaryTreeNode&lt;T&gt;? right = null)
    {
        Value = value;
        Left = left;
        Right = right;
    }

    // In-order: Left, Root, Right
    public IEnumerable&lt;T&gt; InOrder()
    {
        if (Left is not null)
        {
            foreach (var item in Left.InOrder())
            {
                yield return item;
            }
        }

        yield return Value;

        if (Right is not null)
        {
            foreach (var item in Right.InOrder())
            {
                yield return item;
            }
        }
    }

    // Pre-order: Root, Left, Right
    public IEnumerable&lt;T&gt; PreOrder()
    {
        yield return Value;

        if (Left is not null)
        {
            foreach (var item in Left.PreOrder())
            {
                yield return item;
            }
        }

        if (Right is not null)
        {
            foreach (var item in Right.PreOrder())
            {
                yield return item;
            }
        }
    }

    // Post-order: Left, Right, Root
    public IEnumerable&lt;T&gt; PostOrder()
    {
        if (Left is not null)
        {
            foreach (var item in Left.PostOrder())
            {
                yield return item;
            }
        }

        if (Right is not null)
        {
            foreach (var item in Right.PostOrder())
            {
                yield return item;
            }
        }

        yield return Value;
    }
}
</code></pre>
</div>
<p>Each traversal method returns <code>IEnumerable&lt;T&gt;</code> and uses <code>yield return</code> to lazily produce values. The caller picks the traversal strategy they need:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">//       4
//      / 
//     2   6
//    /  / 
//   1  3 5  7
var tree = new BinaryTreeNode&lt;int&gt;(4,
    new BinaryTreeNode&lt;int&gt;(2,
        new BinaryTreeNode&lt;int&gt;(1),
        new BinaryTreeNode&lt;int&gt;(3)),
    new BinaryTreeNode&lt;int&gt;(6,
        new BinaryTreeNode&lt;int&gt;(5),
        new BinaryTreeNode&lt;int&gt;(7)));

// In-order: 1, 2, 3, 4, 5, 6, 7
foreach (var value in tree.InOrder())
{
    Console.Write($&quot;{value} &quot;);
}

Console.WriteLine();

// Pre-order: 4, 2, 1, 3, 6, 5, 7
foreach (var value in tree.PreOrder())
{
    Console.Write($&quot;{value} &quot;);
}
</code></pre>
</div>
<p>This is where the <a href="https://www.devleader.ca/2026/04/02/composite-design-pattern-in-c-complete-guide-with-examples">composite design pattern</a> and the iterator pattern complement each other. Composite gives you a uniform interface for treating individual objects and compositions the same way. The iterator pattern gives you a uniform way to traverse that composition. When your composite structure represents a file system, an organization chart, or a UI component tree, exposing <code>IEnumerable&lt;T&gt;</code> traversals lets consumers work with the hierarchy without understanding its shape.</p>
<h2 id="scenario-paginated-api-results">Scenario: Paginated API Results</h2>
<p>APIs that return paginated results are a strong case for when to use the iterator pattern in C#. The consumer wants to iterate over all results. The iterator handles fetching page by page behind the scenes. Client code sees a simple <code>foreach</code> and never thinks about page tokens, offsets, or batch sizes.</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;

public sealed class PaginatedApiIterator&lt;T&gt;
{
    private readonly HttpClient _httpClient;
    private readonly string _baseUrl;
    private readonly int _pageSize;

    public PaginatedApiIterator(
        HttpClient httpClient,
        string baseUrl,
        int pageSize = 50)
    {
        _httpClient = httpClient;
        _baseUrl = baseUrl;
        _pageSize = pageSize;
    }

    public IEnumerable&lt;T&gt; GetAll()
    {
        int page = 1;
        bool hasMore = true;

        while (hasMore)
        {
            var url = $&quot;{_baseUrl}?page={page}&quot; +
                      $&quot;&amp;pageSize={_pageSize}&quot;;

            var response = _httpClient
                .GetStringAsync(url)
                .GetAwaiter()
                .GetResult();

            var pageResult = JsonSerializer
                .Deserialize&lt;PageResult&lt;T&gt;&gt;(response);

            if (pageResult?.Items is null ||
                pageResult.Items.Count == 0)
            {
                yield break;
            }

            foreach (var item in pageResult.Items)
            {
                yield return item;
            }

            hasMore = pageResult.Items.Count == _pageSize;
            page++;
        }
    }
}

public sealed class PageResult&lt;T&gt;
{
    public List&lt;T&gt;? Items { get; set; }

    public int TotalCount { get; set; }
}
</code></pre>
</div>
<p>The consumer code is clean and completely unaware of pagination:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">var httpClient = new HttpClient();
var iterator = new PaginatedApiIterator&lt;UserDto&gt;(
    httpClient,
    &quot;https://api.example.com/users&quot;,
    pageSize: 25);

foreach (var user in iterator.GetAll())
{
    Console.WriteLine($&quot;{user.Name} ({user.Email})&quot;);
}
</code></pre>
</div>
<p>There's an important note here. This synchronous example uses <code>GetAwaiter().GetResult()</code> for simplicity. In production, you'd use an async version with <code>IAsyncEnumerable&lt;T&gt;</code> and <code>await foreach</code> instead. The pattern is the same -- the iterator abstracts away the page-fetching mechanics -- but the async variant avoids blocking threads.</p>
<p>For a production-quality async version:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">public async IAsyncEnumerable&lt;T&gt; GetAllAsync()
{
    int page = 1;
    bool hasMore = true;

    while (hasMore)
    {
        var url = $&quot;{_baseUrl}?page={page}&quot; +
                  $&quot;&amp;pageSize={_pageSize}&quot;;

        var response = await _httpClient
            .GetStringAsync(url);

        var pageResult = JsonSerializer
            .Deserialize&lt;PageResult&lt;T&gt;&gt;(response);

        if (pageResult?.Items is null ||
            pageResult.Items.Count == 0)
        {
            yield break;
        }

        foreach (var item in pageResult.Items)
        {
            yield return item;
        }

        hasMore = pageResult.Items.Count == _pageSize;
        page++;
    }
}
</code></pre>
</div>
<p>This pattern works for any paginated source -- REST APIs, database cursors, or even file-based batch processing. The key benefit is encapsulation: the consumer iterates without knowing about the pagination protocol. If the API changes its pagination scheme from page numbers to cursor tokens, you update one method and every consumer continues working unchanged. This kind of encapsulation is a core benefit you'll see across many design patterns, including the <a href="https://www.devleader.ca/2026/05/25/chain-of-responsibility-design-pattern-in-c-complete-guide-with-examples">chain of responsibility pattern</a> where each handler hides processing details from the caller.</p>
<h2 id="when-not-to-use-the-iterator-pattern">When NOT to Use the Iterator Pattern</h2>
<p>Knowing when to use the iterator pattern in C# means also knowing when it's the wrong tool. The pattern adds value when you need lazy evaluation, custom traversal, or data source abstraction. In other cases, it introduces complexity without a payoff.</p>
<h3 id="simple-lists-and-arrays">Simple Lists and Arrays</h3>
<p>If your data is already in a <code>List&lt;T&gt;</code>, an array, or any other built-in collection, there's no reason to use the iterator pattern in C# for a custom implementation. The <code>foreach</code> keyword works out of the box. These types implement <code>IEnumerable&lt;T&gt;</code> and provide efficient enumerators. Writing a custom iterator around an existing collection doesn't add value -- it adds a layer of indirection that makes debugging harder and code longer.</p>
<h3 id="random-access-requirements">Random Access Requirements</h3>
<p>Iterators are sequential by design. You move forward through a sequence, one element at a time. If your consumer needs to jump to element 47, access the last element, or index by position, an iterator is the wrong abstraction. Use an indexer, a <code>List&lt;T&gt;</code>, or an array where <code>collection[index]</code> gives you O(1) access.</p>
<h3 id="all-elements-must-be-available-at-once">All Elements Must Be Available at Once</h3>
<p>Some algorithms need the full dataset in memory before they can proceed -- sorting, grouping, aggregating, or finding the median all require the complete collection. If your consumers consistently call <code>.ToList()</code> or <code>.ToArray()</code> on your <code>IEnumerable&lt;T&gt;</code> before doing any work, using the iterator pattern in C# adds overhead without benefit. You're paying the cost of the iterator state machine and then materializing everything anyway. Return a <code>List&lt;T&gt;</code> or <code>IReadOnlyList&lt;T&gt;</code> instead and make the intent clear.</p>
<p>There's one more situation worth flagging.</p>
<h3 id="deferred-execution-causing-subtle-bugs">Deferred Execution Causing Subtle Bugs</h3>
<p>Deferred execution means your iterator's body doesn't run until someone enumerates it. This is usually a feature, but it can cause problems when the data source changes between when you create the iterator and when you consume it. If you yield from a database connection and the connection gets disposed before enumeration starts, you'll get an exception at enumeration time -- not at creation time. This makes bugs harder to trace. When the lifetime of your data source doesn't align with enumeration timing, the iterator pattern in C# can work against you. Consider materializing the results eagerly or using a pattern like <a href="https://www.devleader.ca/2026/05/08/flyweight-design-pattern-in-c-complete-guide-with-examples">the flyweight pattern</a> to share computed results efficiently.</p>
<h2 id="iterator-pattern-in-the.net-ecosystem">Iterator Pattern in the .NET Ecosystem</h2>
<p>The iterator pattern isn't just a textbook concept in C# -- it's woven into the framework at every level. Understanding the iterator pattern in C# makes you better at using the tools .NET provides.</p>
<p><strong>LINQ is built on iterators.</strong> Every LINQ method like <code>Where</code>, <code>Select</code>, and <code>Take</code> returns an <code>IEnumerable&lt;T&gt;</code> that uses deferred execution. When you chain <code>.Where(x =&gt; x &gt; 5).Select(x =&gt; x * 2).Take(10)</code>, no work happens until you enumerate. Each operator is an iterator that pulls from the previous one. Understanding the iterator pattern explains why LINQ is lazy, why calling <code>.ToList()</code> triggers execution, and why you can compose queries without performance penalties until enumeration begins.</p>
<p><strong><code>IAsyncEnumerable&lt;T&gt;</code> extends the pattern to async.</strong> When you need to iterate over data that arrives asynchronously -- reading from a gRPC stream, consuming messages from a channel, or querying a database with streaming results -- <code>IAsyncEnumerable&lt;T&gt;</code> is the async version of the iterator pattern. The <code>await foreach</code> syntax works identically to <code>foreach</code>, and <code>yield return</code> inside an <code>async</code> method produces the async iterator state machine. If you've worked with <a href="https://www.devleader.ca/2024/02/21/iservicecollection-in-c-simplified-beginners-guide-for-dependency-injection">dependency injection through IServiceCollection</a>, you've seen how .NET wires up services behind a clean API. <code>IAsyncEnumerable&lt;T&gt;</code> does the same for async data streams -- it hides the complexity of async iteration behind a simple loop.</p>
<p><strong>Channels use the pattern for producer-consumer scenarios.</strong> <code>System.Threading.Channels</code> provides bounded and unbounded channels where producers write and consumers read. The <code>ChannelReader&lt;T&gt;.ReadAllAsync()</code> method returns an <code>IAsyncEnumerable&lt;T&gt;</code>, letting you consume channel items with <code>await foreach</code>. Without understanding the iterator pattern, the relationship between channels and async enumeration isn't obvious.</p>
<p><strong>System.IO.Pipelines leverages sequential reading.</strong> While pipelines use a different API than <code>IEnumerable&lt;T&gt;</code>, the conceptual model is the same: you read data incrementally from a source without loading everything into memory. The iterator pattern gives you the mental model to understand why pipelines work the way they do and when to choose them over simpler <code>IEnumerable&lt;T&gt;</code> based approaches.</p>
<p>The takeaway is straightforward. Understanding when to use the iterator pattern in C# isn't just about learning an academic design pattern -- you're learning the foundation that powers LINQ, async streams, channels, and half the APIs in the .NET Base Class Library.</p>
<h2 id="frequently-asked-questions">Frequently Asked Questions</h2>
<h3 id="what-is-the-difference-between-ienumerable-and-ienumerator-in-c">What is the difference between IEnumerable and IEnumerator in C#?</h3>
<p><code>IEnumerable&lt;T&gt;</code> represents a sequence that can be iterated. It has one method: <code>GetEnumerator()</code>, which returns an <code>IEnumerator&lt;T&gt;</code>. The enumerator is the actual cursor that tracks where you are in the sequence. It has <code>Current</code> (the element you're pointing at) and <code>MoveNext()</code> (advance to the next element). Think of <code>IEnumerable&lt;T&gt;</code> as the collection and <code>IEnumerator&lt;T&gt;</code> as the bookmark. When you use <code>yield return</code> in C#, the compiler generates both for you automatically, which is why you rarely implement <code>IEnumerator&lt;T&gt;</code> by hand.</p>
<h3 id="should-i-use-yield-return-or-implement-ienumerator-manually">Should I use yield return or implement IEnumerator manually?</h3>
<p>Use <code>yield return</code> in the vast majority of cases. The C# compiler generates the state machine code that <code>IEnumerator&lt;T&gt;</code> requires, handling <code>MoveNext</code>, <code>Current</code>, <code>Reset</code>, and <code>Dispose</code> for you. Manual implementation is only justified when you need fine-grained control over the state machine -- for example, when building a high-performance enumerator that avoids heap allocations by using a <code>struct</code> enumerator. For everyday scenarios like file processing, tree traversal, and API pagination, <code>yield return</code> produces correct, readable, and maintainable code.</p>
<h3 id="how-does-yield-return-work-under-the-hood-in-c">How does yield return work under the hood in C#?</h3>
<p>When the C# compiler encounters <code>yield return</code>, it transforms your method into a state machine class that implements <code>IEnumerable&lt;T&gt;</code> and <code>IEnumerator&lt;T&gt;</code>. Each <code>yield return</code> becomes a state transition. Local variables become fields on the generated class so their values persist across calls to <code>MoveNext()</code>. When the consumer calls <code>MoveNext()</code>, execution resumes from where the last <code>yield return</code> suspended. This is why iterator methods don't execute when you call them -- they return the state machine object. Execution only begins when the consumer starts enumerating.</p>
<h3 id="can-i-use-the-iterator-pattern-with-async-code-in-c">Can I use the iterator pattern with async code in C#?</h3>
<p>Yes. C# supports <code>async</code> iterator methods that return <code>IAsyncEnumerable&lt;T&gt;</code>. You can use <code>yield return</code> inside an <code>async</code> method, and consumers iterate with <code>await foreach</code>. This is essential for streaming data from async sources -- databases, HTTP APIs, message queues, and gRPC streams all benefit from async iteration. The compiler generates an async state machine that handles both the <code>await</code> suspension points and the <code>yield return</code> suspension points, giving you the same lazy evaluation semantics you get with synchronous iterators.</p>
<h3 id="when-should-i-call-tolist-instead-of-keeping-an-ienumerable">When should I call ToList() instead of keeping an IEnumerable?</h3>
<p>Call <code>ToList()</code> when you need to iterate the sequence more than once, when you need <code>Count</code> or index access without re-enumerating, or when the underlying data source might change or be disposed before you finish enumerating. Keep the <code>IEnumerable&lt;T&gt;</code> when you want lazy evaluation, when you might not need all elements, or when memory is a concern and you're working with large datasets. A common mistake is calling <code>ToList()</code> on a LINQ query deep inside a method and then returning <code>IEnumerable&lt;T&gt;</code> from the method -- you've already paid the materialization cost, so return <code>IReadOnlyList&lt;T&gt;</code> to communicate the intent clearly.</p>
<h3 id="how-does-the-iterator-pattern-relate-to-the-observer-pattern">How does the iterator pattern relate to the observer pattern?</h3>
<p>The iterator pattern and the <a href="https://www.devleader.ca/2026/03/26/observer-design-pattern-in-c-complete-guide-with-examples">observer pattern</a> are duals of each other. The iterator pattern is &quot;pull-based&quot; -- the consumer requests the next element when it's ready. The observer pattern is &quot;push-based&quot; -- the producer sends elements to subscribers when they're available. In .NET, <code>IEnumerable&lt;T&gt;</code> represents the pull model and <code>IObservable&lt;T&gt;</code> represents the push model. Understanding both helps you choose the right approach for your data flow. If the consumer controls the pace, use iterators. If the producer controls the pace, use observers.</p>
<h3 id="is-the-iterator-pattern-thread-safe-in-c">Is the iterator pattern thread-safe in C#?</h3>
<p>Not by default. The enumerator state machine generated by <code>yield return</code> is not thread-safe. If multiple threads enumerate the same <code>IEnumerable&lt;T&gt;</code> instance concurrently, each gets its own enumerator (from <code>GetEnumerator()</code>), so that's fine. But if multiple threads share a single <code>IEnumerator&lt;T&gt;</code> and call <code>MoveNext()</code> concurrently, the behavior is undefined. For concurrent scenarios, use <code>IAsyncEnumerable&lt;T&gt;</code> with proper async coordination, or materialize to a thread-safe collection first. Channels in .NET provide a thread-safe producer-consumer model that integrates with <code>IAsyncEnumerable&lt;T&gt;</code> for safe concurrent iteration.</p>
]]></content:encoded>
      <media:content url="https://devleader-d2f9ggbjfpdqcka7.z01.azurefd.net/media/when-to-use-iterator-pattern-csharp.webp" />
    </item>
    <item>
      <guid isPermaLink="false">5e7ee42e-1348-4aa4-9a47-a741027b1729</guid>
      <link>https://www.devleader.ca/2026/06/04/api-versioning-in-aspnet-core-url-header-and-query-string-strategies</link>
      <category>asp.net core api versioning</category>
      <category>aspnet versioning</category>
      <category>api version url header</category>
      <title>API Versioning in ASP.NET Core: URL, Header, and Query String Strategies</title>
      <pubDate>Thu, 04 Jun 2026 21:00:00 Z</pubDate>
      <description><![CDATA[<p><strong>ASP.NET Core API versioning</strong> is a critical practice for maintaining backward compatibility while evolving your API. The moment your API has external consumers -- mobile apps, third-party integrations, partner systems -- you have an obligation to not break them without notice. A versioning strategy lets you introduce breaking changes in a new version while keeping the old version alive long enough for clients to migrate. It signals professional API design. It also forces you to think carefully about what &quot;breaking&quot; means, which is a healthy discipline in itself.</p>
<p>This article covers the full versioning toolkit for .NET 10 ASP.NET Core projects using the <code>Asp.Versioning.Mvc</code> package. You'll see URL segment versioning, query string versioning, header versioning, how to combine multiple strategies, and how to configure OpenAPI documentation for multiple versions.</p>
<h2 id="why-version-your-api">Why Version Your API?</h2>
<p>Most developers know they should version their APIs but skip it at the start because it feels like premature optimization. That reasoning holds until the first time you need to change a response shape or remove a field that a mobile app is reading. At that point, you either break the client or you have to maintain both behaviors in the same endpoint -- which quickly becomes a mess of <code>if (legacyBehavior)</code> branches. Implementing asp.net core api versioning early avoids this pain entirely.</p>
<p>Versioning gives you a clean separation. Breaking changes land in v2. Clients on v1 keep working. You can communicate a deprecation timeline, monitor traffic to know when v1 clients have migrated, and finally decommission v1 when the numbers justify it. This is how public APIs at scale -- GitHub, Stripe, AWS -- manage change without breaking their ecosystems. asp.net core api versioning gives you the same professional-grade change management that top API providers use.</p>
<p>The parallel client support argument is especially important for mobile apps, where you can't force users to update immediately. A v1 response might be in production on app store versions that users haven't updated for months. Your backend needs to support both while you ship improvements in v2.</p>
<h2 id="the-asp.versioning.mvc-nuget-package">The Asp.Versioning.Mvc NuGet Package</h2>
<p>ASP.NET Core doesn't include API versioning in the framework itself. The <code>Asp.Versioning.Mvc</code> NuGet package (third-party, not part of the framework) -- formerly <code>Microsoft.AspNetCore.Mvc.Versioning</code> before it was moved to a separate project -- is a widely used, mature package for controller-based API versioning. It's maintained by the original author with active development and excellent .NET 10 support.</p>
<p>Install it with:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code>dotnet add package Asp.Versioning.Mvc
dotnet add package Asp.Versioning.Mvc.ApiExplorer
</code></pre>
</div>
<p>The second package is needed for OpenAPI/Swagger integration. With those in place, you configure versioning in <code>Program.cs</code>:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">// Program.cs -- API versioning setup with Asp.Versioning.Mvc in .NET 10
using Asp.Versioning;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddApiVersioning(options =&gt;
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
    options.ApiVersionReader = new UrlSegmentApiVersionReader();
})
.AddApiExplorer(options =&gt;
{
    options.GroupNameFormat = &quot;'v'VVV&quot;;
    options.SubstituteApiVersionInUrl = true;
});

var app = builder.Build();

app.UseRouting();
app.UseAuthorization();
app.MapControllers();

app.Run();
</code></pre>
</div>
<p><code>ReportApiVersions = true</code> adds <code>api-supported-versions</code> and <code>api-deprecated-versions</code> response headers to every request. This is a useful signal to API clients -- they can read the headers and notify their users when they're running on a deprecated version. <code>AssumeDefaultVersionWhenUnspecified = true</code> means requests that don't include a version identifier are treated as requesting the default version, which keeps unversioned clients working during a transition.</p>
<h2 id="url-segment-versioning">URL Segment Versioning</h2>
<p>URL segment versioning is the most visible and widely understood asp.net core api versioning strategy. The version appears directly in the URL path: <code>/api/v1/products</code> versus <code>/api/v2/products</code>. It's easy to test in a browser, obvious in API documentation, and straightforward to reason about. The downside is that the URL changes with each version, which violates REST purist principles about stable resource identifiers -- but for pragmatic API design, this is rarely a real problem.</p>
<p>To use URL segment versioning, set the version reader to <code>UrlSegmentApiVersionReader</code> (as shown in the setup above) and configure your controllers with the version in the route template:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">// URL segment versioning -- v1 and v2 controllers side by side

// V1 Products Controller
[ApiController]
[ApiVersion(&quot;1.0&quot;)]
[Route(&quot;api/v{version:apiVersion}/products&quot;)]
public sealed class ProductsV1Controller : ControllerBase
{
    private readonly IProductService _productService;

    public ProductsV1Controller(IProductService productService)
    {
        _productService = productService;
    }

    [HttpGet]
    public async Task&lt;IActionResult&gt; GetAll()
    {
        var products = await _productService.GetAllAsync();
        // V1 returns a flat list
        return Ok(products.Select(p =&gt; new
        {
            p.Id,
            p.Name,
            p.Price
        }));
    }

    [HttpGet(&quot;{id:int}&quot;)]
    public async Task&lt;IActionResult&gt; GetById(int id)
    {
        var product = await _productService.GetByIdAsync(id);
        return product is null ? NotFound() : Ok(new
        {
            product.Id,
            product.Name,
            product.Price
        });
    }
}

// V2 Products Controller -- enriched response shape
[ApiController]
[ApiVersion(&quot;2.0&quot;)]
[Route(&quot;api/v{version:apiVersion}/products&quot;)]
public sealed class ProductsV2Controller : ControllerBase
{
    private readonly IProductService _productService;
    private readonly ICategoryService _categoryService;

    public ProductsV2Controller(
        IProductService productService,
        ICategoryService categoryService)
    {
        _productService = productService;
        _categoryService = categoryService;
    }

    [HttpGet]
    public async Task&lt;IActionResult&gt; GetAll()
    {
        var products = await _productService.GetAllAsync();
        // V2 includes category details and inventory
        return Ok(products.Select(p =&gt; new
        {
            p.Id,
            p.Name,
            p.Price,
            p.CategoryId,
            CategoryName = p.Category?.Name,
            p.StockQuantity,
            p.IsAvailable
        }));
    }

    [HttpGet(&quot;{id:int}&quot;)]
    public async Task&lt;IActionResult&gt; GetById(int id)
    {
        var product = await _productService.GetByIdAsync(id);
        return product is null ? NotFound() : Ok(product);
    }
}
</code></pre>
</div>
<p>Notice that v1 and v2 are entirely separate controllers. This is the cleanest approach -- each version has its own controller with its own logic, and you can evolve them independently. Some teams prefer to put both versions in a single controller using <code>[MapToApiVersion]</code>, which is covered below.</p>
<p>This kind of version-by-behavior design maps well to broader architectural patterns. When you're thinking about how features and versions interact with your system boundaries, <a href="https://www.devleader.ca/2026/07/06/modular-monolith-in-c-complete-implementation-guide-for-net-developers">modular monolith architecture</a> offers a useful lens for organizing the internal structure.</p>
<h2 id="query-string-versioning">Query String Versioning</h2>
<p>Query string versioning appends the version as a URL parameter: <code>/api/products?api-version=1.0</code>. It's less visually prominent than URL segment versioning, which some teams prefer. It keeps the base resource path stable across versions and is easy to add to existing requests without restructuring URLs.</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">// Query string versioning configuration
builder.Services.AddApiVersioning(options =&gt;
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
    options.ApiVersionReader = new QueryStringApiVersionReader(&quot;api-version&quot;);
});

// Controller attributes remain the same -- only the route template changes
[ApiController]
[ApiVersion(&quot;1.0&quot;)]
[Route(&quot;api/products&quot;)] // No version in route -- version comes from query string
public sealed class ProductsController : ControllerBase
{
    // ...
}
</code></pre>
</div>
<p>The default parameter name is <code>api-version</code>, which is conventional. You can rename it by passing a different string to <code>QueryStringApiVersionReader</code>. One trade-off: the route template doesn't include the version, so both v1 and v2 controllers point to the same path. The routing framework disambiguates them using the <code>[ApiVersion]</code> attribute and the query string value.</p>
<h2 id="header-versioning">Header Versioning</h2>
<p>Header versioning reads the API version from a custom HTTP header, typically <code>X-Api-Version</code>. The URL stays completely clean -- <code>/api/products</code> -- and the version is a protocol-level concern expressed in the headers. This is the most &quot;RESTful&quot; approach in the strict sense, but it's the least discoverable. Browsers can't version requests through headers without developer tools, and tools like Postman require an explicit header configuration step.</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">// Header versioning configuration
builder.Services.AddApiVersioning(options =&gt;
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
    options.ApiVersionReader = new HeaderApiVersionReader(&quot;X-Api-Version&quot;);
});
</code></pre>
</div>
<p>Header versioning pairs well with API gateway setups where the gateway can inject or rewrite version headers based on routing rules. Internal microservices behind an API gateway often use header versioning because the URL namespace is already managed by the gateway, and headers are a natural extension point for routing metadata.</p>
<h2 id="combining-multiple-version-readers">Combining Multiple Version Readers</h2>
<p>Limiting your API to a single versioning strategy means clients are forced to use that exact mechanism. A more flexible approach is to support multiple strategies simultaneously. The <code>ApiVersionReader.Combine()</code> method accepts multiple readers and uses whichever one the incoming request provides. If multiple readers find a version identifier, the first match wins.</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">// Supporting URL, query string, and header versioning simultaneously
builder.Services.AddApiVersioning(options =&gt;
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
    options.ApiVersionReader = ApiVersionReader.Combine(
        new UrlSegmentApiVersionReader(),
        new QueryStringApiVersionReader(&quot;api-version&quot;),
        new HeaderApiVersionReader(&quot;X-Api-Version&quot;)
    );
});
</code></pre>
</div>
<p>This is useful during migrations. If you're moving from query string versioning to URL segment versioning, running both readers simultaneously gives clients time to update their requests without a hard cutover.</p>
<h2 id="maptoapiversion-multiple-versions-in-one-controller">MapToApiVersion: Multiple Versions in One Controller</h2>
<p>Sometimes you want to handle two API versions in a single controller -- for example, when the change between v1 and v2 is minor and doesn't justify a separate class. The <code>[MapToApiVersion]</code> attribute lets you map individual actions to specific versions.</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">// Single controller handling both v1 and v2 with [MapToApiVersion]
[ApiController]
[ApiVersion(&quot;1.0&quot;)]
[ApiVersion(&quot;2.0&quot;)]
[Route(&quot;api/v{version:apiVersion}/orders&quot;)]
public sealed class OrdersController : ControllerBase
{
    private readonly IOrderService _orderService;

    public OrdersController(IOrderService orderService)
    {
        _orderService = orderService;
    }

    // This action handles BOTH v1 and v2 requests (same behavior)
    [HttpGet]
    public async Task&lt;IActionResult&gt; GetAll() =&gt;
        Ok(await _orderService.GetAllAsync());

    // V1 response -- simple flat structure
    [HttpGet(&quot;{id:int}&quot;)]
    [MapToApiVersion(&quot;1.0&quot;)]
    public async Task&lt;IActionResult&gt; GetByIdV1(int id)
    {
        var order = await _orderService.GetByIdAsync(id);
        if (order is null) return NotFound();

        return Ok(new { order.Id, order.Status, order.Total });
    }

    // V2 response -- includes line items and shipping info
    [HttpGet(&quot;{id:int}&quot;)]
    [MapToApiVersion(&quot;2.0&quot;)]
    public async Task&lt;IActionResult&gt; GetByIdV2(int id)
    {
        var order = await _orderService.GetByIdAsync(id);
        if (order is null) return NotFound();

        return Ok(order); // Full object with all fields
    }
}
</code></pre>
</div>
<p>This pattern keeps related logic co-located when the difference between versions is small. Use it judiciously -- if the version difference grows significantly, the controller becomes harder to read, and separate controllers are cleaner.</p>
<p>Design patterns can help manage the complexity of multiple versions sharing behavior. The <a href="https://www.devleader.ca/2026/04/30/when-to-use-facade-pattern-in-c-decision-guide-with-examples">facade design pattern</a> is useful for exposing a simplified interface that internally delegates to different version-specific implementations, keeping the controller action thin.</p>
<h2 id="version-deprecation">Version Deprecation</h2>
<p>Marking a version as deprecated tells clients it will eventually be removed without removing it immediately. The <code>[ApiVersion]</code> attribute accepts a <code>Deprecated</code> property:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">[ApiVersion(&quot;1.0&quot;, Deprecated = true)]
[ApiVersion(&quot;2.0&quot;)]
[Route(&quot;api/v{version:apiVersion}/customers&quot;)]
public sealed class CustomersController : ControllerBase
{
    // ...
}
</code></pre>
</div>
<p>When <code>ReportApiVersions = true</code> is set and a client requests a deprecated version, the response includes both <code>api-supported-versions</code> and <code>api-deprecated-versions</code> headers. This lets client developers see which versions are available and which are on their way out. Pair this with communication through your API changelog and documentation to give clients adequate notice before decommissioning.</p>
<h2 id="swaggeropenapi-multi-version-configuration">Swagger/OpenAPI Multi-Version Configuration</h2>
<p>Having multiple API versions without corresponding documentation creates confusion. Before diving into the wiring, it's worth distinguishing three separate pieces that work together here:</p>
<ul>
<li><strong>Built-in ASP.NET Core OpenAPI metadata</strong> -- the <code>Microsoft.AspNetCore.OpenApi</code> package handles metadata generation from your controllers and endpoints. Built-in OpenAPI support first arrived in .NET 9 and is supported in .NET 10.</li>
<li><strong>Swashbuckle (third-party, not part of the framework)</strong> -- provides the Swagger UI and the <code>SwaggerGen</code> middleware that renders your OpenAPI metadata as an interactive documentation page.</li>
<li><strong><code>Asp.Versioning.Mvc.ApiExplorer</code></strong> -- the companion package that teaches the API explorer about your version groups so each version gets its own generated document.</li>
</ul>
<p><code>Asp.Versioning.Mvc.ApiExplorer</code> enables the API explorer to generate separate OpenAPI documents per version. The Swashbuckle integration needs a small amount of wiring:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">// OpenAPI multi-version setup with Swashbuckle in .NET 10
using Asp.Versioning.ApiExplorer;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;

// In Program.cs after AddApiVersioning():
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.ConfigureOptions&lt;ConfigureSwaggerOptions&gt;();

var app = builder.Build();

app.UseSwagger();
app.UseSwaggerUI(options =&gt;
{
    // Dynamically add a Swagger UI endpoint per API version
    var provider = app.Services.GetRequiredService&lt;IApiVersionDescriptionProvider&gt;();
    foreach (var description in provider.ApiVersionDescriptions)
    {
        options.SwaggerEndpoint(
            $&quot;/swagger/{description.GroupName}/swagger.json&quot;,
            $&quot;My API {description.GroupName.ToUpperInvariant()}&quot;);
    }
});

// ConfigureSwaggerOptions generates one Swagger doc per API version
public sealed class ConfigureSwaggerOptions
    : IConfigureNamedOptions&lt;SwaggerGenOptions&gt;
{
    private readonly IApiVersionDescriptionProvider _provider;

    public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider)
    {
        _provider = provider;
    }

    public void Configure(SwaggerGenOptions options)
    {
        foreach (var description in _provider.ApiVersionDescriptions)
        {
            options.SwaggerDoc(description.GroupName, new OpenApiInfo
            {
                Title = &quot;My API&quot;,
                Version = description.ApiVersion.ToString(),
                Description = description.IsDeprecated
                    ? &quot;This API version is deprecated. Please migrate to a newer version.&quot;
                    : &quot;Current stable version.&quot;
            });
        }
    }

    public void Configure(string? name, SwaggerGenOptions options) =&gt;
        Configure(options);
}
</code></pre>
</div>
<p>With this setup, Swagger UI shows a dropdown to switch between API versions. Each version shows only the endpoints relevant to that version -- v1 shows the v1 endpoints, v2 shows the v2 endpoints. This is a significant improvement over a single flat document that includes everything.</p>
<h2 id="default-version-and-assumed-default">Default Version and Assumed Default</h2>
<p>Two settings work together to handle unversioned requests gracefully in asp.net core api versioning. <code>DefaultApiVersion</code> sets which version is returned when the system needs to fall back. <code>AssumeDefaultVersionWhenUnspecified = true</code> means a request that provides no version identifier is treated as requesting the default version rather than returning a 400 error.</p>
<p>These settings are your safety net during migration. If you add versioning to an existing API that has clients with hardcoded URLs (no version in the path or query string), setting the default to v1 and enabling assumed default means those clients continue to work transparently. They're effectively pinned to v1 until they explicitly request v2.</p>
<p>The logging infrastructure that sits beneath your versioned API matters for diagnosing which versions clients are using. <a href="https://www.devleader.ca/2026/07/05/serilog-in-net-complete-guide-to-structured-logging">Serilog in .NET</a> covers structured logging that lets you log the API version on each request and query that data in your log aggregation tool -- critical for understanding migration progress across your client base.</p>
<h2 id="conclusion">Conclusion</h2>
<p><strong>ASP.NET Core API versioning</strong> with <code>Asp.Versioning.Mvc</code> gives you a clean, declarative system for managing change in your API. There is no single universal best strategy -- URL segment, query string, and header versioning each suit different client types and organizational conventions. URL segment versioning is the most visible and broadly understood starting point. Query string and header versioning offer alternatives that suit specific client types or organizational preferences. Combined readers let you support multiple strategies simultaneously, and <code>[MapToApiVersion]</code> keeps related version logic co-located when separation isn't warranted.</p>
<p>The full OpenAPI multi-version setup completes the picture -- your versioned API is well-documented and easy for new clients to explore. Add deprecation markers early and communicate them clearly, and you'll have a versioning system that actually helps clients migrate rather than just announcing changes after the fact.</p>
<p>For a deeper look at how version management fits into broader application architecture decisions, the <a href="https://www.devleader.ca/2026/07/04/monolith-architecture-in-c-the-complete-guide">complete guide to monolith architecture in C#</a> and <a href="https://www.devleader.ca/2026/07/06/modular-monolith-in-c-complete-implementation-guide-for-net-developers">modular monolith patterns</a> provide useful framing for where versioning fits in the larger picture.</p>
<hr />
<h2 id="frequently-asked-questions">Frequently Asked Questions</h2>
<h3 id="what-is-the-best-versioning-strategy-for-asp.net-core-apis">What is the best versioning strategy for ASP.NET Core APIs?</h3>
<p>URL segment versioning is the most commonly recommended starting point because it's immediately visible, easy to test in any HTTP client, and unambiguous in logs and analytics. When you see <code>/api/v2/orders</code> in a log entry, you know exactly which version handled that request. There's no header inspection or query string parsing required.</p>
<p>That said, there's no universal &quot;best&quot; strategy -- it depends on your client types and constraints. If your API is primarily consumed by mobile apps where URLs are hardcoded into app store versions, query string versioning gives you more flexibility without forcing URL changes. If you're building a microservice behind an API gateway, header versioning is often cleaner because the gateway can manage version routing transparently.</p>
<p>The practical recommendation is to start with URL segment versioning unless you have a specific reason not to, and use <code>ApiVersionReader.Combine()</code> if you need to support multiple strategies during a transition.</p>
<h3 id="can-i-add-versioning-to-an-existing-api-without-breaking-existing-clients">Can I add versioning to an existing API without breaking existing clients?</h3>
<p>Yes -- this is exactly the use case for <code>AssumeDefaultVersionWhenUnspecified = true</code> combined with setting <code>DefaultApiVersion</code> to match the version your existing endpoints implicitly implement. Existing clients that don't send a version identifier continue to receive the v1 behavior. You can add v2 alongside it without touching any existing routes.</p>
<p>The key is to introduce versioning attributes to your existing controllers without changing their behavior. Add <code>[ApiVersion(&quot;1.0&quot;)]</code> to your controllers, configure the defaults, and deploy. Existing clients see no difference. Then you can introduce v2 at your own pace, communicate the change to clients, and eventually deprecate v1.</p>
<h3 id="should-i-version-every-endpoint-or-just-breaking-changes">Should I version every endpoint or just breaking changes?</h3>
<p>Version the API surface as a whole rather than individual endpoints. Versioning at the endpoint level creates an inconsistent experience where different resources of the same API are at different versions, which is confusing for clients. When v2 of your products endpoint is ready, it's cleaner to release a v2 of the API that includes the new products behavior along with the unchanged endpoints from v1.</p>
<p>The practical approach is to build v2 as a full copy of v1 that includes your breaking changes. Non-breaking additions -- new optional fields, new endpoints -- can be added to v1 directly. Breaking changes -- removed fields, changed response shapes, different semantics -- go in v2. This keeps v1 stable for existing clients while v2 gets the new behavior.</p>
<h3 id="how-do-i-deprecate-an-api-version-and-notify-clients">How do I deprecate an API version and notify clients?</h3>
<p>Mark the version with <code>[ApiVersion(&quot;1.0&quot;, Deprecated = true)]</code> on your controllers. This causes the framework to include it in the <code>api-deprecated-versions</code> response header when <code>ReportApiVersions = true</code> is set. Clients that check this header can detect they're on a deprecated version.</p>
<p>Beyond the header, communication matters more than the technical mechanism. Document the deprecation timeline in your API changelog and on the Swagger UI page (include a note in the <code>OpenApiInfo.Description</code>). If you have client contact information, direct outreach is more reliable than expecting clients to monitor response headers. Set a realistic sunset date -- six to twelve months is common -- and stick to it.</p>
<h3 id="how-does-versioning-interact-with-openapi-documentation">How does versioning interact with OpenAPI documentation?</h3>
<p>With <code>Asp.Versioning.Mvc.ApiExplorer</code> and Swashbuckle, each API version gets its own generated OpenAPI document. Swagger UI shows a dropdown to switch between versions, and each document contains only the endpoints and schemas relevant to that version. Deprecated versions can include a note in the <code>OpenApiInfo</code> description.</p>
<p>The wiring requires implementing <code>IConfigureNamedOptions&lt;SwaggerGenOptions&gt;</code> to iterate over the available version descriptions and create a <code>SwaggerDoc</code> entry for each, as shown in the code example above. Once that's in place, the endpoint filtering happens automatically -- Swashbuckle reads the <code>ApiVersion</code> attributes and assigns each endpoint to the appropriate document.</p>
<h3 id="what-happens-when-a-client-requests-a-version-that-doesnt-exist">What happens when a client requests a version that doesn't exist?</h3>
<p>By default, the framework returns a 400 Bad Request with a problem details response indicating that the requested API version is unsupported. The response includes the <code>api-supported-versions</code> header so the client knows which versions are available. This is much more informative than a 404 or a generic error message.</p>
<p>You can customize this behavior by implementing a custom <code>IErrorResponseProvider</code> if your API has a specific error format requirement. The default behavior follows the RFC 9110 problem details format, which is appropriate for most APIs and consistent with what the <a href="https://learn.microsoft.com/en-us/aspnet/core/web-api/">ASP.NET Core Web API fundamentals docs</a> recommend.</p>
<h3 id="can-i-use-api-versioning-with-minimal-apis">Can I use API versioning with minimal APIs?</h3>
<p>Yes. <code>Asp.Versioning.Http</code> provides versioning support for minimal APIs. The configuration in <code>Program.cs</code> is similar, but instead of <code>[ApiVersion]</code> attributes on controllers, you call <code>.WithApiVersionSet()</code> and <code>.HasApiVersion()</code> extension methods on endpoints or route groups.</p>
<p>The minimal API versioning syntax looks like this: create an <code>ApiVersionSet</code> using <code>app.NewApiVersionSet(&quot;My API&quot;).Build()</code>, then apply it to endpoints with <code>.WithApiVersionSet(versionSet).HasApiVersion(1, 0)</code>. Route groups can apply a version set at the group level, which cascades to all endpoints in the group. The framework can handle version negotiation and routing the same way regardless of whether you're using controllers or minimal APIs.</p>
]]></description>
      <content:encoded><![CDATA[<p><strong>ASP.NET Core API versioning</strong> is a critical practice for maintaining backward compatibility while evolving your API. The moment your API has external consumers -- mobile apps, third-party integrations, partner systems -- you have an obligation to not break them without notice. A versioning strategy lets you introduce breaking changes in a new version while keeping the old version alive long enough for clients to migrate. It signals professional API design. It also forces you to think carefully about what &quot;breaking&quot; means, which is a healthy discipline in itself.</p>
<p>This article covers the full versioning toolkit for .NET 10 ASP.NET Core projects using the <code>Asp.Versioning.Mvc</code> package. You'll see URL segment versioning, query string versioning, header versioning, how to combine multiple strategies, and how to configure OpenAPI documentation for multiple versions.</p>
<h2 id="why-version-your-api">Why Version Your API?</h2>
<p>Most developers know they should version their APIs but skip it at the start because it feels like premature optimization. That reasoning holds until the first time you need to change a response shape or remove a field that a mobile app is reading. At that point, you either break the client or you have to maintain both behaviors in the same endpoint -- which quickly becomes a mess of <code>if (legacyBehavior)</code> branches. Implementing asp.net core api versioning early avoids this pain entirely.</p>
<p>Versioning gives you a clean separation. Breaking changes land in v2. Clients on v1 keep working. You can communicate a deprecation timeline, monitor traffic to know when v1 clients have migrated, and finally decommission v1 when the numbers justify it. This is how public APIs at scale -- GitHub, Stripe, AWS -- manage change without breaking their ecosystems. asp.net core api versioning gives you the same professional-grade change management that top API providers use.</p>
<p>The parallel client support argument is especially important for mobile apps, where you can't force users to update immediately. A v1 response might be in production on app store versions that users haven't updated for months. Your backend needs to support both while you ship improvements in v2.</p>
<h2 id="the-asp.versioning.mvc-nuget-package">The Asp.Versioning.Mvc NuGet Package</h2>
<p>ASP.NET Core doesn't include API versioning in the framework itself. The <code>Asp.Versioning.Mvc</code> NuGet package (third-party, not part of the framework) -- formerly <code>Microsoft.AspNetCore.Mvc.Versioning</code> before it was moved to a separate project -- is a widely used, mature package for controller-based API versioning. It's maintained by the original author with active development and excellent .NET 10 support.</p>
<p>Install it with:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code>dotnet add package Asp.Versioning.Mvc
dotnet add package Asp.Versioning.Mvc.ApiExplorer
</code></pre>
</div>
<p>The second package is needed for OpenAPI/Swagger integration. With those in place, you configure versioning in <code>Program.cs</code>:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">// Program.cs -- API versioning setup with Asp.Versioning.Mvc in .NET 10
using Asp.Versioning;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddApiVersioning(options =&gt;
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
    options.ApiVersionReader = new UrlSegmentApiVersionReader();
})
.AddApiExplorer(options =&gt;
{
    options.GroupNameFormat = &quot;'v'VVV&quot;;
    options.SubstituteApiVersionInUrl = true;
});

var app = builder.Build();

app.UseRouting();
app.UseAuthorization();
app.MapControllers();

app.Run();
</code></pre>
</div>
<p><code>ReportApiVersions = true</code> adds <code>api-supported-versions</code> and <code>api-deprecated-versions</code> response headers to every request. This is a useful signal to API clients -- they can read the headers and notify their users when they're running on a deprecated version. <code>AssumeDefaultVersionWhenUnspecified = true</code> means requests that don't include a version identifier are treated as requesting the default version, which keeps unversioned clients working during a transition.</p>
<h2 id="url-segment-versioning">URL Segment Versioning</h2>
<p>URL segment versioning is the most visible and widely understood asp.net core api versioning strategy. The version appears directly in the URL path: <code>/api/v1/products</code> versus <code>/api/v2/products</code>. It's easy to test in a browser, obvious in API documentation, and straightforward to reason about. The downside is that the URL changes with each version, which violates REST purist principles about stable resource identifiers -- but for pragmatic API design, this is rarely a real problem.</p>
<p>To use URL segment versioning, set the version reader to <code>UrlSegmentApiVersionReader</code> (as shown in the setup above) and configure your controllers with the version in the route template:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">// URL segment versioning -- v1 and v2 controllers side by side

// V1 Products Controller
[ApiController]
[ApiVersion(&quot;1.0&quot;)]
[Route(&quot;api/v{version:apiVersion}/products&quot;)]
public sealed class ProductsV1Controller : ControllerBase
{
    private readonly IProductService _productService;

    public ProductsV1Controller(IProductService productService)
    {
        _productService = productService;
    }

    [HttpGet]
    public async Task&lt;IActionResult&gt; GetAll()
    {
        var products = await _productService.GetAllAsync();
        // V1 returns a flat list
        return Ok(products.Select(p =&gt; new
        {
            p.Id,
            p.Name,
            p.Price
        }));
    }

    [HttpGet(&quot;{id:int}&quot;)]
    public async Task&lt;IActionResult&gt; GetById(int id)
    {
        var product = await _productService.GetByIdAsync(id);
        return product is null ? NotFound() : Ok(new
        {
            product.Id,
            product.Name,
            product.Price
        });
    }
}

// V2 Products Controller -- enriched response shape
[ApiController]
[ApiVersion(&quot;2.0&quot;)]
[Route(&quot;api/v{version:apiVersion}/products&quot;)]
public sealed class ProductsV2Controller : ControllerBase
{
    private readonly IProductService _productService;
    private readonly ICategoryService _categoryService;

    public ProductsV2Controller(
        IProductService productService,
        ICategoryService categoryService)
    {
        _productService = productService;
        _categoryService = categoryService;
    }

    [HttpGet]
    public async Task&lt;IActionResult&gt; GetAll()
    {
        var products = await _productService.GetAllAsync();
        // V2 includes category details and inventory
        return Ok(products.Select(p =&gt; new
        {
            p.Id,
            p.Name,
            p.Price,
            p.CategoryId,
            CategoryName = p.Category?.Name,
            p.StockQuantity,
            p.IsAvailable
        }));
    }

    [HttpGet(&quot;{id:int}&quot;)]
    public async Task&lt;IActionResult&gt; GetById(int id)
    {
        var product = await _productService.GetByIdAsync(id);
        return product is null ? NotFound() : Ok(product);
    }
}
</code></pre>
</div>
<p>Notice that v1 and v2 are entirely separate controllers. This is the cleanest approach -- each version has its own controller with its own logic, and you can evolve them independently. Some teams prefer to put both versions in a single controller using <code>[MapToApiVersion]</code>, which is covered below.</p>
<p>This kind of version-by-behavior design maps well to broader architectural patterns. When you're thinking about how features and versions interact with your system boundaries, <a href="https://www.devleader.ca/2026/07/06/modular-monolith-in-c-complete-implementation-guide-for-net-developers">modular monolith architecture</a> offers a useful lens for organizing the internal structure.</p>
<h2 id="query-string-versioning">Query String Versioning</h2>
<p>Query string versioning appends the version as a URL parameter: <code>/api/products?api-version=1.0</code>. It's less visually prominent than URL segment versioning, which some teams prefer. It keeps the base resource path stable across versions and is easy to add to existing requests without restructuring URLs.</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">// Query string versioning configuration
builder.Services.AddApiVersioning(options =&gt;
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
    options.ApiVersionReader = new QueryStringApiVersionReader(&quot;api-version&quot;);
});

// Controller attributes remain the same -- only the route template changes
[ApiController]
[ApiVersion(&quot;1.0&quot;)]
[Route(&quot;api/products&quot;)] // No version in route -- version comes from query string
public sealed class ProductsController : ControllerBase
{
    // ...
}
</code></pre>
</div>
<p>The default parameter name is <code>api-version</code>, which is conventional. You can rename it by passing a different string to <code>QueryStringApiVersionReader</code>. One trade-off: the route template doesn't include the version, so both v1 and v2 controllers point to the same path. The routing framework disambiguates them using the <code>[ApiVersion]</code> attribute and the query string value.</p>
<h2 id="header-versioning">Header Versioning</h2>
<p>Header versioning reads the API version from a custom HTTP header, typically <code>X-Api-Version</code>. The URL stays completely clean -- <code>/api/products</code> -- and the version is a protocol-level concern expressed in the headers. This is the most &quot;RESTful&quot; approach in the strict sense, but it's the least discoverable. Browsers can't version requests through headers without developer tools, and tools like Postman require an explicit header configuration step.</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">// Header versioning configuration
builder.Services.AddApiVersioning(options =&gt;
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
    options.ApiVersionReader = new HeaderApiVersionReader(&quot;X-Api-Version&quot;);
});
</code></pre>
</div>
<p>Header versioning pairs well with API gateway setups where the gateway can inject or rewrite version headers based on routing rules. Internal microservices behind an API gateway often use header versioning because the URL namespace is already managed by the gateway, and headers are a natural extension point for routing metadata.</p>
<h2 id="combining-multiple-version-readers">Combining Multiple Version Readers</h2>
<p>Limiting your API to a single versioning strategy means clients are forced to use that exact mechanism. A more flexible approach is to support multiple strategies simultaneously. The <code>ApiVersionReader.Combine()</code> method accepts multiple readers and uses whichever one the incoming request provides. If multiple readers find a version identifier, the first match wins.</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">// Supporting URL, query string, and header versioning simultaneously
builder.Services.AddApiVersioning(options =&gt;
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
    options.ApiVersionReader = ApiVersionReader.Combine(
        new UrlSegmentApiVersionReader(),
        new QueryStringApiVersionReader(&quot;api-version&quot;),
        new HeaderApiVersionReader(&quot;X-Api-Version&quot;)
    );
});
</code></pre>
</div>
<p>This is useful during migrations. If you're moving from query string versioning to URL segment versioning, running both readers simultaneously gives clients time to update their requests without a hard cutover.</p>
<h2 id="maptoapiversion-multiple-versions-in-one-controller">MapToApiVersion: Multiple Versions in One Controller</h2>
<p>Sometimes you want to handle two API versions in a single controller -- for example, when the change between v1 and v2 is minor and doesn't justify a separate class. The <code>[MapToApiVersion]</code> attribute lets you map individual actions to specific versions.</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">// Single controller handling both v1 and v2 with [MapToApiVersion]
[ApiController]
[ApiVersion(&quot;1.0&quot;)]
[ApiVersion(&quot;2.0&quot;)]
[Route(&quot;api/v{version:apiVersion}/orders&quot;)]
public sealed class OrdersController : ControllerBase
{
    private readonly IOrderService _orderService;

    public OrdersController(IOrderService orderService)
    {
        _orderService = orderService;
    }

    // This action handles BOTH v1 and v2 requests (same behavior)
    [HttpGet]
    public async Task&lt;IActionResult&gt; GetAll() =&gt;
        Ok(await _orderService.GetAllAsync());

    // V1 response -- simple flat structure
    [HttpGet(&quot;{id:int}&quot;)]
    [MapToApiVersion(&quot;1.0&quot;)]
    public async Task&lt;IActionResult&gt; GetByIdV1(int id)
    {
        var order = await _orderService.GetByIdAsync(id);
        if (order is null) return NotFound();

        return Ok(new { order.Id, order.Status, order.Total });
    }

    // V2 response -- includes line items and shipping info
    [HttpGet(&quot;{id:int}&quot;)]
    [MapToApiVersion(&quot;2.0&quot;)]
    public async Task&lt;IActionResult&gt; GetByIdV2(int id)
    {
        var order = await _orderService.GetByIdAsync(id);
        if (order is null) return NotFound();

        return Ok(order); // Full object with all fields
    }
}
</code></pre>
</div>
<p>This pattern keeps related logic co-located when the difference between versions is small. Use it judiciously -- if the version difference grows significantly, the controller becomes harder to read, and separate controllers are cleaner.</p>
<p>Design patterns can help manage the complexity of multiple versions sharing behavior. The <a href="https://www.devleader.ca/2026/04/30/when-to-use-facade-pattern-in-c-decision-guide-with-examples">facade design pattern</a> is useful for exposing a simplified interface that internally delegates to different version-specific implementations, keeping the controller action thin.</p>
<h2 id="version-deprecation">Version Deprecation</h2>
<p>Marking a version as deprecated tells clients it will eventually be removed without removing it immediately. The <code>[ApiVersion]</code> attribute accepts a <code>Deprecated</code> property:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">[ApiVersion(&quot;1.0&quot;, Deprecated = true)]
[ApiVersion(&quot;2.0&quot;)]
[Route(&quot;api/v{version:apiVersion}/customers&quot;)]
public sealed class CustomersController : ControllerBase
{
    // ...
}
</code></pre>
</div>
<p>When <code>ReportApiVersions = true</code> is set and a client requests a deprecated version, the response includes both <code>api-supported-versions</code> and <code>api-deprecated-versions</code> headers. This lets client developers see which versions are available and which are on their way out. Pair this with communication through your API changelog and documentation to give clients adequate notice before decommissioning.</p>
<h2 id="swaggeropenapi-multi-version-configuration">Swagger/OpenAPI Multi-Version Configuration</h2>
<p>Having multiple API versions without corresponding documentation creates confusion. Before diving into the wiring, it's worth distinguishing three separate pieces that work together here:</p>
<ul>
<li><strong>Built-in ASP.NET Core OpenAPI metadata</strong> -- the <code>Microsoft.AspNetCore.OpenApi</code> package handles metadata generation from your controllers and endpoints. Built-in OpenAPI support first arrived in .NET 9 and is supported in .NET 10.</li>
<li><strong>Swashbuckle (third-party, not part of the framework)</strong> -- provides the Swagger UI and the <code>SwaggerGen</code> middleware that renders your OpenAPI metadata as an interactive documentation page.</li>
<li><strong><code>Asp.Versioning.Mvc.ApiExplorer</code></strong> -- the companion package that teaches the API explorer about your version groups so each version gets its own generated document.</li>
</ul>
<p><code>Asp.Versioning.Mvc.ApiExplorer</code> enables the API explorer to generate separate OpenAPI documents per version. The Swashbuckle integration needs a small amount of wiring:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">// OpenAPI multi-version setup with Swashbuckle in .NET 10
using Asp.Versioning.ApiExplorer;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;

// In Program.cs after AddApiVersioning():
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.ConfigureOptions&lt;ConfigureSwaggerOptions&gt;();

var app = builder.Build();

app.UseSwagger();
app.UseSwaggerUI(options =&gt;
{
    // Dynamically add a Swagger UI endpoint per API version
    var provider = app.Services.GetRequiredService&lt;IApiVersionDescriptionProvider&gt;();
    foreach (var description in provider.ApiVersionDescriptions)
    {
        options.SwaggerEndpoint(
            $&quot;/swagger/{description.GroupName}/swagger.json&quot;,
            $&quot;My API {description.GroupName.ToUpperInvariant()}&quot;);
    }
});

// ConfigureSwaggerOptions generates one Swagger doc per API version
public sealed class ConfigureSwaggerOptions
    : IConfigureNamedOptions&lt;SwaggerGenOptions&gt;
{
    private readonly IApiVersionDescriptionProvider _provider;

    public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider)
    {
        _provider = provider;
    }

    public void Configure(SwaggerGenOptions options)
    {
        foreach (var description in _provider.ApiVersionDescriptions)
        {
            options.SwaggerDoc(description.GroupName, new OpenApiInfo
            {
                Title = &quot;My API&quot;,
                Version = description.ApiVersion.ToString(),
                Description = description.IsDeprecated
                    ? &quot;This API version is deprecated. Please migrate to a newer version.&quot;
                    : &quot;Current stable version.&quot;
            });
        }
    }

    public void Configure(string? name, SwaggerGenOptions options) =&gt;
        Configure(options);
}
</code></pre>
</div>
<p>With this setup, Swagger UI shows a dropdown to switch between API versions. Each version shows only the endpoints relevant to that version -- v1 shows the v1 endpoints, v2 shows the v2 endpoints. This is a significant improvement over a single flat document that includes everything.</p>
<h2 id="default-version-and-assumed-default">Default Version and Assumed Default</h2>
<p>Two settings work together to handle unversioned requests gracefully in asp.net core api versioning. <code>DefaultApiVersion</code> sets which version is returned when the system needs to fall back. <code>AssumeDefaultVersionWhenUnspecified = true</code> means a request that provides no version identifier is treated as requesting the default version rather than returning a 400 error.</p>
<p>These settings are your safety net during migration. If you add versioning to an existing API that has clients with hardcoded URLs (no version in the path or query string), setting the default to v1 and enabling assumed default means those clients continue to work transparently. They're effectively pinned to v1 until they explicitly request v2.</p>
<p>The logging infrastructure that sits beneath your versioned API matters for diagnosing which versions clients are using. <a href="https://www.devleader.ca/2026/07/05/serilog-in-net-complete-guide-to-structured-logging">Serilog in .NET</a> covers structured logging that lets you log the API version on each request and query that data in your log aggregation tool -- critical for understanding migration progress across your client base.</p>
<h2 id="conclusion">Conclusion</h2>
<p><strong>ASP.NET Core API versioning</strong> with <code>Asp.Versioning.Mvc</code> gives you a clean, declarative system for managing change in your API. There is no single universal best strategy -- URL segment, query string, and header versioning each suit different client types and organizational conventions. URL segment versioning is the most visible and broadly understood starting point. Query string and header versioning offer alternatives that suit specific client types or organizational preferences. Combined readers let you support multiple strategies simultaneously, and <code>[MapToApiVersion]</code> keeps related version logic co-located when separation isn't warranted.</p>
<p>The full OpenAPI multi-version setup completes the picture -- your versioned API is well-documented and easy for new clients to explore. Add deprecation markers early and communicate them clearly, and you'll have a versioning system that actually helps clients migrate rather than just announcing changes after the fact.</p>
<p>For a deeper look at how version management fits into broader application architecture decisions, the <a href="https://www.devleader.ca/2026/07/04/monolith-architecture-in-c-the-complete-guide">complete guide to monolith architecture in C#</a> and <a href="https://www.devleader.ca/2026/07/06/modular-monolith-in-c-complete-implementation-guide-for-net-developers">modular monolith patterns</a> provide useful framing for where versioning fits in the larger picture.</p>
<hr />
<h2 id="frequently-asked-questions">Frequently Asked Questions</h2>
<h3 id="what-is-the-best-versioning-strategy-for-asp.net-core-apis">What is the best versioning strategy for ASP.NET Core APIs?</h3>
<p>URL segment versioning is the most commonly recommended starting point because it's immediately visible, easy to test in any HTTP client, and unambiguous in logs and analytics. When you see <code>/api/v2/orders</code> in a log entry, you know exactly which version handled that request. There's no header inspection or query string parsing required.</p>
<p>That said, there's no universal &quot;best&quot; strategy -- it depends on your client types and constraints. If your API is primarily consumed by mobile apps where URLs are hardcoded into app store versions, query string versioning gives you more flexibility without forcing URL changes. If you're building a microservice behind an API gateway, header versioning is often cleaner because the gateway can manage version routing transparently.</p>
<p>The practical recommendation is to start with URL segment versioning unless you have a specific reason not to, and use <code>ApiVersionReader.Combine()</code> if you need to support multiple strategies during a transition.</p>
<h3 id="can-i-add-versioning-to-an-existing-api-without-breaking-existing-clients">Can I add versioning to an existing API without breaking existing clients?</h3>
<p>Yes -- this is exactly the use case for <code>AssumeDefaultVersionWhenUnspecified = true</code> combined with setting <code>DefaultApiVersion</code> to match the version your existing endpoints implicitly implement. Existing clients that don't send a version identifier continue to receive the v1 behavior. You can add v2 alongside it without touching any existing routes.</p>
<p>The key is to introduce versioning attributes to your existing controllers without changing their behavior. Add <code>[ApiVersion(&quot;1.0&quot;)]</code> to your controllers, configure the defaults, and deploy. Existing clients see no difference. Then you can introduce v2 at your own pace, communicate the change to clients, and eventually deprecate v1.</p>
<h3 id="should-i-version-every-endpoint-or-just-breaking-changes">Should I version every endpoint or just breaking changes?</h3>
<p>Version the API surface as a whole rather than individual endpoints. Versioning at the endpoint level creates an inconsistent experience where different resources of the same API are at different versions, which is confusing for clients. When v2 of your products endpoint is ready, it's cleaner to release a v2 of the API that includes the new products behavior along with the unchanged endpoints from v1.</p>
<p>The practical approach is to build v2 as a full copy of v1 that includes your breaking changes. Non-breaking additions -- new optional fields, new endpoints -- can be added to v1 directly. Breaking changes -- removed fields, changed response shapes, different semantics -- go in v2. This keeps v1 stable for existing clients while v2 gets the new behavior.</p>
<h3 id="how-do-i-deprecate-an-api-version-and-notify-clients">How do I deprecate an API version and notify clients?</h3>
<p>Mark the version with <code>[ApiVersion(&quot;1.0&quot;, Deprecated = true)]</code> on your controllers. This causes the framework to include it in the <code>api-deprecated-versions</code> response header when <code>ReportApiVersions = true</code> is set. Clients that check this header can detect they're on a deprecated version.</p>
<p>Beyond the header, communication matters more than the technical mechanism. Document the deprecation timeline in your API changelog and on the Swagger UI page (include a note in the <code>OpenApiInfo.Description</code>). If you have client contact information, direct outreach is more reliable than expecting clients to monitor response headers. Set a realistic sunset date -- six to twelve months is common -- and stick to it.</p>
<h3 id="how-does-versioning-interact-with-openapi-documentation">How does versioning interact with OpenAPI documentation?</h3>
<p>With <code>Asp.Versioning.Mvc.ApiExplorer</code> and Swashbuckle, each API version gets its own generated OpenAPI document. Swagger UI shows a dropdown to switch between versions, and each document contains only the endpoints and schemas relevant to that version. Deprecated versions can include a note in the <code>OpenApiInfo</code> description.</p>
<p>The wiring requires implementing <code>IConfigureNamedOptions&lt;SwaggerGenOptions&gt;</code> to iterate over the available version descriptions and create a <code>SwaggerDoc</code> entry for each, as shown in the code example above. Once that's in place, the endpoint filtering happens automatically -- Swashbuckle reads the <code>ApiVersion</code> attributes and assigns each endpoint to the appropriate document.</p>
<h3 id="what-happens-when-a-client-requests-a-version-that-doesnt-exist">What happens when a client requests a version that doesn't exist?</h3>
<p>By default, the framework returns a 400 Bad Request with a problem details response indicating that the requested API version is unsupported. The response includes the <code>api-supported-versions</code> header so the client knows which versions are available. This is much more informative than a 404 or a generic error message.</p>
<p>You can customize this behavior by implementing a custom <code>IErrorResponseProvider</code> if your API has a specific error format requirement. The default behavior follows the RFC 9110 problem details format, which is appropriate for most APIs and consistent with what the <a href="https://learn.microsoft.com/en-us/aspnet/core/web-api/">ASP.NET Core Web API fundamentals docs</a> recommend.</p>
<h3 id="can-i-use-api-versioning-with-minimal-apis">Can I use API versioning with minimal APIs?</h3>
<p>Yes. <code>Asp.Versioning.Http</code> provides versioning support for minimal APIs. The configuration in <code>Program.cs</code> is similar, but instead of <code>[ApiVersion]</code> attributes on controllers, you call <code>.WithApiVersionSet()</code> and <code>.HasApiVersion()</code> extension methods on endpoints or route groups.</p>
<p>The minimal API versioning syntax looks like this: create an <code>ApiVersionSet</code> using <code>app.NewApiVersionSet(&quot;My API&quot;).Build()</code>, then apply it to endpoints with <code>.WithApiVersionSet(versionSet).HasApiVersion(1, 0)</code>. Route groups can apply a version set at the group level, which cascades to all endpoints in the group. The framework can handle version negotiation and routing the same way regardless of whether you're using controllers or minimal APIs.</p>
]]></content:encoded>
      <media:content url="https://devleader-d2f9ggbjfpdqcka7.z01.azurefd.net/media/aspnet-core-api-versioning.webp" />
    </item>
    <item>
      <guid isPermaLink="false">6b21da72-2f9e-4402-9f71-c3bc484b9441</guid>
      <link>https://www.devleader.ca/2026/06/04/chain-of-responsibility-pattern-realworld-example-in-c-complete-implementation</link>
      <category>chain of responsibility pattern</category>
      <category>c#</category>
      <category>real world example</category>
      <category>design patterns</category>
      <category>implementation</category>
      <title>Chain of Responsibility Pattern Real-World Example in C#: Complete Implementation</title>
      <pubDate>Thu, 04 Jun 2026 13:00:00 Z</pubDate>
      <description><![CDATA[<h1 id="chain-of-responsibility-pattern-real-world-example-in-c-complete-implementation">Chain of Responsibility Pattern Real-World Example in C#: Complete Implementation</h1>
<p>Reading about design patterns in the abstract only gets you so far. A <strong>chain of responsibility pattern real world example in C#</strong> with real infrastructure concerns is what cements the concept into something you can actually use at work. This article walks through a complete HTTP request processing pipeline -- the kind you'd build for an API gateway -- where each handler in the chain decides whether a request should proceed or get rejected. By the end, you'll have a fully implemented chain covering rate limiting, authentication, authorization, validation, and routing -- all configurable through <a href="https://www.devleader.ca/2024/02/21/iservicecollection-in-c-simplified-beginners-guide-for-dependency-injection">dependency injection</a>.</p>
<h2 id="the-problem-http-request-processing-pipeline">The Problem: HTTP Request Processing Pipeline</h2>
<p>Imagine you're building an API gateway that sits in front of several backend services. Every incoming request needs to pass through several stages before it reaches its destination: rate limiting checks the caller's quota, authentication verifies the JWT token, and authorization confirms the right permissions.</p>
<p>After those security gates, the request still needs body validation and routing to the correct backend service. Without a structured approach, this logic collapses into a massive nested if/else block:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">public sealed class MonolithicGateway
{
    public async Task&lt;GatewayResponse&gt; ProcessAsync(
        GatewayRequest request)
    {
        if (IsRateLimited(request.ClientId))
            return new GatewayResponse(429, &quot;Rate limit exceeded&quot;);

        if (!IsValidToken(request.AuthToken))
            return new GatewayResponse(401, &quot;Invalid token&quot;);

        if (!HasPermission(request.UserId, request.Resource))
            return new GatewayResponse(403, &quot;Forbidden&quot;);

        if (!IsValidPayload(request.Body))
            return new GatewayResponse(400, &quot;Invalid request body&quot;);

        return await RouteToServiceAsync(request);
    }
}
</code></pre>
</div>
<p>This works for five checks. But real API gateways accumulate dozens of concerns over time -- logging, tracing, CORS, request transformation. Each new concern makes the method longer and harder to test. The chain of responsibility pattern solves this by turning each concern into an independent handler that can be composed, reordered, and tested in isolation.</p>
<h2 id="designing-the-handler-base">Designing the Handler Base</h2>
<p>The foundation of our chain of responsibility pattern real world example in C# is a pair of types: a request/response model and an abstract handler that defines the chaining mechanism. Let's start with the request and response:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">namespace Gateway.Pipeline;

public sealed class GatewayRequest
{
    public required string ClientId { get; init; }

    public required string AuthToken { get; init; }

    public required string UserId { get; init; }

    public required string Resource { get; init; }

    public required string HttpMethod { get; init; }

    public required string Path { get; init; }

    public string? Body { get; init; }
}

public sealed class GatewayResponse
{
    public int StatusCode { get; init; }

    public string Message { get; init; }

    public GatewayResponse(int statusCode, string message)
    {
        StatusCode = statusCode;
        Message = message;
    }
}
</code></pre>
</div>
<p>Now for the abstract handler that all concrete implementations will extend:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">namespace Gateway.Pipeline;

public abstract class RequestHandler
{
    private RequestHandler? _next;

    public RequestHandler SetNext(RequestHandler next)
    {
        _next = next;
        return next;
    }

    public virtual async Task&lt;GatewayResponse&gt; HandleAsync(
        GatewayRequest request)
    {
        if (_next is not null)
        {
            return await _next.HandleAsync(request);
        }

        return new GatewayResponse(
            500,
            &quot;No handler processed the request&quot;);
    }
}
</code></pre>
</div>
<p>The <code>SetNext</code> method wires handlers together and returns the next handler for fluent chaining. The <code>HandleAsync</code> method provides default pass-through behavior -- if a concrete handler doesn't short-circuit, it delegates to the next handler. If you've worked with the <a href="https://www.devleader.ca/2026/03/02/strategy-design-pattern-in-c-complete-guide-with-examples">strategy design pattern</a>, you'll notice a similar emphasis on isolating behavior behind a common contract -- except here, handlers are linked sequentially rather than swapped interchangeably.</p>
<h2 id="implementing-the-handler-chain">Implementing the Handler Chain</h2>
<p>With the base class in place, let's build five concrete handlers. Each one examines the request, either rejects it or passes it along.</p>
<h3 id="ratelimithandler">RateLimitHandler</h3>
<p>The first handler checks whether the client has exceeded their allowed request rate:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">using System.Collections.Concurrent;

namespace Gateway.Pipeline.Handlers;

public sealed class RateLimitHandler : RequestHandler
{
    private readonly ConcurrentDictionary&lt;string, ClientRateInfo&gt;
        _clients = new();
    private readonly int _maxRequests;
    private readonly TimeSpan _window;

    public RateLimitHandler(
        int maxRequests = 100,
        TimeSpan? window = null)
    {
        _maxRequests = maxRequests;
        _window = window ?? TimeSpan.FromMinutes(1);
    }

    public override async Task&lt;GatewayResponse&gt; HandleAsync(
        GatewayRequest request)
    {
        var info = _clients.GetOrAdd(
            request.ClientId,
            _ =&gt; new ClientRateInfo());

        if (info.IsExpired(_window))
        {
            info.Reset();
        }

        info.IncrementCount();

        if (info.Count &gt; _maxRequests)
        {
            return new GatewayResponse(
                429,
                &quot;Rate limit exceeded&quot;);
        }

        return await base.HandleAsync(request);
    }
}

public sealed class ClientRateInfo
{
    public int Count { get; private set; }

    public DateTimeOffset WindowStart { get; private set; }
        = DateTimeOffset.UtcNow;

    public bool IsExpired(TimeSpan window) =&gt;
        DateTimeOffset.UtcNow - WindowStart &gt; window;

    public void Reset()
    {
        Count = 0;
        WindowStart = DateTimeOffset.UtcNow;
    }

    public void IncrementCount() =&gt; Count++;
}
</code></pre>
</div>
<p>When the client is within their limit, the handler calls <code>base.HandleAsync(request)</code> to pass the request down the chain. When the limit is exceeded, it short-circuits with a 429 response.</p>
<h3 id="authenticationhandler">AuthenticationHandler</h3>
<p>The authentication handler validates the JWT token attached to the request:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">namespace Gateway.Pipeline.Handlers;

public sealed class AuthenticationHandler : RequestHandler
{
    private readonly ITokenValidator _tokenValidator;

    public AuthenticationHandler(
        ITokenValidator tokenValidator)
    {
        _tokenValidator = tokenValidator;
    }

    public override async Task&lt;GatewayResponse&gt; HandleAsync(
        GatewayRequest request)
    {
        if (string.IsNullOrEmpty(request.AuthToken))
        {
            return new GatewayResponse(
                401,
                &quot;Missing authentication token&quot;);
        }

        var isValid = await _tokenValidator
            .ValidateAsync(request.AuthToken);

        if (!isValid)
        {
            return new GatewayResponse(
                401,
                &quot;Invalid or expired token&quot;);
        }

        return await base.HandleAsync(request);
    }
}

public interface ITokenValidator
{
    Task&lt;bool&gt; ValidateAsync(string token);
}
</code></pre>
</div>
<p>The <code>ITokenValidator</code> interface keeps the handler testable -- this is the same <a href="https://www.devleader.ca/2024/01/07/what-is-inversion-of-control-a-simplified-beginners-guide">inversion of control</a> principle that makes the whole chain composable.</p>
<h3 id="authorizationhandler">AuthorizationHandler</h3>
<p>The authorization handler checks whether the user has permission to access the requested resource:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">namespace Gateway.Pipeline.Handlers;

public sealed class AuthorizationHandler : RequestHandler
{
    private readonly IAuthorizationService _authService;

    public AuthorizationHandler(
        IAuthorizationService authService)
    {
        _authService = authService;
    }

    public override async Task&lt;GatewayResponse&gt; HandleAsync(
        GatewayRequest request)
    {
        var isAuthorized = await _authService
            .IsAuthorizedAsync(
                request.UserId,
                request.Resource,
                request.HttpMethod);

        if (!isAuthorized)
        {
            return new GatewayResponse(
                403,
                &quot;Insufficient permissions&quot;);
        }

        return await base.HandleAsync(request);
    }
}

public interface IAuthorizationService
{
    Task&lt;bool&gt; IsAuthorizedAsync(
        string userId,
        string resource,
        string httpMethod);
}
</code></pre>
</div>
<p>The pattern is consistent across every handler: check a condition, return an error response if it fails, or delegate to the next handler.</p>
<h3 id="inputvalidationhandler">InputValidationHandler</h3>
<p>The validation handler examines the request body for write operations and passes read operations through:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">namespace Gateway.Pipeline.Handlers;

public sealed class InputValidationHandler : RequestHandler
{
    private readonly IRequestBodyValidator _bodyValidator;

    public InputValidationHandler(
        IRequestBodyValidator bodyValidator)
    {
        _bodyValidator = bodyValidator;
    }

    public override async Task&lt;GatewayResponse&gt; HandleAsync(
        GatewayRequest request)
    {
        var requiresBody = request.HttpMethod is
            &quot;POST&quot; or &quot;PUT&quot; or &quot;PATCH&quot;;

        if (requiresBody &amp;&amp;
            string.IsNullOrWhiteSpace(request.Body))
        {
            return new GatewayResponse(
                400,
                &quot;Request body is required&quot;);
        }

        if (requiresBody)
        {
            var validation = await _bodyValidator
                .ValidateAsync(
                    request.Path,
                    request.Body!);

            if (!validation.IsValid)
            {
                return new GatewayResponse(
                    400,
                    $&quot;Validation failed: &quot; +
                    $&quot;{validation.ErrorMessage}&quot;);
            }
        }

        return await base.HandleAsync(request);
    }
}

public interface IRequestBodyValidator
{
    Task&lt;ValidationResult&gt; ValidateAsync(
        string path,
        string body);
}

public sealed class ValidationResult
{
    public bool IsValid { get; init; }

    public string? ErrorMessage { get; init; }

    public static ValidationResult Success() =&gt;
        new() { IsValid = true };

    public static ValidationResult Failure(string error) =&gt;
        new() { IsValid = false, ErrorMessage = error };
}
</code></pre>
</div>
<p>Not every handler needs to act on every request -- GET and DELETE requests skip body validation entirely.</p>
<h3 id="requestroutinghandler">RequestRoutingHandler</h3>
<p>The final handler routes the validated request to the appropriate backend service:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">namespace Gateway.Pipeline.Handlers;

public sealed class RequestRoutingHandler : RequestHandler
{
    private readonly IServiceRouter _router;

    public RequestRoutingHandler(IServiceRouter router)
    {
        _router = router;
    }

    public override async Task&lt;GatewayResponse&gt; HandleAsync(
        GatewayRequest request)
    {
        var result = await _router.RouteAsync(
            request.Path,
            request.HttpMethod,
            request.Body);

        return new GatewayResponse(
            result.StatusCode,
            result.ResponseBody);
    }
}

public interface IServiceRouter
{
    Task&lt;RouteResult&gt; RouteAsync(
        string path,
        string httpMethod,
        string? body);
}

public sealed class RouteResult
{
    public required int StatusCode { get; init; }

    public required string ResponseBody { get; init; }
}
</code></pre>
</div>
<p>This handler intentionally does <em>not</em> call <code>base.HandleAsync</code> -- it's the end of the chain. If you're familiar with the <a href="https://www.devleader.ca/2026/04/02/composite-design-pattern-in-c-complete-guide-with-examples">composite design pattern</a>, the distinction is worth noting: composite delegates recursively to children, while chain of responsibility delegates along a flat sequence with explicit short-circuiting.</p>
<h2 id="building-and-configuring-the-chain">Building and Configuring the Chain</h2>
<p>Having five handlers is great, but you need a clean way to assemble them. A factory class combined with <a href="https://www.devleader.ca/2024/02/21/iservicecollection-in-c-simplified-beginners-guide-for-dependency-injection">IServiceCollection registration</a> gives you a flexible, configuration-driven pipeline:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">using Microsoft.Extensions.DependencyInjection;

namespace Gateway.Pipeline;

public static class PipelineServiceExtensions
{
    public static IServiceCollection AddGatewayPipeline(
        this IServiceCollection services)
    {
        services.AddSingleton&lt;RateLimitHandler&gt;();
        services.AddSingleton&lt;AuthenticationHandler&gt;();
        services.AddSingleton&lt;AuthorizationHandler&gt;();
        services.AddSingleton&lt;InputValidationHandler&gt;();
        services.AddSingleton&lt;RequestRoutingHandler&gt;();
        services.AddSingleton&lt;IPipelineFactory,
            PipelineFactory&gt;();

        return services;
    }
}

public interface IPipelineFactory
{
    RequestHandler CreatePipeline();
}

public sealed class PipelineFactory : IPipelineFactory
{
    private readonly IServiceProvider _serviceProvider;

    public PipelineFactory(
        IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public RequestHandler CreatePipeline()
    {
        var handlers = new RequestHandler[]
        {
            _serviceProvider
                .GetRequiredService&lt;RateLimitHandler&gt;(),
            _serviceProvider
                .GetRequiredService&lt;AuthenticationHandler&gt;(),
            _serviceProvider
                .GetRequiredService&lt;AuthorizationHandler&gt;(),
            _serviceProvider
                .GetRequiredService&lt;InputValidationHandler&gt;(),
            _serviceProvider
                .GetRequiredService&lt;RequestRoutingHandler&gt;(),
        };

        for (int i = 0; i &lt; handlers.Length - 1; i++)
        {
            handlers[i].SetNext(handlers[i + 1]);
        }

        return handlers[0];
    }
}
</code></pre>
</div>
<p>The factory resolves each handler from the DI container and chains them in a loop. The order in the array is the order of execution -- if you need to insert a new handler, you add one line to the array. This separation between construction and execution is the same principle behind <a href="https://www.devleader.ca/2024/01/07/what-is-inversion-of-control-a-simplified-beginners-guide">inversion of control</a>. Using the factory from application code is straightforward:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">var pipeline = pipelineFactory.CreatePipeline();
var response = await pipeline.HandleAsync(request);
</code></pre>
</div><h2 id="adding-resilience-and-observability">Adding Resilience and Observability</h2>
<p>A chain of responsibility pattern real world example in C# isn't complete without addressing production concerns. Let's enhance the pipeline with structured logging, metrics, and circuit breaker support.</p>
<p>The cleanest approach is an observability-aware base handler that wraps each handler with cross-cutting concerns:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">using System.Diagnostics;

using Microsoft.Extensions.Logging;

namespace Gateway.Pipeline;

public abstract class ObservableHandler : RequestHandler
{
    private readonly ILogger _logger;
    private readonly string _handlerName;

    protected ObservableHandler(ILogger logger)
    {
        _logger = logger;
        _handlerName = GetType().Name;
    }

    public override async Task&lt;GatewayResponse&gt; HandleAsync(
        GatewayRequest request)
    {
        var stopwatch = Stopwatch.StartNew();

        _logger.LogInformation(
            &quot;Handler {HandlerName} processing &quot; +
            &quot;request for {Path}&quot;,
            _handlerName,
            request.Path);

        try
        {
            var response = await ProcessAsync(request);
            stopwatch.Stop();

            _logger.LogInformation(
                &quot;Handler {HandlerName} completed &quot; +
                &quot;in {ElapsedMs}ms with status {StatusCode}&quot;,
                _handlerName,
                stopwatch.ElapsedMilliseconds,
                response.StatusCode);

            return response;
        }
        catch (Exception ex)
        {
            stopwatch.Stop();

            _logger.LogError(
                ex,
                &quot;Handler {HandlerName} failed &quot; +
                &quot;after {ElapsedMs}ms&quot;,
                _handlerName,
                stopwatch.ElapsedMilliseconds);

            return new GatewayResponse(
                500,
                $&quot;Internal error in {_handlerName}&quot;);
        }
    }

    protected abstract Task&lt;GatewayResponse&gt; ProcessAsync(
        GatewayRequest request);
}
</code></pre>
</div>
<p>This base class wraps every handler execution with timing, structured logging, and exception handling. Concrete handlers override <code>ProcessAsync</code> instead of <code>HandleAsync</code> -- the observable base adds instrumentation transparently. If you've worked with the <a href="https://www.devleader.ca/2026/03/26/observer-design-pattern-in-c-complete-guide-with-examples">observer design pattern</a>, the concept is similar: external observers react to handler execution without modifying core logic.</p>
<p>For circuit breaker support, create a state object that any handler calling external services can check:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">namespace Gateway.Pipeline.Handlers;

public sealed class CircuitBreakerState
{
    private int _failureCount;
    private DateTimeOffset _lastFailure;

    public int FailureThreshold { get; init; } = 5;

    public TimeSpan RecoveryWindow { get; init; }
        = TimeSpan.FromSeconds(30);

    public bool IsOpen =&gt;
        _failureCount &gt;= FailureThreshold &amp;&amp;
        DateTimeOffset.UtcNow - _lastFailure
            &lt; RecoveryWindow;

    public void RecordFailure()
    {
        _failureCount++;
        _lastFailure = DateTimeOffset.UtcNow;
    }

    public void Reset() =&gt; _failureCount = 0;
}
</code></pre>
</div>
<p>This state object can be injected into any handler that calls external services. When the circuit is open, the handler returns a 503 immediately instead of forwarding the request to a service that's likely down.</p>
<h2 id="testing-the-complete-pipeline">Testing the Complete Pipeline</h2>
<p>Testing is where the chain of responsibility pattern shines. Because each handler is a separate class with injected dependencies, you can write focused tests for individual handlers and the full chain.</p>
<p>First, test helpers and stub dependencies:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">using Xunit;

namespace Gateway.Pipeline.Tests;

public static class TestRequestFactory
{
    public static GatewayRequest CreateValid() =&gt;
        new()
        {
            ClientId = &quot;client-1&quot;,
            AuthToken = &quot;valid-token&quot;,
            UserId = &quot;user-1&quot;,
            Resource = &quot;/api/orders&quot;,
            HttpMethod = &quot;GET&quot;,
            Path = &quot;/api/orders&quot;,
            Body = null,
        };
}

public sealed class StubTokenValidator : ITokenValidator
{
    private readonly bool _isValid;

    public StubTokenValidator(bool isValid) =&gt;
        _isValid = isValid;

    public Task&lt;bool&gt; ValidateAsync(string token) =&gt;
        Task.FromResult(_isValid);
}

public sealed class StubAuthorizationService
    : IAuthorizationService
{
    private readonly bool _isAuthorized;

    public StubAuthorizationService(bool isAuthorized) =&gt;
        _isAuthorized = isAuthorized;

    public Task&lt;bool&gt; IsAuthorizedAsync(
        string userId,
        string resource,
        string httpMethod) =&gt;
        Task.FromResult(_isAuthorized);
}

public sealed class StubServiceRouter : IServiceRouter
{
    public Task&lt;RouteResult&gt; RouteAsync(
        string path,
        string httpMethod,
        string? body) =&gt;
        Task.FromResult(new RouteResult
        {
            StatusCode = 200,
            ResponseBody = &quot;OK&quot;,
        });
}

public sealed class StubRequestBodyValidator
    : IRequestBodyValidator
{
    private readonly bool _isValid;

    public StubRequestBodyValidator(bool isValid) =&gt;
        _isValid = isValid;

    public Task&lt;ValidationResult&gt; ValidateAsync(
        string path,
        string body) =&gt;
        Task.FromResult(
            _isValid
                ? ValidationResult.Success()
                : ValidationResult.Failure(
                    &quot;Invalid body&quot;));
}
}
</code></pre>
</div>
<p>Now, individual handler tests:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">namespace Gateway.Pipeline.Tests;

public sealed class AuthenticationHandlerTests
{
    [Fact]
    public async Task HandleAsync_MissingToken_Returns401()
    {
        var handler = new AuthenticationHandler(
            new StubTokenValidator(true));

        var request = new GatewayRequest
        {
            ClientId = &quot;c1&quot;,
            AuthToken = string.Empty,
            UserId = &quot;u1&quot;,
            Resource = &quot;/api/orders&quot;,
            HttpMethod = &quot;GET&quot;,
            Path = &quot;/api/orders&quot;,
        };

        var response = await handler.HandleAsync(request);

        Assert.Equal(401, response.StatusCode);
    }

    [Fact]
    public async Task HandleAsync_ValidToken_DelegatesToNext()
    {
        var handler = new AuthenticationHandler(
            new StubTokenValidator(true));

        var routing = new RequestRoutingHandler(
            new StubServiceRouter());

        handler.SetNext(routing);

        var request = TestRequestFactory.CreateValid();

        var response = await handler.HandleAsync(request);

        Assert.Equal(200, response.StatusCode);
    }
}
</code></pre>
</div>
<p>Full chain integration tests verify the handlers work together:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">namespace Gateway.Pipeline.Tests;

public sealed class FullPipelineTests
{
    private RequestHandler BuildChain(
        bool tokenValid = true,
        bool authorized = true)
    {
        var rateLimit = new RateLimitHandler(
            maxRequests: 100);

        var auth = new AuthenticationHandler(
            new StubTokenValidator(tokenValid));

        var authz = new AuthorizationHandler(
            new StubAuthorizationService(authorized));

        var validation = new InputValidationHandler(
            new StubRequestBodyValidator(true));

        var routing = new RequestRoutingHandler(
            new StubServiceRouter());

        rateLimit.SetNext(auth);
        auth.SetNext(authz);
        authz.SetNext(validation);
        validation.SetNext(routing);

        return rateLimit;
    }

    [Fact]
    public async Task
        HandleAsync_ValidRequest_Returns200()
    {
        var pipeline = BuildChain();
        var request = TestRequestFactory.CreateValid();

        var response = await pipeline
            .HandleAsync(request);

        Assert.Equal(200, response.StatusCode);
        Assert.Equal(&quot;OK&quot;, response.Message);
    }

    [Fact]
    public async Task
        HandleAsync_InvalidToken_ShortCircuitsAt401()
    {
        var pipeline = BuildChain(tokenValid: false);
        var request = TestRequestFactory.CreateValid();

        var response = await pipeline
            .HandleAsync(request);

        Assert.Equal(401, response.StatusCode);
    }

    [Fact]
    public async Task
        HandleAsync_Unauthorized_ShortCircuitsAt403()
    {
        var pipeline = BuildChain(authorized: false);
        var request = TestRequestFactory.CreateValid();

        var response = await pipeline
            .HandleAsync(request);

        Assert.Equal(403, response.StatusCode);
    }

    [Fact]
    public async Task
        HandleAsync_ExceedsRateLimit_Returns429()
    {
        var rateLimit = new RateLimitHandler(
            maxRequests: 2);

        var routing = new RequestRoutingHandler(
            new StubServiceRouter());

        rateLimit.SetNext(routing);

        var request = TestRequestFactory.CreateValid();

        await rateLimit.HandleAsync(request);
        await rateLimit.HandleAsync(request);
        var response = await rateLimit
            .HandleAsync(request);

        Assert.Equal(429, response.StatusCode);
    }
}
</code></pre>
</div>
<p>The <code>BuildChain</code> helper accepts parameters that control which stubs succeed or fail, making it trivial to test every short-circuit point without duplicating setup code. Compare this to the monolithic gateway -- you'd need to mock every dependency simultaneously and verify behavior through a single sprawling method.</p>
<p>If you're interested in how the <a href="https://www.devleader.ca/2026/05/13/proxy-design-pattern-in-c-complete-guide-with-examples">proxy design pattern</a> or the <a href="https://www.devleader.ca/2026/03/14/decorator-design-pattern-in-c-complete-guide-with-examples">decorator design pattern</a> handle similar concerns with different structural tradeoffs, those patterns are worth comparing.</p>
<h2 id="frequently-asked-questions">Frequently Asked Questions</h2>
<h3 id="how-is-the-chain-of-responsibility-pattern-different-from-middleware-in-asp.net-core">How is the chain of responsibility pattern different from middleware in ASP.NET Core?</h3>
<p>ASP.NET Core middleware and the chain of responsibility pattern share the same fundamental idea: a request flows through a sequence of processing steps, and any step can short-circuit the pipeline. The key difference is structural. Middleware uses a delegate-based pipeline managed by the framework, while the chain of responsibility pattern uses explicit object references with a <code>SetNext</code> method. The chain of responsibility pattern gives you stronger type safety and is portable to any context -- not just HTTP request processing.</p>
<h3 id="when-should-i-use-the-chain-of-responsibility-pattern-instead-of-a-simple-switch-statement">When should I use the chain of responsibility pattern instead of a simple switch statement?</h3>
<p>Use the chain of responsibility pattern when the processing stages are independently complex, need to be reordered or reconfigured, or when new stages are added frequently. A switch statement works fine when you're dispatching to a fixed, well-known set of handlers that rarely change. Once you find yourself adding new cases every sprint and the method is growing past a screen's length, the chain gives you a much cleaner extension path.</p>
<h3 id="can-handlers-in-the-chain-modify-the-request-before-passing-it-along">Can handlers in the chain modify the request before passing it along?</h3>
<p>Yes. Handlers can enrich or transform the request before delegating to the next handler. For example, an authentication handler could attach a parsed user identity to the request object after validating the token. Use a mutable context object that handlers can write to, but keep modifications documented so future developers know which handler produces which data.</p>
<h3 id="how-do-i-handle-errors-that-occur-deep-in-the-handler-chain">How do I handle errors that occur deep in the handler chain?</h3>
<p>Use the <code>ObservableHandler</code> base class shown in this article, which catches exceptions at each handler level and logs exactly which handler failed. For production systems, combining per-handler exception handling with a global error boundary gives you both specificity and safety.</p>
<h3 id="whats-the-performance-impact-of-a-long-handler-chain">What's the performance impact of a long handler chain?</h3>
<p>Each handler adds one async method call and one null check. This overhead is negligible compared to the actual work each handler performs -- database lookups, HTTP calls, token validation. Profile before optimizing, and focus on the I/O-bound operations inside your handlers rather than the chain structure itself.</p>
<h3 id="should-each-handler-be-a-singleton-or-transient-in-the-di-container">Should each handler be a singleton or transient in the DI container?</h3>
<p>It depends on whether the handler holds state. The <code>RateLimitHandler</code> tracks per-client request counts, so it needs to be a singleton. Stateless handlers like <code>InputValidationHandler</code> can be transient. When in doubt, register as singleton and ensure your handler is thread-safe.</p>
<h3 id="how-do-i-add-a-new-handler-without-modifying-existing-code">How do I add a new handler without modifying existing code?</h3>
<p>Register the new handler in the DI container, then add it to the array in <code>PipelineFactory.CreatePipeline()</code> at the position where you want it to execute. No existing handler code changes. This is the Open/Closed Principle in action -- the chain is open for extension (new handlers) but closed for modification (existing handlers remain untouched).</p>
<h2 id="wrapping-up-this-chain-of-responsibility-pattern-example">Wrapping Up This Chain of Responsibility Pattern Example</h2>
<p>This implementation demonstrates the chain of responsibility pattern handling a real infrastructure concern -- API gateway request processing -- with five handlers that each focus on a single responsibility. We started with a monolithic method that tangled every concern together and ended with independent, testable handlers that snap together in a configurable pipeline.</p>
<p>The pattern scales gracefully. Adding a new concern means writing one handler class and inserting it into the factory's array. Removing one means deleting a single line. Take this implementation, swap the stubs for real JWT validators and authorization services, and you have a production-ready gateway pipeline.</p>
]]></description>
      <content:encoded><![CDATA[<h1 id="chain-of-responsibility-pattern-real-world-example-in-c-complete-implementation">Chain of Responsibility Pattern Real-World Example in C#: Complete Implementation</h1>
<p>Reading about design patterns in the abstract only gets you so far. A <strong>chain of responsibility pattern real world example in C#</strong> with real infrastructure concerns is what cements the concept into something you can actually use at work. This article walks through a complete HTTP request processing pipeline -- the kind you'd build for an API gateway -- where each handler in the chain decides whether a request should proceed or get rejected. By the end, you'll have a fully implemented chain covering rate limiting, authentication, authorization, validation, and routing -- all configurable through <a href="https://www.devleader.ca/2024/02/21/iservicecollection-in-c-simplified-beginners-guide-for-dependency-injection">dependency injection</a>.</p>
<h2 id="the-problem-http-request-processing-pipeline">The Problem: HTTP Request Processing Pipeline</h2>
<p>Imagine you're building an API gateway that sits in front of several backend services. Every incoming request needs to pass through several stages before it reaches its destination: rate limiting checks the caller's quota, authentication verifies the JWT token, and authorization confirms the right permissions.</p>
<p>After those security gates, the request still needs body validation and routing to the correct backend service. Without a structured approach, this logic collapses into a massive nested if/else block:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">public sealed class MonolithicGateway
{
    public async Task&lt;GatewayResponse&gt; ProcessAsync(
        GatewayRequest request)
    {
        if (IsRateLimited(request.ClientId))
            return new GatewayResponse(429, &quot;Rate limit exceeded&quot;);

        if (!IsValidToken(request.AuthToken))
            return new GatewayResponse(401, &quot;Invalid token&quot;);

        if (!HasPermission(request.UserId, request.Resource))
            return new GatewayResponse(403, &quot;Forbidden&quot;);

        if (!IsValidPayload(request.Body))
            return new GatewayResponse(400, &quot;Invalid request body&quot;);

        return await RouteToServiceAsync(request);
    }
}
</code></pre>
</div>
<p>This works for five checks. But real API gateways accumulate dozens of concerns over time -- logging, tracing, CORS, request transformation. Each new concern makes the method longer and harder to test. The chain of responsibility pattern solves this by turning each concern into an independent handler that can be composed, reordered, and tested in isolation.</p>
<h2 id="designing-the-handler-base">Designing the Handler Base</h2>
<p>The foundation of our chain of responsibility pattern real world example in C# is a pair of types: a request/response model and an abstract handler that defines the chaining mechanism. Let's start with the request and response:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">namespace Gateway.Pipeline;

public sealed class GatewayRequest
{
    public required string ClientId { get; init; }

    public required string AuthToken { get; init; }

    public required string UserId { get; init; }

    public required string Resource { get; init; }

    public required string HttpMethod { get; init; }

    public required string Path { get; init; }

    public string? Body { get; init; }
}

public sealed class GatewayResponse
{
    public int StatusCode { get; init; }

    public string Message { get; init; }

    public GatewayResponse(int statusCode, string message)
    {
        StatusCode = statusCode;
        Message = message;
    }
}
</code></pre>
</div>
<p>Now for the abstract handler that all concrete implementations will extend:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">namespace Gateway.Pipeline;

public abstract class RequestHandler
{
    private RequestHandler? _next;

    public RequestHandler SetNext(RequestHandler next)
    {
        _next = next;
        return next;
    }

    public virtual async Task&lt;GatewayResponse&gt; HandleAsync(
        GatewayRequest request)
    {
        if (_next is not null)
        {
            return await _next.HandleAsync(request);
        }

        return new GatewayResponse(
            500,
            &quot;No handler processed the request&quot;);
    }
}
</code></pre>
</div>
<p>The <code>SetNext</code> method wires handlers together and returns the next handler for fluent chaining. The <code>HandleAsync</code> method provides default pass-through behavior -- if a concrete handler doesn't short-circuit, it delegates to the next handler. If you've worked with the <a href="https://www.devleader.ca/2026/03/02/strategy-design-pattern-in-c-complete-guide-with-examples">strategy design pattern</a>, you'll notice a similar emphasis on isolating behavior behind a common contract -- except here, handlers are linked sequentially rather than swapped interchangeably.</p>
<h2 id="implementing-the-handler-chain">Implementing the Handler Chain</h2>
<p>With the base class in place, let's build five concrete handlers. Each one examines the request, either rejects it or passes it along.</p>
<h3 id="ratelimithandler">RateLimitHandler</h3>
<p>The first handler checks whether the client has exceeded their allowed request rate:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">using System.Collections.Concurrent;

namespace Gateway.Pipeline.Handlers;

public sealed class RateLimitHandler : RequestHandler
{
    private readonly ConcurrentDictionary&lt;string, ClientRateInfo&gt;
        _clients = new();
    private readonly int _maxRequests;
    private readonly TimeSpan _window;

    public RateLimitHandler(
        int maxRequests = 100,
        TimeSpan? window = null)
    {
        _maxRequests = maxRequests;
        _window = window ?? TimeSpan.FromMinutes(1);
    }

    public override async Task&lt;GatewayResponse&gt; HandleAsync(
        GatewayRequest request)
    {
        var info = _clients.GetOrAdd(
            request.ClientId,
            _ =&gt; new ClientRateInfo());

        if (info.IsExpired(_window))
        {
            info.Reset();
        }

        info.IncrementCount();

        if (info.Count &gt; _maxRequests)
        {
            return new GatewayResponse(
                429,
                &quot;Rate limit exceeded&quot;);
        }

        return await base.HandleAsync(request);
    }
}

public sealed class ClientRateInfo
{
    public int Count { get; private set; }

    public DateTimeOffset WindowStart { get; private set; }
        = DateTimeOffset.UtcNow;

    public bool IsExpired(TimeSpan window) =&gt;
        DateTimeOffset.UtcNow - WindowStart &gt; window;

    public void Reset()
    {
        Count = 0;
        WindowStart = DateTimeOffset.UtcNow;
    }

    public void IncrementCount() =&gt; Count++;
}
</code></pre>
</div>
<p>When the client is within their limit, the handler calls <code>base.HandleAsync(request)</code> to pass the request down the chain. When the limit is exceeded, it short-circuits with a 429 response.</p>
<h3 id="authenticationhandler">AuthenticationHandler</h3>
<p>The authentication handler validates the JWT token attached to the request:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">namespace Gateway.Pipeline.Handlers;

public sealed class AuthenticationHandler : RequestHandler
{
    private readonly ITokenValidator _tokenValidator;

    public AuthenticationHandler(
        ITokenValidator tokenValidator)
    {
        _tokenValidator = tokenValidator;
    }

    public override async Task&lt;GatewayResponse&gt; HandleAsync(
        GatewayRequest request)
    {
        if (string.IsNullOrEmpty(request.AuthToken))
        {
            return new GatewayResponse(
                401,
                &quot;Missing authentication token&quot;);
        }

        var isValid = await _tokenValidator
            .ValidateAsync(request.AuthToken);

        if (!isValid)
        {
            return new GatewayResponse(
                401,
                &quot;Invalid or expired token&quot;);
        }

        return await base.HandleAsync(request);
    }
}

public interface ITokenValidator
{
    Task&lt;bool&gt; ValidateAsync(string token);
}
</code></pre>
</div>
<p>The <code>ITokenValidator</code> interface keeps the handler testable -- this is the same <a href="https://www.devleader.ca/2024/01/07/what-is-inversion-of-control-a-simplified-beginners-guide">inversion of control</a> principle that makes the whole chain composable.</p>
<h3 id="authorizationhandler">AuthorizationHandler</h3>
<p>The authorization handler checks whether the user has permission to access the requested resource:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">namespace Gateway.Pipeline.Handlers;

public sealed class AuthorizationHandler : RequestHandler
{
    private readonly IAuthorizationService _authService;

    public AuthorizationHandler(
        IAuthorizationService authService)
    {
        _authService = authService;
    }

    public override async Task&lt;GatewayResponse&gt; HandleAsync(
        GatewayRequest request)
    {
        var isAuthorized = await _authService
            .IsAuthorizedAsync(
                request.UserId,
                request.Resource,
                request.HttpMethod);

        if (!isAuthorized)
        {
            return new GatewayResponse(
                403,
                &quot;Insufficient permissions&quot;);
        }

        return await base.HandleAsync(request);
    }
}

public interface IAuthorizationService
{
    Task&lt;bool&gt; IsAuthorizedAsync(
        string userId,
        string resource,
        string httpMethod);
}
</code></pre>
</div>
<p>The pattern is consistent across every handler: check a condition, return an error response if it fails, or delegate to the next handler.</p>
<h3 id="inputvalidationhandler">InputValidationHandler</h3>
<p>The validation handler examines the request body for write operations and passes read operations through:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">namespace Gateway.Pipeline.Handlers;

public sealed class InputValidationHandler : RequestHandler
{
    private readonly IRequestBodyValidator _bodyValidator;

    public InputValidationHandler(
        IRequestBodyValidator bodyValidator)
    {
        _bodyValidator = bodyValidator;
    }

    public override async Task&lt;GatewayResponse&gt; HandleAsync(
        GatewayRequest request)
    {
        var requiresBody = request.HttpMethod is
            &quot;POST&quot; or &quot;PUT&quot; or &quot;PATCH&quot;;

        if (requiresBody &amp;&amp;
            string.IsNullOrWhiteSpace(request.Body))
        {
            return new GatewayResponse(
                400,
                &quot;Request body is required&quot;);
        }

        if (requiresBody)
        {
            var validation = await _bodyValidator
                .ValidateAsync(
                    request.Path,
                    request.Body!);

            if (!validation.IsValid)
            {
                return new GatewayResponse(
                    400,
                    $&quot;Validation failed: &quot; +
                    $&quot;{validation.ErrorMessage}&quot;);
            }
        }

        return await base.HandleAsync(request);
    }
}

public interface IRequestBodyValidator
{
    Task&lt;ValidationResult&gt; ValidateAsync(
        string path,
        string body);
}

public sealed class ValidationResult
{
    public bool IsValid { get; init; }

    public string? ErrorMessage { get; init; }

    public static ValidationResult Success() =&gt;
        new() { IsValid = true };

    public static ValidationResult Failure(string error) =&gt;
        new() { IsValid = false, ErrorMessage = error };
}
</code></pre>
</div>
<p>Not every handler needs to act on every request -- GET and DELETE requests skip body validation entirely.</p>
<h3 id="requestroutinghandler">RequestRoutingHandler</h3>
<p>The final handler routes the validated request to the appropriate backend service:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">namespace Gateway.Pipeline.Handlers;

public sealed class RequestRoutingHandler : RequestHandler
{
    private readonly IServiceRouter _router;

    public RequestRoutingHandler(IServiceRouter router)
    {
        _router = router;
    }

    public override async Task&lt;GatewayResponse&gt; HandleAsync(
        GatewayRequest request)
    {
        var result = await _router.RouteAsync(
            request.Path,
            request.HttpMethod,
            request.Body);

        return new GatewayResponse(
            result.StatusCode,
            result.ResponseBody);
    }
}

public interface IServiceRouter
{
    Task&lt;RouteResult&gt; RouteAsync(
        string path,
        string httpMethod,
        string? body);
}

public sealed class RouteResult
{
    public required int StatusCode { get; init; }

    public required string ResponseBody { get; init; }
}
</code></pre>
</div>
<p>This handler intentionally does <em>not</em> call <code>base.HandleAsync</code> -- it's the end of the chain. If you're familiar with the <a href="https://www.devleader.ca/2026/04/02/composite-design-pattern-in-c-complete-guide-with-examples">composite design pattern</a>, the distinction is worth noting: composite delegates recursively to children, while chain of responsibility delegates along a flat sequence with explicit short-circuiting.</p>
<h2 id="building-and-configuring-the-chain">Building and Configuring the Chain</h2>
<p>Having five handlers is great, but you need a clean way to assemble them. A factory class combined with <a href="https://www.devleader.ca/2024/02/21/iservicecollection-in-c-simplified-beginners-guide-for-dependency-injection">IServiceCollection registration</a> gives you a flexible, configuration-driven pipeline:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">using Microsoft.Extensions.DependencyInjection;

namespace Gateway.Pipeline;

public static class PipelineServiceExtensions
{
    public static IServiceCollection AddGatewayPipeline(
        this IServiceCollection services)
    {
        services.AddSingleton&lt;RateLimitHandler&gt;();
        services.AddSingleton&lt;AuthenticationHandler&gt;();
        services.AddSingleton&lt;AuthorizationHandler&gt;();
        services.AddSingleton&lt;InputValidationHandler&gt;();
        services.AddSingleton&lt;RequestRoutingHandler&gt;();
        services.AddSingleton&lt;IPipelineFactory,
            PipelineFactory&gt;();

        return services;
    }
}

public interface IPipelineFactory
{
    RequestHandler CreatePipeline();
}

public sealed class PipelineFactory : IPipelineFactory
{
    private readonly IServiceProvider _serviceProvider;

    public PipelineFactory(
        IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public RequestHandler CreatePipeline()
    {
        var handlers = new RequestHandler[]
        {
            _serviceProvider
                .GetRequiredService&lt;RateLimitHandler&gt;(),
            _serviceProvider
                .GetRequiredService&lt;AuthenticationHandler&gt;(),
            _serviceProvider
                .GetRequiredService&lt;AuthorizationHandler&gt;(),
            _serviceProvider
                .GetRequiredService&lt;InputValidationHandler&gt;(),
            _serviceProvider
                .GetRequiredService&lt;RequestRoutingHandler&gt;(),
        };

        for (int i = 0; i &lt; handlers.Length - 1; i++)
        {
            handlers[i].SetNext(handlers[i + 1]);
        }

        return handlers[0];
    }
}
</code></pre>
</div>
<p>The factory resolves each handler from the DI container and chains them in a loop. The order in the array is the order of execution -- if you need to insert a new handler, you add one line to the array. This separation between construction and execution is the same principle behind <a href="https://www.devleader.ca/2024/01/07/what-is-inversion-of-control-a-simplified-beginners-guide">inversion of control</a>. Using the factory from application code is straightforward:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">var pipeline = pipelineFactory.CreatePipeline();
var response = await pipeline.HandleAsync(request);
</code></pre>
</div><h2 id="adding-resilience-and-observability">Adding Resilience and Observability</h2>
<p>A chain of responsibility pattern real world example in C# isn't complete without addressing production concerns. Let's enhance the pipeline with structured logging, metrics, and circuit breaker support.</p>
<p>The cleanest approach is an observability-aware base handler that wraps each handler with cross-cutting concerns:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">using System.Diagnostics;

using Microsoft.Extensions.Logging;

namespace Gateway.Pipeline;

public abstract class ObservableHandler : RequestHandler
{
    private readonly ILogger _logger;
    private readonly string _handlerName;

    protected ObservableHandler(ILogger logger)
    {
        _logger = logger;
        _handlerName = GetType().Name;
    }

    public override async Task&lt;GatewayResponse&gt; HandleAsync(
        GatewayRequest request)
    {
        var stopwatch = Stopwatch.StartNew();

        _logger.LogInformation(
            &quot;Handler {HandlerName} processing &quot; +
            &quot;request for {Path}&quot;,
            _handlerName,
            request.Path);

        try
        {
            var response = await ProcessAsync(request);
            stopwatch.Stop();

            _logger.LogInformation(
                &quot;Handler {HandlerName} completed &quot; +
                &quot;in {ElapsedMs}ms with status {StatusCode}&quot;,
                _handlerName,
                stopwatch.ElapsedMilliseconds,
                response.StatusCode);

            return response;
        }
        catch (Exception ex)
        {
            stopwatch.Stop();

            _logger.LogError(
                ex,
                &quot;Handler {HandlerName} failed &quot; +
                &quot;after {ElapsedMs}ms&quot;,
                _handlerName,
                stopwatch.ElapsedMilliseconds);

            return new GatewayResponse(
                500,
                $&quot;Internal error in {_handlerName}&quot;);
        }
    }

    protected abstract Task&lt;GatewayResponse&gt; ProcessAsync(
        GatewayRequest request);
}
</code></pre>
</div>
<p>This base class wraps every handler execution with timing, structured logging, and exception handling. Concrete handlers override <code>ProcessAsync</code> instead of <code>HandleAsync</code> -- the observable base adds instrumentation transparently. If you've worked with the <a href="https://www.devleader.ca/2026/03/26/observer-design-pattern-in-c-complete-guide-with-examples">observer design pattern</a>, the concept is similar: external observers react to handler execution without modifying core logic.</p>
<p>For circuit breaker support, create a state object that any handler calling external services can check:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">namespace Gateway.Pipeline.Handlers;

public sealed class CircuitBreakerState
{
    private int _failureCount;
    private DateTimeOffset _lastFailure;

    public int FailureThreshold { get; init; } = 5;

    public TimeSpan RecoveryWindow { get; init; }
        = TimeSpan.FromSeconds(30);

    public bool IsOpen =&gt;
        _failureCount &gt;= FailureThreshold &amp;&amp;
        DateTimeOffset.UtcNow - _lastFailure
            &lt; RecoveryWindow;

    public void RecordFailure()
    {
        _failureCount++;
        _lastFailure = DateTimeOffset.UtcNow;
    }

    public void Reset() =&gt; _failureCount = 0;
}
</code></pre>
</div>
<p>This state object can be injected into any handler that calls external services. When the circuit is open, the handler returns a 503 immediately instead of forwarding the request to a service that's likely down.</p>
<h2 id="testing-the-complete-pipeline">Testing the Complete Pipeline</h2>
<p>Testing is where the chain of responsibility pattern shines. Because each handler is a separate class with injected dependencies, you can write focused tests for individual handlers and the full chain.</p>
<p>First, test helpers and stub dependencies:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">using Xunit;

namespace Gateway.Pipeline.Tests;

public static class TestRequestFactory
{
    public static GatewayRequest CreateValid() =&gt;
        new()
        {
            ClientId = &quot;client-1&quot;,
            AuthToken = &quot;valid-token&quot;,
            UserId = &quot;user-1&quot;,
            Resource = &quot;/api/orders&quot;,
            HttpMethod = &quot;GET&quot;,
            Path = &quot;/api/orders&quot;,
            Body = null,
        };
}

public sealed class StubTokenValidator : ITokenValidator
{
    private readonly bool _isValid;

    public StubTokenValidator(bool isValid) =&gt;
        _isValid = isValid;

    public Task&lt;bool&gt; ValidateAsync(string token) =&gt;
        Task.FromResult(_isValid);
}

public sealed class StubAuthorizationService
    : IAuthorizationService
{
    private readonly bool _isAuthorized;

    public StubAuthorizationService(bool isAuthorized) =&gt;
        _isAuthorized = isAuthorized;

    public Task&lt;bool&gt; IsAuthorizedAsync(
        string userId,
        string resource,
        string httpMethod) =&gt;
        Task.FromResult(_isAuthorized);
}

public sealed class StubServiceRouter : IServiceRouter
{
    public Task&lt;RouteResult&gt; RouteAsync(
        string path,
        string httpMethod,
        string? body) =&gt;
        Task.FromResult(new RouteResult
        {
            StatusCode = 200,
            ResponseBody = &quot;OK&quot;,
        });
}

public sealed class StubRequestBodyValidator
    : IRequestBodyValidator
{
    private readonly bool _isValid;

    public StubRequestBodyValidator(bool isValid) =&gt;
        _isValid = isValid;

    public Task&lt;ValidationResult&gt; ValidateAsync(
        string path,
        string body) =&gt;
        Task.FromResult(
            _isValid
                ? ValidationResult.Success()
                : ValidationResult.Failure(
                    &quot;Invalid body&quot;));
}
}
</code></pre>
</div>
<p>Now, individual handler tests:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">namespace Gateway.Pipeline.Tests;

public sealed class AuthenticationHandlerTests
{
    [Fact]
    public async Task HandleAsync_MissingToken_Returns401()
    {
        var handler = new AuthenticationHandler(
            new StubTokenValidator(true));

        var request = new GatewayRequest
        {
            ClientId = &quot;c1&quot;,
            AuthToken = string.Empty,
            UserId = &quot;u1&quot;,
            Resource = &quot;/api/orders&quot;,
            HttpMethod = &quot;GET&quot;,
            Path = &quot;/api/orders&quot;,
        };

        var response = await handler.HandleAsync(request);

        Assert.Equal(401, response.StatusCode);
    }

    [Fact]
    public async Task HandleAsync_ValidToken_DelegatesToNext()
    {
        var handler = new AuthenticationHandler(
            new StubTokenValidator(true));

        var routing = new RequestRoutingHandler(
            new StubServiceRouter());

        handler.SetNext(routing);

        var request = TestRequestFactory.CreateValid();

        var response = await handler.HandleAsync(request);

        Assert.Equal(200, response.StatusCode);
    }
}
</code></pre>
</div>
<p>Full chain integration tests verify the handlers work together:</p>
<div class="position-relative"><button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
        type="button"
        aria-label="Copy code"
        onclick="navigator.clipboard.writeText(this.parentElement.querySelector('pre code').textContent)">
        <i class="copy" aria-hidden="true"></i>
</button>
<pre><code class="language-csharp">namespace Gateway.Pipeline.Tests;

public sealed class FullPipelineTests
{
    private RequestHandler BuildChain(
        bool tokenValid = true,
        bool authorized = true)
    {
        var rateLimit = new RateLimitHandler(
            maxRequests: 100);

        var auth = new AuthenticationHandler(
            new StubTokenValidator(tokenValid));

        var authz = new AuthorizationHandler(
            new StubAuthorizationService(authorized));

        var validation = new InputValidationHandler(
            new StubRequestBodyValidator(true));

        var routing = new RequestRoutingHandler(
            new StubServiceRouter());

        rateLimit.SetNext(auth);
        auth.SetNext(authz);
        authz.SetNext(validation);
        validation.SetNext(routing);

        return rateLimit;
    }

    [Fact]
    public async Task
        HandleAsync_ValidRequest_Returns200()
    {
        var pipeline = BuildChain();
        var request = TestRequestFactory.CreateValid();

        var response = await pipeline
            .HandleAsync(request);

        Assert.Equal(200, response.StatusCode);
        Assert.Equal(&quot;OK&quot;, response.Message);
    }

    [Fact]
    public async Task
        HandleAsync_InvalidToken_ShortCircuitsAt401()
    {
        var pipeline = BuildChain(tokenValid: false);
        var request = TestRequestFactory.CreateValid();

        var response = await pipeline
            .HandleAsync(request);

        Assert.Equal(401, response.StatusCode);
    }

    [Fact]
    public async Task
        HandleAsync_Unauthorized_ShortCircuitsAt403()
    {
        var pipeline = BuildChain(authorized: false);
        var request = TestRequestFactory.CreateValid();

        var response = await pipeline
            .HandleAsync(request);

        Assert.Equal(403, response.StatusCode);
    }

    [Fact]
    public async Task
        HandleAsync_ExceedsRateLimit_Returns429()
    {
        var rateLimit = new RateLimitHandler(
            maxRequests: 2);

        var routing = new RequestRoutingHandler(
            new StubServiceRouter());

        rateLimit.SetNext(routing);

        var request = TestRequestFactory.CreateValid();

        await rateLimit.HandleAsync(request);
        await rateLimit.HandleAsync(request);
        var response = await rateLimit
            .HandleAsync(request);

        Assert.Equal(429, response.StatusCode);
    }
}
</code></pre>
</div>
<p>The <code>BuildChain</code> helper accepts parameters that control which stubs succeed or fail, making it trivial to test every short-circuit point without duplicating setup code. Compare this to the monolithic gateway -- you'd need to mock every dependency simultaneously and verify behavior through a single sprawling method.</p>
<p>If you're interested in how the <a href="https://www.devleader.ca/2026/05/13/proxy-design-pattern-in-c-complete-guide-with-examples">proxy design pattern</a> or the <a href="https://www.devleader.ca/2026/03/14/decorator-design-pattern-in-c-complete-guide-with-examples">decorator design pattern</a> handle similar concerns with different structural tradeoffs, those patterns are worth comparing.</p>
<h2 id="frequently-asked-questions">Frequently Asked Questions</h2>
<h3 id="how-is-the-chain-of-responsibility-pattern-different-from-middleware-in-asp.net-core">How is the chain of responsibility pattern different from middleware in ASP.NET Core?</h3>
<p>ASP.NET Core middleware and the chain of responsibility pattern share the same fundamental idea: a request flows through a sequence of processing steps, and any step can short-circuit the pipeline. The key difference is structural. Middleware uses a delegate-based pipeline managed by the framework, while the chain of responsibility pattern uses explicit object references with a <code>SetNext</code> method. The chain of responsibility pattern gives you stronger type safety and is portable to any context -- not just HTTP request processing.</p>
<h3 id="when-should-i-use-the-chain-of-responsibility-pattern-instead-of-a-simple-switch-statement">When should I use the chain of responsibility pattern instead of a simple switch statement?</h3>
<p>Use the chain of responsibility pattern when the processing stages are independently complex, need to be reordered or reconfigured, or when new stages are added frequently. A switch statement works fine when you're dispatching to a fixed, well-known set of handlers that rarely change. Once you find yourself adding new cases every sprint and the method is growing past a screen's length, the chain gives you a much cleaner extension path.</p>
<h3 id="can-handlers-in-the-chain-modify-the-request-before-passing-it-along">Can handlers in the chain modify the request before passing it along?</h3>
<p>Yes. Handlers can enrich or transform the request before delegating to the next handler. For example, an authentication handler could attach a parsed user identity to the request object after validating the token. Use a mutable context object that handlers can write to, but keep modifications documented so future developers know which handler produces which data.</p>
<h3 id="how-do-i-handle-errors-that-occur-deep-in-the-handler-chain">How do I handle errors that occur deep in the handler chain?</h3>
<p>Use the <code>ObservableHandler</code> base class shown in this article, which catches exceptions at each handler level and logs exactly which handler failed. For production systems, combining per-handler exception handling with a global error boundary gives you both specificity and safety.</p>
<h3 id="whats-the-performance-impact-of-a-long-handler-chain">What's the performance impact of a long handler chain?</h3>
<p>Each handler adds one async method call and one null check. This overhead is negligible compared to the actual work each handler performs -- database lookups, HTTP calls, token validation. Profile before optimizing, and focus on the I/O-bound operations inside your handlers rather than the chain structure itself.</p>
<h3 id="should-each-handler-be-a-singleton-or-transient-in-the-di-container">Should each handler be a singleton or transient in the DI container?</h3>
<p>It depends on whether the handler holds state. The <code>RateLimitHandler</code> tracks per-client request counts, so it needs to be a singleton. Stateless handlers like <code>InputValidationHandler</code> can be transient. When in doubt, register as singleton and ensure your handler is thread-safe.</p>
<h3 id="how-do-i-add-a-new-handler-without-modifying-existing-code">How do I add a new handler without modifying existing code?</h3>
<p>Register the new handler in the DI container, then add it to the array in <code>PipelineFactory.CreatePipeline()</code> at the position where you want it to execute. No existing handler code changes. This is the Open/Closed Principle in action -- the chain is open for extension (new handlers) but closed for modification (existing handlers remain untouched).</p>
<h2 id="wrapping-up-this-chain-of-responsibility-pattern-example">Wrapping Up This Chain of Responsibility Pattern Example</h2>
<p>This implementation demonstrates the chain of responsibility pattern handling a real infrastructure concern -- API gateway request processing -- with five handlers that each focus on a single responsibility. We started with a monolithic method that tangled every concern together and ended with independent, testable handlers that snap together in a configurable pipeline.</p>
<p>The pattern scales gracefully. Adding a new concern means writing one handler class and inserting it into the factory's array. Removing one means deleting a single line. Take this implementation, swap the stubs for real JWT validators and authorization services, and you have a production-ready gateway pipeline.</p>
]]></content:encoded>
      <media:content url="https://devleader-d2f9ggbjfpdqcka7.z01.azurefd.net/media/chain-of-responsibility-pattern-real-world-example-csharp.webp" />
    </item>
  </channel>
</rss>