Mid-level 9 min · March 06, 2026

ASP.NET Core 502 — Auth Middleware After Endpoints

A 502 with no logs? Authentication middleware after UseEndpoints() causes NullReferenceException.

N
Naren Founder & Principal Engineer

20+ years shipping production .NET services in enterprise systems. Notes here come from systems that actually shipped.

Follow
Production
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • ASP.NET Core is a cross-platform, modular web framework for .NET
  • The request pipeline is built from middleware components executed in configured order
  • Built-in dependency injection manages service lifetimes (Singleton, Scoped, Transient)
  • Minimal APIs let you write HTTP endpoints without controllers or startup ceremony
  • Performance gains come from trimming unused middleware and using async I/O throughout
✦ Definition~90s read
What is Introduction to ASP.NET Core?

ASP.NET Core is Microsoft's cross-platform, high-performance framework for building modern, cloud-connected web applications. It's a complete rewrite of the classic ASP.NET, designed from the ground up for modularity, testability, and performance — clocking in among the fastest web frameworks in TechEmpower benchmarks.

Imagine you run a busy restaurant.

You use it when you need a production-grade HTTP server stack that runs on Windows, Linux, or macOS, with built-in support for dependency injection, configuration, logging, and a middleware pipeline that gives you fine-grained control over every request. It's the default choice for new .NET web projects, replacing both ASP.NET Framework and the older OWIN/Katana stack.

The framework's architecture centers on three pillars: the middleware pipeline, dependency injection (DI), and the configuration system. The pipeline is a sequential chain of components — each can inspect, modify, or short-circuit the request before passing it to the next.

This is where auth middleware ordering becomes critical: if you place UseAuthentication after UseEndpoints, your controllers or minimal APIs will never see an authenticated user, because the endpoint middleware runs first. DI is built into the host, with three lifetimes (singleton, scoped, transient) that directly affect how services like DbContext or IHttpClientFactory behave under load.

The options pattern (IOptions<T>) lets you bind strongly-typed settings from JSON, environment variables, or other providers, with reload-on-change support.

ASP.NET Core offers two API paradigms: Controllers (the traditional MVC/REST approach with attribute routing, model binding, and filters) and Minimal APIs (introduced in .NET 6 for lightweight, handler-per-endpoint patterns). Controllers are better for large, team-based projects with complex validation, authorization policies, and versioning — think enterprise CRUD apps or RESTful services with dozens of endpoints.

Minimal APIs shine for microservices, health checks, or simple CRUD where you want less ceremony and faster startup. Both share the same underlying middleware, DI, and auth infrastructure, so your choice is about team preference and project complexity, not capability.

Avoid Controllers if you're building a handful of endpoints and value conciseness; avoid Minimal APIs if you need rich conventions, filters, or model binding customization.

Plain-English First

Imagine you run a busy restaurant. Every customer (web request) walks in through the front door, gets greeted by the host, checked for a reservation, seated by a waiter, served food, then shown the exit — in that exact order, every time. ASP.NET Core is the restaurant's operating system: it defines that pipeline of steps, lets you add or remove staff (middleware) at each stage, and makes sure every customer gets consistent, professional service. You design the menu (your app logic); ASP.NET Core handles everything else that makes the restaurant run.

Every time you hit a 'Buy Now' button on Amazon or log into your bank online, a web server somewhere processes that HTTP request in milliseconds. ASP.NET Core is Microsoft's answer to the question: 'How do we build that server-side machinery — fast, cross-platform, and scalable — with C#'? It's not just a framework; it's the backbone of millions of production applications ranging from startup APIs to Fortune 500 enterprise portals. Understanding it isn't optional for a .NET developer — it's the air you breathe.

Before ASP.NET Core, the original ASP.NET was bolted onto Windows and IIS, carrying decades of legacy baggage that made it slow to start, hard to configure, and impossible to run on Linux or Mac. ASP.NET Core was a ground-up rewrite that solved three concrete problems: it made the web pipeline composable (you only pay for what you use), it made dependency injection a first-class citizen instead of a bolt-on afterthought, and it unified MVC, Web API, and Razor Pages under one roof instead of three overlapping frameworks fighting each other.

By the end of this article you'll understand how ASP.NET Core boots up, how the middleware pipeline processes every request, how dependency injection wires your services together, and how to scaffold a real minimal API that you could extend into a production service today. You'll also know the exact mistakes that trip up developers moving from classic ASP.NET — and how to sidestep them.

Why Auth Middleware Order Matters More Than You Think

