ASP.NET Core 502 — Auth Middleware After Endpoints
A 502 with no logs? Authentication middleware after UseEndpoints() causes NullReferenceException.
20+ years shipping production .NET services in enterprise systems. Notes here come from systems that actually shipped.
- 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
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., then UseRouting()app., then UseAuthentication()app., then UseAuthorization()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.
HttpContext.User.Identity.IsAuthenticated is false inside the endpoint, but no middleware rejects the request.UseAuthentication and UseAuthorization immediately after UseRouting and before UseEndpoints.Program.cs as part of code review for any new endpoint.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.
- 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.
Build(), the service collection is frozen.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.
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.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.
- 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.
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<T>, IOptionsSnapshot<T>, IOptionsMonitor<T>) allows you to bind configuration sections to strongly typed POCOs. IOptions<T> is singleton and reads values at app startup. IOptionsSnapshot<T> is scoped and re-reads per request. IOptionsMonitor<T> is singleton but reacts to configuration changes at runtime (e.g., when using Azure App Configuration).
Common mistake: injecting IOptions<T> 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<TOptions>(configuration.GetSection("SectionName")) leads to a runtime error when the Options class is resolved.
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.
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.
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.
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.
app.UseExceptionHandler() after your endpoints will swallow exceptions silently. Always register error handling as the first middleware so it wraps all downstream code.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.
The Silent 502: Authentication Middleware Placed After Endpoint Routing
Configure() would work because 'ASP.NET Core is smart enough to figure it out'.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.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.- Middleware order in ASP.NET Core is deterministic and critical — it's not 'smart'.
- Always place authentication and authorization middleware after
UseRouting()and beforeUseEndpoints(). - When you see a 502 with no clear logs, suspect an unhandled exception in the pipeline that kills the response before logging.
UseRouting() is called before app.UseEndpoints(). Also verify that UseEndpoints maps the correct route pattern.UseStaticFiles() is placed before app.UseRouting(). Static files middleware short-circuits if it finds a match, so it must come early.UseCors() is called before app.UseRouting(). If you have authentication middleware, CORS must run before it to handle OPTIONS requests without authentication.UseExceptionHandler() at the top of Configure(). Never expose exception details in production — use ILogger and return a generic error response.dotnet run --environment Developmentdotnet add package Microsoft.AspNetCore.Diagnostics --version 6.0.0UseDeveloperExceptionPage() inside if (env.IsDevelopment()) to see the full pipeline execution.Key takeaways
Common mistakes to avoid
5 patternsUsing depends_on without a healthcheck in Docker Compose
Registering services after Build()
Build() is called.Using IOptions in a Singleton service expecting runtime changes
Middleware ordering: exception handling placed at the bottom of pipeline
Not disposing of scoped services in background tasks
Interview Questions on This Topic
Explain the middleware pipeline in ASP.NET Core. How does the order of middleware affect the response?
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.Frequently Asked Questions
20+ years shipping production .NET services in enterprise systems. Notes here come from systems that actually shipped.
That's ASP.NET. Mark it forged?
9 min read · try the examples if you haven't