Junior ASP.NET Core interviews ask "what is middleware." Senior interviews ask why your UseAuthorization ran before routing and silently allowed an anonymous request through, or why a singleton that took a DbContext corrupted data across tenants. The framework is easy to use and easy to misuse in ways that only show up in production — under load, across tenants, at 3am.
This is 30 questions across the four parts that actually separate seniors: the request pipeline, DI & lifetimes, hosting & performance, and auth & APIs. Every answer is a short answer you could say in one breath, a deep dive into the mechanism with code, and the senior signal the interviewer is listening for. Every sample is from Mattrx — a multi-tenant marketing-analytics SaaS on .NET 9 / ASP.NET Core, ~3,200 req/sec peak, Azure SQL + Redis, OAuth via Entra ID.
The lens for all 30
A junior describes a feature; a senior explains the ordering, lifetime, and failure mode. "What is middleware?" gets "a request delegate chain" from a junior and "an ordered, unwinding pipeline where auth must sit between routing and endpoints, and a mis-order silently lets requests through" from a senior.
Block A highlight — the pipeline, and the #1 ordering bug
Middleware is a chain of components, each of which can act on the request, call next(), and act on the response as the chain unwinds. Order is everything: a component only sees what the components before it let through.
Request
→ [ ExceptionHandler ] → [ HTTPS ] → [ Routing ] → [ AuthN ] → [ AuthZ ] → [ Endpoint ]
← ───────────────────── response unwinds back up ───────────────────────── ┘
The single most common pipeline bug: UseAuthentication/UseAuthorization must sit between UseRouting and the endpoints. Authorization needs to know which endpoint matched to read its [Authorize] metadata — which only exists after routing — and it must run before the endpoint executes.
app.UseRouting(); // endpoint selected + its [Authorize] metadata attached
app.UseAuthentication(); // populate ctx.User
app.UseAuthorization(); // enforce the selected endpoint's policy
app.MapControllers(); // BUG if this ran before UseAuthorization
Block B highlight — the captive dependency bug
A longer-lived service capturing a shorter-lived one — classically a singleton holding a scoped service. The scoped service now lives forever, leaking state across requests/tenants and breaking thread-safety.
// BUG: singleton CacheWarmer captures a scoped DbContext -> lives for the whole app.
builder.Services.AddSingleton<CacheWarmer>(); // ctor takes MattrxDbContext (scoped)
// FIX: create a scope per unit of work.
public sealed class CacheWarmer(IServiceScopeFactory scopes)
{
public async Task WarmAsync(CancellationToken ct)
{
using var scope = scopes.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MattrxDbContext>();
// ...use db within this scope...
}
}
And why DbContext is scoped: it's not thread-safe and tracks entity state per unit of work. A singleton DbContext across concurrent requests corrupts its change tracker ("A second operation was started on this context") and leaks data across tenants. The same scope-per-unit-of-work rule is how you use a DbContext inside a BackgroundService (which is itself a singleton).
Block C highlight — sync-over-async and thread-pool starvation
Blocking a thread-pool thread (.Result/.Wait()) on async I/O ties up threads that can't serve other requests; under load the pool can't grow fast enough and latency explodes.
// STARVATION: blocks a pool thread on I/O.
var kpis = _service.GetKpisAsync(t, c, r, ct).Result;
// CORRECT: keep the thread free.
var kpis = await _service.GetKpisAsync(t, c, r, ct);
Plus: Kestrel behind a reverse proxy + UseForwardedHeaders, config precedence with secrets in Key Vault, graceful BackgroundService shutdown honoring the stopping token, caching tiers (in-process vs distributed Redis vs output caching), liveness vs readiness health checks, and the built-in rate limiter partitioned per tenant.
Block D highlight — multi-tenant isolation and JWT validation
Derive the tenant from the authenticated token, never from a query string, route, or header the client controls — then scope every query by it (with row-level security as a backstop).
// SAFE: tenant from the verified token, not from input.
var tenantId = User.FindFirstValue("tenant_id")!; // trusted
var data = await _db.Campaigns.Where(c => c.TenantId == tenantId).ToListAsync(ct);
// NEVER: var tenantId = Request.Query["tenantId"]; // client-controlled -> cross-tenant read
A JWT bearer token is validated every request, statelessly: signature (against the issuer's JWKS), issuer, audience, expiry. The audience check is what stops a token minted for another API being replayed against yours (the confused-deputy guard). Also covered: roles vs claims vs policies vs resource-based auth, the CORS AllowAnyOrigin+credentials mistake, when CSRF actually matters (cookie auth vs bearer tokens), and middleware vs MediatR pipeline behaviors for cross-cutting concerns.
The through-line
Senior ASP.NET Core is four instincts: the pipeline is ordered and it unwinds; lifetimes are about shared state and thread-safety; the tenant and the user come from the token, never the request; and cross-cutting concerns live at the right layer. Answer every question with the mechanism, the real failure it prevents, and the trade-off.
The full guide has all 30 — each with the short answer, the deep dive with code, and the exact senior signal:
Originally published on PrepStack.
Top comments (0)