ASP.NET Core's middleware pipeline is a sequential chain where each component processes an HTTP request in order. The core mechanic is that middleware runs in the order it's registered in Program.cs — and UseAuthentication and UseAuthorization must appear before UseEndpoints or MapControllers. If you place auth middleware after endpoints, the endpoint executes before authentication, bypassing all security checks. This is not a configuration preference; it's a structural requirement enforced by the framework's design.

In practice, the pipeline processes requests like a stack: each middleware can short-circuit or pass to the next. When auth middleware runs after endpoints, the endpoint handler already ran — so HttpContext.User is still null or unauthenticated. The framework does not throw an error; it silently allows the request through. This leads to endpoints that appear to work in development but fail in production when authorization policies actually enforce. The order is: app.UseRouting(), then app.UseAuthentication(), then app.UseAuthorization(), then app.UseEndpoints().

Use this pattern in every ASP.NET Core application that requires authentication or authorization — which is virtually every real-world API. The consequence of misordering is not a compile-time error or a 500; it's a silent security gap where unauthenticated users can access protected resources. Teams often discover this during penetration testing or after a production incident. The rule: auth middleware must always sit between routing and endpoints.

Silent Bypass
Placing auth middleware after endpoints does not throw an error — it silently allows unauthenticated requests through. Only a security audit reveals the gap.
Production Insight
Team deploys a new API with JWT auth; endpoints work in local dev but in production, unauthenticated requests return 200 instead of 401.
Symptom: HttpContext.User.Identity.IsAuthenticated is false inside the endpoint, but no middleware rejects the request.
Rule of thumb: always place UseAuthentication and UseAuthorization immediately after UseRouting and before UseEndpoints.
Key Takeaway
Middleware order is the pipeline's security contract — auth must run before endpoints.
A misordered auth middleware is a silent vulnerability, not a configuration error.
Always verify pipeline order in Program.cs as part of code review for any new endpoint.
ASP.NET Core Middleware Order & Auth Flow THECODEFORGE.IO ASP.NET Core Middleware Order & Auth Flow Why auth middleware must come after endpoints in the pipeline Host Bootstraps Pipeline WebApplication builds middleware from Configure Middleware Order Matters Auth middleware placed before endpoints Auth Middleware Runs Validates tokens, sets user principal Endpoint Middleware Executes matched route handler ⚠ Auth after endpoints = no auth check on routes Always place UseAuthentication before UseEndpoints THECODEFORGE.IO
thecodeforge.io
ASP.NET Core Middleware Order & Auth Flow
Introduction Aspnet Core

The Host: How ASP.NET Core Boots

Every ASP.NET Core application starts with a host. The host is the object that owns the application's resources, lifetime, and services. There are two host types: Generic Host (default for most apps) and Web Host (legacy, still used in some templates). The host is responsible for building the dependency injection container, configuring the middleware pipeline, and running the server (Kestrel or IIS).

When you call CreateBuilder(args).Build().Run(), here's what happens under the hood: 1. The builder registers default services (logging, configuration, environment) into a ServiceCollection. 2. It loads configuration from appsettings.json, environment variables, and command-line arguments. 3. It builds the IHost instance by calling Build(). At this point, the ServiceProvider is created and all singletons are instantiated. 4. Run() starts the server, listens on configured ports, and begins processing requests.

The key thing: everything you add in ConfigureServices runs before the host is built. After Build(), you cannot modify the service collection — it's frozen. That's a common trap for devs trying to add services inside middleware.

Program.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using io.thecodeforge.Hosting;

var builder = WebHost.CreateDefaultBuilder<Startup>(args)
    .UseKestrel(options =>
    {
        options.Listen(System.Net.IPAddress.Loopback, 5000);
    });

var app = builder.Build();
await app.RunAsync();

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
        // Register custom services here
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseRouting();
        app.UseAuthentication();
        app.UseAuthorization();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}
Output
Application starts on http://localhost:5000
Listening for incoming requests...
Mental Model: The Host as a Factory
  • ConfigureServices is the assembly line — you add all components (services) before the machine runs.
  • Build() locks the assembly line and creates the service provider.
  • Run() starts the machine. After that, you can't add or remove services without a restart.
  • If you need dynamic services, use a factory pattern or a hosted service that manages its own container.
