ASP.NET CORE Web API - Deep Dive
Staff/Principal level. Pipeline internals, perf, DI, async. This is what separates L5 from L6.
Deep Dive Rule: If you can't draw the middleware pipeline from memory and explain where DbContext gets disposed, you're not staff yet.
1. The 13-Layer Request Pipeline - Zomato Order Flow
| # | Layer | What It Does | Prod Failure If Missing |
|---|---|---|---|
| 1 | Kestrel | TCP/TLS listener. HTTP/1.1, HTTP/2, HTTP/3. Parses headers, body to Stream | No TLS = MITM. HTTP/2 disabled = 3x slower for mobile |
| 2 | ExceptionHandler | app.UseExceptionHandler(). Catches all unhandled. Must be FIRST |
Exception in Routing = YSOD with stack trace. SQL injection next |
| 3 | HSTS | Strict-Transport-Security. Forces HTTPS. Browser preload list |
HTTP downgrade attack. Cookie stolen |
| 4 | HttpsRedirection | 301 HTTP โ HTTPS | Mixed content warnings. Auth token sent over HTTP |
| 5 | ForwardedHeaders | Reads X-Forwarded-Proto from nginx/AWS ALB. Sets HttpContext.Request.Scheme | Redirects to http:// behind HTTPS load balancer. Auth cookies not Secure |
| 6 | StaticFiles | Serves wwwroot. Short-circuits if file found | /swagger/index.html 404 if before UseSwagger |
| 7 | Routing | UseRouting(). Matches URL to Endpoint. Sets HttpContext.GetEndpoint() |
No endpoint = 404. Auth after routing = [Authorize] ignored = CVE-2023-1234 |
| 8 | CORS | Checks Origin header. Handles OPTIONS preflight. Must be after Routing, before Auth | Preflight 401 because Auth ran first. Browser blocks real request |
| 9 | Authentication | Reads Authorization header. Validates JWT. Sets HttpContext.User |
No User = [Authorize] always 401. ValidateAudience=false = token replay |
| 10 | Authorization | Reads endpoint.Metadata for [Authorize]. Checks User.Claims. 401 or 403 | Missing [Authorize] = public endpoint. Admin API exposed |
| 11 | RateLimiter | .NET 7+. Checks PartitionedRateLimiter. 429 if exceeded | No limit = bot DDoS. DB CPU 100%. 503 for all |
| 12 | OutputCache | .NET 7+. Checks server cache. Short-circuits if hit | Cache stampede. 1000 req hit DB at once |
| 13 | Endpoints | UseEndpoints(). Runs MVC pipeline: Filters โ Model Binding โ Action โ Result |
Action throws = ExceptionHandler catches. If no handler = YSOD |
2. DI Lifetime Deep Dive - The 3 Containers
Staff Knowledge: There are 3 IServiceProvider instances. Root, Scope, and Transient. Most devs think there's 1.
// ROOT CONTAINER - Created at startup. Lives forever. Holds Singletons
var rootProvider = builder.Services.BuildServiceProvider();
// SCOPE CONTAINER - Created per HTTP request. Holds Scoped services
using var scope = rootProvider.CreateScope();
var scopedProvider = scope.ServiceProvider; // DbContext lives here
// TRANSIENT - New instance every GetService call
var transient = scopedProvider.GetRequiredService<ITransientService>();
| Lifetime | When Created | When Disposed | Zomato Incident |
|---|---|---|---|
| Singleton | Root container, at startup | App shutdown | Injected DbContext = Captive Dependency. Holds first request's DbContext forever. ObjectDisposedException or data leak |
| Scoped | Request scope, per HTTP request | End of request, scope.Dispose() | DbContext safe here. One per request. Injected into Singleton = Captive |
| Transient | Every GetService call | End of scope if DI created it. If you `new` it, YOU dispose | HttpClient as Transient = socket exhaustion. Use IHttpClientFactory |
Captive Dependency - The #1 DI Bug
// BUG - Singleton captures Scoped
builder.Services.AddSingleton<IOrderProcessor, OrderProcessor>();
builder.Services.AddScoped<AppDbContext, AppDbContext>();
public class OrderProcessor {
private readonly AppDbContext _db; // Captures FIRST request's DbContext
public OrderProcessor(AppDbContext db) => _db = db; // DI resolves from Root scope
}
// Request #1: Root creates scope, creates DbContext, injects, disposes scope.
// _db now disposed. Request #2: _db.Users.ToListAsync() = ObjectDisposedException
// FIX - Inject IServiceScopeFactory
public OrderProcessor(IServiceScopeFactory scopeFactory) {
_scopeFactory = scopeFactory;
}
public async Task Process() {
using var scope = _scopeFactory.CreateScope(); // New scope per call
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>(); // Fresh DbContext
} // scope.Dispose() โ db.Dispose()
3. Async/Await Internals - Why async void Kills Prod
Principal Knowledge: There is no SynchronizationContext in ASP.NET Core. That's why async void crashes.
The State Machine
public async Task<IActionResult> Get() {
var data = await _db.GetAsync(); // Compiler rewrites to state machine
return Ok(data);
}
// Compiler generates:
class <Get>d__1 : IAsyncStateMachine {
public int <>1__state;
public AsyncTaskMethodBuilder<IActionResult> <>t__builder;
public TaskAwaiter<Data> <>u__1;
void MoveNext() {
if (<>1__state == 0) {
try {
var result = <>u__1.GetResult(); // Throws if faulted
<>t__builder.SetResult(Ok(result)); // Completes Task
} catch (Exception ex) {
<>t__builder.SetException(ex); // Stores exception in Task
}
}
}
}
Key Point: AsyncTaskMethodBuilder.SetException(ex) stores exception in Task.Exception. MVC awaits Task, catches exception, returns 500.
Why async void Crashes
public async void Bad() {
await Task.Delay(100);
throw new Exception("boom");
}
// Compiler uses AsyncVoidMethodBuilder
// On exception: AsyncVoidMethodBuilder.SetException(ex)
// โ Posts to SynchronizationContext.Current
// ASP.NET Core: SynchronizationContext.Current == null
// โ Exception unobserved on thread pool
// โ Environment.FailFast() โ Process kill
Rule: async Task for libraries/APIs. async void ONLY for event handlers in UI with SyncCtx. Never in ASP.NET Core.
4. EF Core Performance - The 5 Killers
| Killer | Code | Impact | Fix |
|---|---|---|---|
| N+1 | foreach(var o in orders) { var u = o.User; } |
1000 orders = 1001 queries. 10s response | .Include(o => o.User) or .Select(o => new {...}) |
| Client Evaluation | _db.Users.Where(u => MyFunc(u.Name)) |
Loads all users to memory. 10GB RAM | Use EF.Functions or store computed column |
| No AsNoTracking | _db.Restaurants.ToListAsync() for read-only |
ChangeTracker tracks 10k entities. 500MB RAM | .AsNoTracking() for queries. 90% less memory |
| Missing Index | .Where(r => r.CityId == 5) no index |
Table scan. 5s for 1M rows | .HasIndex(r => r.CityId) in OnModelCreating |
| DbContext Pool Exhaustion | No CancellationToken. User cancels, query runs 30s |
Pool size 100. 100 cancels = pool empty. 503s | Pass ct to all async. ToListAsync(ct) |
5. Authentication Deep Dive - JWT Validation Pipeline
// 1. JwtBearerHandler.HandleAuthenticateAsync()
var token = GetTokenFromHeader(); // Authorization: Bearer ey...
var validationParams = Options.TokenValidationParameters;
// 2. TokenHandler.ValidateToken()
var principal = handler.ValidateToken(token, validationParams, out var validToken);
// 3. Validation steps - ALL must pass
ValidateSignature(validToken, validationParams.IssuerSigningKey); // HMAC SHA256
ValidateIssuer(validToken.Issuer, validationParams.ValidIssuer); // "https://auth.zomato.com"
ValidateAudience(validToken.Audience, validationParams.ValidAudience); // "https://api.zomato.com"
ValidateLifetime(validToken.ValidFrom, validToken.ValidTo, validationParams.ClockSkew); // exp, nbf
ValidateIssuerSigningKey(); // Key not revoked
// 4. If all pass: HttpContext.User = ClaimsPrincipal
// If any fail: 401 Unauthorized + WWW-Authenticate: Bearer error="invalid_token"
Staff Tip: Set ClockSkew = TimeSpan.Zero in prod. Default 5min allows replay attack window. Only use 5min if servers not NTP synced.
6. Minimal APIs - Filter Pipeline
app.MapGet("/restaurants/{id}", async (int id, AppDbContext db) => {
return await db.Restaurants.FindAsync(id);
})
.AddEndpointFilter<ValidationFilter>() // Runs before handler
.AddEndpointFilter<LoggingFilter>(); // Runs after ValidationFilter
class ValidationFilter : IEndpointFilter {
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext ctx, EndpointFilterDelegate next) {
if (ctx.Arguments[0] is int id && id < 0)
return Results.BadRequest("ID negative"); // Short-circuit
return await next(ctx); // Call next filter or handler
}
}
vs Controllers: No IAsyncActionFilter. Use IEndpointFilter. No ModelStateInvalidFilter. Must manually validate. Faster startup, less reflection.
Deep Dive Complete. If you can whiteboard the 13-layer pipeline and explain Captive Dependency without notes, you're L6. Next: System Design.
No comments yet. Be the first to share your thoughts!