ASP.NET Core 502 — Auth Middleware After Endpoints
A 502 with no logs? Authentication middleware after UseEndpoints() causes NullReferenceException.
- 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.
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.
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.UseDeveloperExceptionPage() 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
That's ASP.NET. Mark it forged?
4 min read · try the examples if you haven't