Production Insight
A common production issue: a service registered as Singleton accidentally captures a Scoped dependency. The fix is to use IServiceScopeFactory or register the service as Scoped and either scope it manually or accept the shorter lifetime.
The host is also the place to configure server timeouts and limits. Forgetting to increase the request body size limit for file uploads is a frequent 413 error source.
Key Takeaway
The host is the single entry point that builds the service container and starts the server.
After Build(), the service collection is frozen.
Register everything in ConfigureServices — nothing after.
Choosing a Host Type
IfBuilding a web app (MVC, API, Razor Pages)
UseUse WebApplication.CreateBuilder(args) — the new minimal API approach. It simplifies startup.
IfBuilding a background service or worker
UseUse Host.CreateDefaultBuilder(args) with AddHostedService<T>.
IfNeed to support IIS in-process hosting
UseUse WebHost.CreateDefaultBuilder<Startup>(args) with UseIISIntegration().

The Middleware Pipeline: Order Matters

Middleware is the heart of ASP.NET Core request processing. Each piece of middleware decides whether to pass the request to the next piece or to short-circuit and return a response immediately. The order in which you register middleware determines the pipeline's behaviour.

The classic pattern is: logging → static files → authentication → routing → authorization → endpoints. But it's not just about ordering — each middleware can modify the request and response. A custom middleware can read the request body, add headers, or stop the pipeline entirely (like app.UseExceptionHandler).

Don't confuse middleware with services. Middleware runs per request; services are injected into middleware's constructor at app startup. If a middleware needs a scoped service, it must inject it via Invoke(HttpContext, IServiceScopedService) — not through the constructor.

io/thecodeforge/Middleware/RequestLoggingMiddleware.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

namespace io.thecodeforge.Middleware
{
    public class RequestLoggingMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly ILogger _logger;

        public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
        {
            _next = next;
            _logger = logger;
        }

        public async Task InvokeAsync(HttpContext context)
        {
            _logger.LogInformation($"Incoming request: {context.Request.Method} {context.Request.Path}");

            // Call the next middleware in the pipeline
            await _next(context);

            _logger.LogInformation($"Response: {context.Response.StatusCode}");
        }
    }

    // Extension method to register middleware
    public static class RequestLoggingMiddlewareExtensions
    {
        public static IApplicationBuilder UseRequestLogging(this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<RequestLoggingMiddleware>();
        }
    }
}
Output
Incoming request: GET /api/products
Response: 200
Pipeline Order Is Irreversible
Once the host is built, middleware order is fixed. To change it, you must restart the application. There is no runtime dynamic ordering. Plan your middleware order at design time.
Production Insight
A real incident: the team placed app.UseCors() after app.UseAuthentication(). The CORS preflight (OPTIONS request) was rejected with 401 because the authentication middleware required Authorization header. The fix was simple: move CORS before authentication.
Another issue: a custom middleware that reads the request body twice. For example, if you want to log the request body and also pass it to the next middleware, you must enable buffering and reset the stream position. Forgetting this causes 'Cannot read after stream is closed' errors.
Key Takeaway
Middleware order is your application's execution contract.
Register exception handling first, then security, then endpoints.
When debugging pipeline issues, start by printing the middleware order or using the DeveloperExceptionPage.
Middleware Order Decision Guide
IfNeed to handle errors universally
UsePlace exception handling middleware (UseExceptionHandler) at the top of the pipeline.
IfServe static files
UsePlace UseStaticFiles early, before authentication, so that static files don't require auth.
IfNeed to add CORS
UsePlace UseCors before UseAuthentication and UseAuthorization to handle preflight without auth.
IfNeed to read request body in middleware
UseEnable request buffering (services.AddHttpContextAccessor) and reset stream position after reading.

Dependency Injection: Service Lifetimes and Scope

ASP.NET Core has a built-in dependency injection container that manages service lifetimes. There are three lifetimes: - Transient: a new instance every time it's requested. Use for lightweight, stateless services. - Scoped: one instance per HTTP request (scope). Use for services that need to share state within a single request, like DbContext. - Singleton: one instance for the entire application lifetime. Use for services that are expensive to create and hold no per-request state.

The most common mistake we see in production: a Singleton service captures a Scoped dependency. For example, a Singleton logger that needs a Scoped database context. The Singleton holds a reference to the DbContext for the first request, then reuses it for all subsequent requests — causing data corruption or stale reads. The fix: inject IServiceScopeFactory and create a new scope per operation.

Another subtle issue: multiple constructors. The container uses the constructor with the most parameters. If you have multiple constructors that are both resolvable, the container picks the greediest one. That leads to weird bugs when you add a new constructor for testing and forget to remove the old one.

io/thecodeforge/Services/OrderService.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Threading.Tasks;

