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

#LayerWhat It DoesProd Failure If Missing
1Kestrel 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
2ExceptionHandler app.UseExceptionHandler(). Catches all unhandled. Must be FIRST Exception in Routing = YSOD with stack trace. SQL injection next
3HSTS Strict-Transport-Security. Forces HTTPS. Browser preload list HTTP downgrade attack. Cookie stolen
4HttpsRedirection 301 HTTP โ†’ HTTPS Mixed content warnings. Auth token sent over HTTP
5ForwardedHeaders Reads X-Forwarded-Proto from nginx/AWS ALB. Sets HttpContext.Request.Scheme Redirects to http:// behind HTTPS load balancer. Auth cookies not Secure
6StaticFiles Serves wwwroot. Short-circuits if file found /swagger/index.html 404 if before UseSwagger
7Routing UseRouting(). Matches URL to Endpoint. Sets HttpContext.GetEndpoint() No endpoint = 404. Auth after routing = [Authorize] ignored = CVE-2023-1234
8CORS Checks Origin header. Handles OPTIONS preflight. Must be after Routing, before Auth Preflight 401 because Auth ran first. Browser blocks real request
9Authentication Reads Authorization header. Validates JWT. Sets HttpContext.User No User = [Authorize] always 401. ValidateAudience=false = token replay
10Authorization Reads endpoint.Metadata for [Authorize]. Checks User.Claims. 401 or 403 Missing [Authorize] = public endpoint. Admin API exposed
11RateLimiter .NET 7+. Checks PartitionedRateLimiter. 429 if exceeded No limit = bot DDoS. DB CPU 100%. 503 for all
12OutputCache .NET 7+. Checks server cache. Short-circuits if hit Cache stampede. 1000 req hit DB at once
13Endpoints 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>();
LifetimeWhen CreatedWhen DisposedZomato 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

KillerCodeImpactFix
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.

Comments on ASP.NET CORE Web API - Deep Dive (0)

No comments yet. Be the first to share your thoughts!