namespace io.thecodeforge.Services
{
    public interface IOrderService
    {
        Task ProcessAsync(int orderId);
    }

    public class OrderService : IOrderService
    {
        private readonly IOrderRepository _repository;
        private readonly ILogger _logger;

        // Correct: both dependencies are injected
        public OrderService(IOrderRepository repository, ILogger<OrderService> logger)
        {
            _repository = repository;
            _logger = logger;
        }

        public async Task ProcessAsync(int orderId)
        {
            _logger.LogInformation("Processing order {OrderId}", orderId);
            await _repository.UpdateStatusAsync(orderId, "Processed");
        }
    }

    // Registration in IServiceCollection
    public static class ServiceCollectionExtensions
    {
        public static IServiceCollection AddIoTheCodeForge(this IServiceCollection services)
        {
            services.AddScoped<IOrderService, OrderService>();
            services.AddScoped<IOrderRepository, OrderRepository>();
            return services;
        }
    }
}
Output
No direct output; services are registered and injected at runtime.
Mental Model: Service Container as a Smart Factory with Time
  • Singleton = one machine in the factory that runs forever.
  • Scoped = a new machine for each shift (HTTP request).
  • Transient = a new machine for every single product.
  • Never let a longer-living service hold a direct reference to a shorter-living service.
Production Insight
A production issue we encountered: a background service (IHostedService) used a Scoped DbContext directly injected into its constructor. The background service ran once and then the DbContext was disposed, causing all subsequent background loops to fail with 'Cannot access a disposed object'. The fix: inject IServiceScopeFactory and create a new scope for each iteration of the background loop.
Key Takeaway
Lifetime matters — Singleton captures Scoped = bugs.
Register your services explicitly. Let the container decide.
Use IServiceScopeFactory for background tasks.
Choosing a Service Lifetime
IfService is stateless and lightweight (e.g., utility functions)
UseTransient
IfService needs to maintain state per HTTP request (e.g., DbContext, unit of work)
UseScoped
IfService is expensive to create, thread-safe, and holds no per-request state (e.g., cache, configuration provider)
UseSingleton
IfService must be shared across multiple requests but is not thread-safe
UseDo not use Singleton. Use Scoped with a lock or consider a different architecture.

Configuration and Options Pattern

ASP.NET Core has a powerful configuration system that aggregates settings from multiple sources: appsettings.json, environment variables, Azure App Configuration, user secrets (development), and command-line arguments. The last source wins.

The Options pattern (IOptions&lt;T&gt;, IOptionsSnapshot&lt;T&gt;, IOptionsMonitor&lt;T&gt;) allows you to bind configuration sections to strongly typed POCOs. IOptions&lt;T&gt; is singleton and reads values at app startup. IOptionsSnapshot&lt;T&gt; is scoped and re-reads per request. IOptionsMonitor&lt;T&gt; is singleton but reacts to configuration changes at runtime (e.g., when using Azure App Configuration).

Common mistake: injecting IOptions&lt;T&gt; into a Scoped service expecting the configuration to change between requests. It won't — you need IOptionsSnapshot for per-request reload. Also, forgetting to call services.Configure&lt;TOptions&gt;(configuration.GetSection("SectionName")) leads to a runtime error when the Options class is resolved.

appsettings.jsonCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
  "IoTheCodeForge": {
    "Database": {
      "ConnectionString": "Server=localhost;Database=forge;User Id=sa;Password=Your_password123;",
      "TimeoutSeconds": 30
    },
    "FeatureFlags": {
      "EnableNewOrderFlow": false
    }
  }
}

// Program.cs
using io.thecodeforge.Configuration;

var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<ForgeDatabaseOptions>(
    builder.Configuration.GetSection("IoTheCodeForge:Database"));

// ForgeDatabaseOptions.cs
namespace io.thecodeforge.Configuration
{
    public class ForgeDatabaseOptions
    {
        public string ConnectionString { get; set; } = string.Empty;
        public int TimeoutSeconds { get; set; } = 30;
    }
}
Output
At runtime, ForgeDatabaseOptions is populated with values from appsettings.json.
Configuration Binding Insight
The Options pattern uses recursive binding. Nested sections map to nested POCO classes. Arrays and lists work too: use '0:Key' notation in JSON or name the section with array index in environment variables (e.g., ForgeOptions:0:Name).
Production Insight
A team in production used IOptions<ConnectionStringOptions> in a Scoped DbContext factory. The connection string was read at app startup only, not per request. When they rotated the database password via a vault secret, the application continued using the old password until restarted. They replaced IOptions with IOptionsSnapshot to pick up the new password on each request (since the vault updated the config source).
Key Takeaway
Configuration is aggregated, last source wins.
Bind strongly typed options to avoid magic strings.
Use IOptionsSnapshot when you need per-request refresh.
Don't assume configuration is static in production.
Which Options Interface to Use
IfConfiguration is static and never changes
UseUse IOptions<T> (singleton).
IfConfiguration can change between requests (e.g., feature flags from external source)
UseUse IOptionsSnapshot<T> (scoped) or IOptionsMonitor<T> (singleton with changes).
IfNeed to react to live configuration changes (e.g., Azure App Configuration)
UseUse IOptionsMonitor<T> with OnChange callback.

Minimal APIs vs Controllers: When to Use Each

In .NET 6, Microsoft introduced Minimal APIs: a way to define HTTP endpoints with a simple lambda instead of creating a full controller class. Minimal APIs are perfect for small services, health checks, and prototypes. They reduce boilerplate and make the code easier to read for lightweight operations.

Controllers are still the right choice for large projects that benefit from model binding, validation attributes, filters, and Swagger generation out of the box. Controllers also support dependency injection via constructor injection, which is more familiar to most .NET devs.

The important thing is to not overuse Minimal APIs. If an endpoint needs more than a couple of dependencies, filters, or complex routing, switch to a controller. The line is blurry, but a good rule: if your endpoint lambda is over 20 lines or uses more than 3 injected services, it's time to create a controller.

Program.cs (Minimal API style)CSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using io.thecodeforge.Data;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IProductRepository, InMemoryProductRepository>();
var app = builder.Build();

app.MapGet("/api/products", async (IProductRepository repo) =>
{
    var products = await repo.GetAllAsync();
    return Results.Ok(products);
});

app.MapGet("/api/products/{id:int}", async (int id, IProductRepository repo) =>
{
    var product = await repo.GetByIdAsync(id);
    return product is null ? Results.NotFound() : Results.Ok(product);
});

app.Run();
Output
GET /api/products returns JSON array
GET /api/products/1 returns single product or 404
When to Choose Controllers Over Minimal APIs
If you need: - Multiple filters (e.g., [Authorize] + [ValidateAntiForgeryToken]) - Shared model binding via [FromForm], [FromRoute] - Complex validation with FluentValidation - Swagger/OpenAPI documentation via [ApiController] Then use a controller. Otherwise Minimal APIs are fine.
Production Insight
In a real project, the team built a large microservice entirely with Minimal APIs. After six months, they had 30 endpoints each with 40-line lambdas, duplicate validation logic, and no easy way to add a global exception filter. They refactored to controllers in two sprints and reduced code duplication by 40%. The lesson: Minimal APIs scale down, not up.
Key Takeaway
Minimal APIs are for small, focused endpoints.
Controllers are for complex, structured APIs.
Do not let a single endpoint exceed 20 lines of lambda code.
Choosing Between Minimal API and Controller
IfEndpoint is simple, few lines, one or two dependencies
UseMinimal API — keep it inline.
IfEndpoint needs filters, model binding, or complex dependency injection
UseController — use [ApiController] and business logic in separate service.
IfBuilding a new microservice or prototype
UseStart with Minimal APIs and refactor to controllers when complexity grows.
IfExisting codebase is controller-based
UseStay consistent — do not mix unless there's a compelling reason (e.g., health check endpoints).

Your Project Isn't Yours Until You Kill the Template Boilerplate

Scaffolding tools spit out a working app. That does not mean it's production-ready. The default 'Create a project' wizard gives you a kitchen sink: sample pages, development certificates you don't control, and a launch profile that assumes localhost:5000 with zero authentication. In a real deployment, that's a liability.

You own the code the moment you delete everything you didn't write. Strip the sample weather endpoints. Remove the default privacy policy page if your legal team supplies their own. Wipe the development certificate from source control — it's a public key that ships with every .NET SDK install. Attackers scan for it.

The 'solution' Visual Studio creates is a starting line, not a finish line. Your first commit should be a surgical removal of everything that isn't yours. The second commit locks down the launchSettings.json to a port your ops team controls. Three years from now, when a junior asks why the app only listens on a Unix socket, you'll point at that commit message.

CleanStartup.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// io.thecodeforge — csharp tutorial

// What the template gives you:
var builder = WebApplication.CreateBuilder(args);
// ... 30 lines of sample junk you never asked for

// What a senior ships:
var builder = WebApplication.CreateSlimBuilder();
builder.WebHost.UseUrls("http://localhost:5800"); // ops-defined port
var app = builder.Build();

app.MapGet("/health", () => Results.Ok(new { status = "alive" }));
// No sample pages. No unused middleware. No development cert.

app.Run();
// Output: Listening on http://localhost:5800
Output
Listening on http://localhost:5800
Production Trap:
Your default launchSettings.json likely includes "applicationUrl": "https://localhost:5001;http://localhost:5000". That HTTPS URL uses the ASP.NET Core development certificate, which is the same .pfx file on every developer's machine. Do not deploy. Replace it with a real cert or terminate TLS at your reverse proxy.
Key Takeaway
The first file you delete in a new project is more important than the first file you write.

Prerequisites: What They Don't Tell You in the Beginner Docs

The official prerequisites list reads like a shopping list: Visual Studio, the ASP.NET workload, .NET SDK 8.0 or later. Fine for a tutorial. Laughable for production.

Here's what you actually need before you write a single line of ASP.NET Core code. First, a containerized build environment. If you're coding on your laptop with the SDK installed globally, you will ship a bug due to a version mismatch between developer workstations and your CI pipeline. Install the .NET SDK inside a Docker image that matches your production runtime. Pin the SDK version. Do not trust 'latest'.

Second, a structured secrets vault. The built-in Secrets Manager ('dotnet user-secrets') is for local dev only. For real environments, you inject connection strings and API keys via environment variables, Azure Key Vault, or HashiCorp Vault. Never, ever commit appsettings.Development.json with real credentials. I have seen production databases exposed because someone forgot a .gitignore entry.

Third, a basic understanding of HTTP. Know what status codes mean. Know the difference between a redirect and a forward. If you can't explain why a 302 causes a GET after a POST, go read the HTTP spec before you touch middleware. Debugging auth flows without this foundation will make you cry.

DockerPrereqs.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// io.thecodeforge — csharp tutorial

// Dockerfile snippet for reproducible builds:
FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build
WORKDIR /src
COPY ["MyApi.csproj", "."]
RUN dotnet restore "./MyApi.csproj" --runtime linux-musl-x64

COPY . .
RUN dotnet publish "./MyApi.csproj" \
    -c Release \
    -r linux-musl-x64 \
    --self-contained false \
    -o /app

// Output: Restored packages from nuget.org, published to /app
Output
Restored packages from nuget.org, published to /app
Senior Shortcut:
Pin your .NET SDK version in a global.json file at the repo root. This prevents 'works on my machine' bugs by forcing every environment — local, CI, container build — to use the same SDK. Run 'dotnet new globaljson --sdk-version 8.0.404'.
Key Takeaway
Your real prerequisites are a locked SDK version, a container build, and a secrets strategy. Skip any of those and you're debugging infrastructure, not code.

Architecture: The Request-Response Workflow

Most tutorials jump straight to controllers and middleware without explaining the fundamental loop that processes every HTTP request. ASP.NET Core follows a clear pipeline: the Kestrel server receives the raw HTTP request, passes it to the host, which creates an HttpContext object. Middleware components execute in configured order—authentication, routing, authorization, then your endpoint. Each middleware can short-circuit the pipeline by writing a response directly, skipping downstream handlers. The key insight: your application is not just code—it's a series of gates that transform a request into a response. Understanding this workflow helps you debug why middleware order matters. For example, if exception-handling middleware comes after endpoint execution, uncaught errors will produce a raw 500 response instead of a formatted error page. Always map the pipeline visually: request enters top, flows through middleware layers, hits your logic, then the response flows back out through the same layers in reverse. This stack-like behavior controls compression, caching, and logging.

RequestResponseFlow.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// io.thecodeforge — csharp tutorial
var app = WebApplication.Create(args);

app.Use(async (context, next) =>
{
    // Request path: runs before endpoint
    Console.WriteLine($"Incoming: {context.Request.Path}");
    await next(); // Call next middleware
    // Response path: runs after endpoint
    Console.WriteLine($"Outgoing: {context.Response.StatusCode}");
});

app.MapGet("/", () => "Hello");

app.Run();
Output
Incoming: /
Outgoing: 200
Production Trap:
Placing app.UseExceptionHandler() after your endpoints will swallow exceptions silently. Always register error handling as the first middleware so it wraps all downstream code.
Key Takeaway
Every request is a stack: middleware runs top-to-bottom on the way in, bottom-to-top on the way out.

Characteristics: How ASP.NET Core Differs from Traditional .NET

ASP.NET Core is not just an update; it's a ground-up rewrite designed to be cross-platform, lightweight, and modular. Key characteristics: it runs on Linux, macOS, and Windows without modification. The runtime is self-contained by default—you can publish a single executable with the runtime bundled, eliminating server dependencies. It uses a unified programming model: MVC, Razor Pages, and Minimal APIs all share the same middleware pipeline, DI container, and configuration system. Unlike classic ASP.NET, there is no System.Web.dll—everything is built on top of Microsoft.AspNetCore.* NuGet packages. This means you only pay for what you use, resulting in smaller deployments and faster startup times. Also, Kestrel is the default web server, a cross-platform HTTP server that doesn't require IIS or Nginx to run. Finally, ASP.NET Core is open source (MIT license) with active community contributions. These characteristics force a shift in mindset: you are no longer tied to Windows and IIS, so deployment decisions like containerization become natural.

Characteristics.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
// io.thecodeforge — csharp tutorial
// Self-contained deployment eliminates server issues
var builder = WebApplication.CreateBuilder(args);
// Cross-platform: same code runs on Linux, Mac, Windows
builder.WebHost.UseKestrel();
// Minimal dependencies: no System.Web
var app = builder.Build();
app.MapGet("/", () => "Runs anywhere");
app.Run();

// Publish: dotnet publish -c Release -r linux-x64 --self-contained true
Output
Runs anywhere
Why It Matters:
Switching to Linux servers can cut hosting costs by 50-70%. ASP.NET Core's cross-platform nature makes this trivial.
Key Takeaway
ASP.NET Core is modular, cross-platform, and self-contained—treat it as a fresh stack, not an upgrade.
● Production incidentPOST-MORTEMseverity: high

The Silent 502: Authentication Middleware Placed After Endpoint Routing

Symptom
Every request to a protected endpoint returns HTTP 502. No error logs appear except a generic 'An unhandled exception occurred'. Local development works fine with debugger attached.
Assumption
The team assumed that adding the authentication middleware anywhere in Configure() would work because 'ASP.NET Core is smart enough to figure it out'.
Root cause
The authentication middleware was registered after app.UseEndpoints(). When a request hits the endpoint middleware, it short-circuits the pipeline if the endpoint is matched. The authentication middleware never ran, but the endpoint handler tried to access the User object that was never populated. This threw a NullReferenceException that bubbled up as a 502.
Fix
Move app.UseAuthentication() and app.UseAuthorization() before app.UseEndpoints(). The correct order is: routing → authentication → authorization → endpoints. Also add a global exception handler middleware to catch unhandled exceptions and return a proper 500 with diagnostic details.
Key lesson
  • Middleware order in ASP.NET Core is deterministic and critical — it's not 'smart'.
  • Always place authentication and authorization middleware after UseRouting() and before UseEndpoints().
  • When you see a 502 with no clear logs, suspect an unhandled exception in the pipeline that kills the response before logging.
Production debug guideSymptom → Action pairs for common middleware misconfiguration4 entries
Symptom · 01
Endpoint returns 404 even though route is defined
Fix
Check that app.UseRouting() is called before app.UseEndpoints(). Also verify that UseEndpoints maps the correct route pattern.
Symptom · 02
Static files return 404 in production but work locally
Fix
Confirm that app.UseStaticFiles() is placed before app.UseRouting(). Static files middleware short-circuits if it finds a match, so it must come early.
Symptom · 03
CORS errors in browser, preflight failing
Fix
Ensure app.UseCors() is called before app.UseRouting(). If you have authentication middleware, CORS must run before it to handle OPTIONS requests without authentication.
Symptom · 04
Exception details shown in browser (HTTP 500 with stack trace)
Fix
Wrap the entire pipeline in a try-catch or use app.UseExceptionHandler() at the top of Configure(). Never expose exception details in production — use ILogger and return a generic error response.
★ Quick Debug Cheat Sheet for ASP.NET Core PipelineThree commands and one fix for the most common pipeline failures in production.
Middleware not executing
Immediate action
Check the order in Program.cs/Startup.Configure(). Middleware runs in registration order.
Commands
dotnet run --environment Development
dotnet add package Microsoft.AspNetCore.Diagnostics --version 6.0.0
Fix now
Add app.UseDeveloperExceptionPage() inside if (env.IsDevelopment()) to see the full pipeline execution.
Dependency injection throws InvalidOperationException+
Immediate action
Check that the service is registered in ConfigureServices/AddServices.
Commands
dotnet build
IServiceCollection extension method: services.AddTransient<IMyService, MyService>()
Fix now
Add the registration and check for circular dependencies by reviewing constructor parameters.
Minimal API endpoint returns 400 Bad Request+
Immediate action
Inspect the request body against the expected model.
Commands
curl -v -X POST https://localhost:5001/api/endpoint -H 'Content-Type: application/json' -d '{}'
Check validation attributes on the parameter model.
Fix now
Add fluent validation or manual model state checks inside the endpoint handler.
ASP.NET Core vs Classic ASP.NET
FeatureASP.NET CoreClassic ASP.NET
PlatformCross-platform (Windows, Linux, macOS)Windows only (IIS)
Dependency InjectionBuilt-in container, first-class citizenNo built-in; required third-party (Unity, Autofac)
PipelineModular middleware; order controlled by developerFixed HTTP module pipeline (global.asax)
Performance~10x faster requests per second than classicSlower due to System.Web dependency
ConfigurationJSON, environment variables, command lineWeb.config XML, appSettings only
HostingKestrel (self-host), IIS, HTTP.sysIIS only

Key takeaways

1
ASP.NET Core is a cross-platform, modular web framework that gives you full control over the request pipeline.
2
Middleware order is critical
error handling first, routing after static files, authentication before endpoints.
3
Dependency injection is built-in and governs service lifetimes. Captive dependencies cause subtle production bugs.
4
Configuration is aggregated and supports runtime reload. Use the correct Options interface for your needs.
5
Minimal APIs are for simple endpoints; controllers are for complex structured APIs. Choose based on code clarity and maintainability.

Common mistakes to avoid

5 patterns
×

Using depends_on without a healthcheck in Docker Compose

Symptom
API crashes on startup with ECONNREFUSED because the database container started but is not yet ready to accept connections.
Fix
Add a healthcheck block to the database service using pg_isready, then use condition: service_healthy in the API depends_on block.
×

Registering services after Build()

Symptom
InvalidOperationException: 'Cannot resolve scoped service from root provider' or 'Collection was modified' during first request.
Fix
Move all service registrations into ConfigureServices (or builder.Services). Never manipulate IServiceCollection after Build() is called.
×

Using IOptions in a Singleton service expecting runtime changes

Symptom
Configuration changes are not picked up even after external updates; service continues using stale values.
Fix
Use IOptionsSnapshot (scoped) or IOptionsMonitor (singleton reactive). For background tasks, inject IServiceScopeFactory and resolve IOptionsSnapshot inside each scope.
×

Middleware ordering: exception handling placed at the bottom of pipeline

Symptom
Exceptions thrown in middleware that run after the exception handler are not caught; user receives blank 500 or full stack trace in production.
Fix
Place app.UseExceptionHandler (or UseDeveloperExceptionPage in dev) as the very first middleware in the pipeline.
×

Not disposing of scoped services in background tasks

Symptom
Memory leak or 'Cannot access a disposed object' exception when background service runs multiple iterations.
Fix
Create and dispose a new scope within each iteration using IServiceScopeFactory. Do not inject scoped services directly into the IHostedService constructor.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain the middleware pipeline in ASP.NET Core. How does the order of m...
Q02JUNIOR
What are the three service lifetimes in ASP.NET Core DI? Give a real pro...
Q03SENIOR
How would you handle configuration changes at runtime without restarting...
Q04SENIOR
Compare Minimal APIs with Controller-based APIs. When would you choose o...
Q01 of 04SENIOR

Explain the middleware pipeline in ASP.NET Core. How does the order of middleware affect the response?

ANSWER
The middleware pipeline is a series of delegates that handle HTTP requests and responses. Each middleware can inspect, modify, or short-circuit the request/response by either calling the next delegate or not. The order is determined by the order you add them in Configure() (or program.cs). If you place authentication after endpoints, authentication never runs. The typical order: Exception → Static Files → Routing → Authentication → Authorization → Endpoints. Order is fixed after app start.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is ASP.NET Core and how is it different from classic ASP.NET?
02
Why does my middleware not execute?
03
How do I read the request body in middleware without breaking downstream middleware?
04
Can I change the middleware order at runtime?
05
What is the correct way to use IOptionsSnapshot in a background service?
N
Naren Founder & Principal Engineer

20+ years shipping production .NET services in enterprise systems. Notes here come from systems that actually shipped.

Follow
Verified
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
🔥

That's ASP.NET. Mark it forged?

9 min read · try the examples if you haven't

Previous
ValueTask in C#
1 / 14 · ASP.NET
Next
REST API with ASP.NET Core