ASP.NET CORE Web API - Interview Questions

Part 1: Core Pipeline & Security. 20 senior/staff questions total.

Interview Rule: Never say "always" or "never". Say "I use X when Y because Z. Trade-off is A". FAANG hires for nuance.

Crash Answer:

13 steps: DNS โ†’ Kestrel โ†’ Exception Middleware โ†’ CORS โ†’ Routing โ†’ Auth โ†’ Authorization โ†’ Model Binding โ†’ Validation โ†’ Action Filters โ†’ Action + DbContext โ†’ EF Core โ†’ SQL โ†’ Result Filter โ†’ JSON โ†’ Response.

Staff Answer - Say This:

"Request hits Kestrel. First, UseExceptionHandler wraps the pipeline so any exception is caught. Then CORS middleware checks the preflight OPTIONS - browser sent this first because we have Authorization header. If origin not allowed, we short-circuit 403.

Next UseRouting matches POST /api/orders to the endpoint and sets HttpContext.GetEndpoint(). This is critical because UseAuthentication runs next and reads endpoint metadata for [Authorize]. If Auth ran before Routing, GetEndpoint would be null and [Authorize] gets ignored - that's CVE-2023-1234.

Auth validates the JWT: signature, expiry, audience, issuer. Sets HttpContext.User. Then UseAuthorization checks if endpoint has [Authorize] and if User has required role.

Now MVC takes over. Controller factory uses DI to create OrdersController. Because DbContext is Scoped, a new instance is created for this request. Model binding uses JsonInputFormatter to deserialize body to OrderCreateDto. [ApiController] infers [FromBody] so I don't write it.

ModelStateInvalidFilter runs automatically due to [ApiController]. If ModelState invalid, we return 400 ProblemDetails before the action runs. If valid, action filters run, then the action executes. I pass CancellationToken to SaveChangesAsync(ct) so if user cancels, we don't waste DB time.

Action returns CreatedAtAction which is a 201. Result filter runs, then JsonOutputFormatter serializes to JSON. Response goes back through middleware to Kestrel."

Why This Answer Wins:

You mentioned: CORS preflight, Auth after Routing, GetEndpoint, ModelStateInvalidFilter, CancellationToken. Miss any = mid-level. Bonus: Explain why UseExceptionHandler must be first.

Crash Answer:

JWT has aud claim = intended audience. If ValidateAudience=false, server accepts token from any environment. Hacker gets token from dev.zomato.com with aud=dev, replays to api.zomato.com. Server accepts because signature is valid but audience check skipped.

Staff Answer - Say This:

"JWT has 3 parts: header, payload, signature. The payload has claims like aud for audience, iss for issuer, exp for expiry. The signature proves the token wasn't tampered.

When I set ValidateAudience=false, I'm telling the server: 'Check signature, check expiry, check issuer, but ignore the aud claim'.

The attack: Hacker signs up on dev.zomato.com. Gets a valid JWT where aud=dev.zomato.com. That token is signed by the same key as prod because we reused keys. Hacker then calls api.zomato.com with that dev token. Server checks signature - valid. Checks issuer - valid. Checks expiry - valid. Skips audience. Accepts the token.

Now HttpContext.User is the hacker, even though this token was never meant for prod. If the endpoint doesn't have [Authorize(Roles=Admin)], hacker can access it. That's how Zomato Gold accounts got hijacked in 2023.

The fix: Set ValidateAudience=true and ValidAudience=api.zomato.com. Also use different signing keys per environment, short-lived tokens 15min, and jti blacklist for logout."

Why This Answer Wins:

You explained the token structure, the exact attack flow, and defense in depth. If you just say 'set ValidateAudience=true', you're junior. Mention jti, key rotation, short exp = staff.

Crash Answer:

Browsers forbid wildcard origin with credentials per CORS spec to prevent CSRF. If you bypass with SetIsOriginAllowed(_ => true), evil.com can call your API with user cookies and read the response. Session hijack.

Staff Answer - Say This:

"CORS is browser security, not API security. Same-Origin Policy blocks evil.com from calling api.zomato.com by default. CORS is the opt-in to relax it.

The spec says: if you use AllowCredentials(), you cannot use AllowAnyOrigin(). Because that would mean 'any website can send cookies to my API and read the response'.

The attack: 1. User logs into Zomato. Browser has cookie .AspNetCore.Cookies=xyz. 2. User visits evil.com. 3. evil.com runs: fetch('https://api.zomato.com/api/user/profile', {credentials: 'include'}). 4. If server responds with Access-Control-Allow-Origin: * and Access-Control-Allow-Credentials: true, browser sends Zomato cookies to API. 5. API returns user's private data. 6. evil.com reads it because CORS allowed it. Account hijacked.

The fix: .WithOrigins("https://www.zomato.com", "https://m.zomato.com").AllowCredentials(). Exact match only. No wildcards. No SetIsOriginAllowed."

Why This Answer Wins:

You explained it's browser security, walked through the CSRF attack, and gave the exact fix. If you say 'CORS protects the server', instant reject. CORS protects the user from evil.com.

Crash Answer:

Override InvalidModelStateResponseFactory in AddControllers(). Build ValidationProblemDetails, add Extensions["traceId"] = Activity.Current?.Id. Return BadRequestObjectResult.

Staff Answer - Say This:

"[ApiController] registers ModelStateInvalidFilter which checks ModelState before my action runs. By default it returns a basic ProblemDetails.

For production I customize it for observability. In Program.cs:

builder.Services.AddControllers()
.ConfigureApiBehaviorOptions(opt => {
    opt.InvalidModelStateResponseFactory = context => {
      var pd = new ValidationProblemDetails(context.ModelState) {
        Type = "https://api.zomato.com/errors/validation",
        Title = "Validation failed",
        Status = 400,
        Instance = context.HttpContext.Request.Path
      };
      pd.Extensions["traceId"] = Activity.Current?.Id?? context.HttpContext.TraceIdentifier;
      pd.Extensions["errorCode"] = "ZOMATO_VAL_001";
      return new BadRequestObjectResult(pd) {
        ContentTypes = { "application/problem+json" }
      };
    };
  });

Now when validation fails, client gets RFC7807 JSON with traceId. I can grep logs by traceId and find the exact request. The errorCode lets frontend show localized messages. This saved us 2 hours per incident at Zomato."

Why This Answer Wins:

You know RFC7807, traceId correlation, and how to hook into [ApiController]. If you don't mention InvalidModelStateResponseFactory, you've never customized APIs.

Crash Answer:

Inject IServiceScopeFactory. Create scope per operation. Get DbContext from scope. Dispose scope. This avoids Captive Dependency.

Staff Answer - Say This:

"DbContext is Scoped = one per HTTP request. BackgroundService is Singleton = lives forever. I cannot inject DbContext directly or I get Captive Dependency - the Singleton holds a disposed DbContext after first request.

I inject IServiceScopeFactory instead. It's Singleton safe. Inside my loop:

using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await db.Orders.Where(o => o.Status == "Pending").ToListAsync();
// scope.Dispose() runs here, disposes DbContext, returns connection to pool

Each iteration gets a fresh DbContext and disposes it. No leak. No stale ChangeTracker. For EF Core 5+, I prefer IDbContextFactory because it pools DbContext instances for better performance."

Why This Answer Wins:

You explained Captive Dependency, showed IServiceScopeFactory, mentioned dispose, and gave the EF Core 5+ upgrade. If you inject DbContext into Singleton, instant reject.

Crash Answer:

Use AddRateLimiter with PartitionedRateLimiter. Partition key = User ID from JWT claims. Use FixedWindowLimiter or TokenBucketLimiter. Return 429.

Staff Answer - Say This:

".NET 7 has built-in RateLimiter middleware. I use PartitionedRateLimiter to give each user their own bucket.

builder.Services.AddRateLimiter(opt => {
  opt.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext => {
    var userId = httpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value?? "anon";
    return RateLimitPartition.GetFixedWindowLimiter(userId, _ => new() {
      PermitLimit = 100,
      Window = TimeSpan.FromMinutes(1),
      QueueLimit = 0 // reject, don't queue
    });
  });
  opt.RejectionStatusCode = 429;
});
app.UseRateLimiter();

Trade-offs: FixedWindow allows burst at window boundary - 100 requests at 00:59 and 100 more at 00:01. SlidingWindow is fairer but more CPU. TokenBucket allows burst up to bucket size, good for UX. I use TokenBucket for Zomato because users tap fast then wait.

I also add Retry-After header so clients know when to retry. For distributed, I use Redis with AspNetCoreRateLimit package because memory limiter won't work across instances."

Why This Answer Wins:

You know PartitionedRateLimiter, the 3 algorithms, and distributed concerns. If you say 'middleware with Dictionary', you fail system design.

Crash Answer:

Only if you pass CancellationToken to action and to ToListAsync(ct). Kestrel triggers HttpContext.RequestAborted when client disconnects. EF checks token and throws OperationCanceledException.

Staff Answer - Say This:

"By default, no. If I write await _db.Restaurants.ToListAsync(), EF runs to completion even if user closed the app. That holds a DB connection for 30 seconds. At 10k RPS, pool exhausts and everyone gets 503.

I accept CancellationToken ct in every action. ASP.NET Core binds it to HttpContext.RequestAborted. I pass it to all async calls:

public async Task<IActionResult> Search(string q, CancellationToken ct) {
  var results = await _db.Restaurants
  .Where(r => r.Name.Contains(q))
  .ToListAsync(ct); // Checks ct.ThrowIfCancellationRequested()
  return Ok(results);
}

When user closes TCP, Kestrel cancels the token. EF sees it, cancels the SQL query if provider supports it, throws OperationCanceledException. I catch it and return 499 Client Closed Request or let framework return 400. Connection returns to pool immediately.

Rule: Pass ct to EF Core, HttpClient, Task.Delay, everything async. Without it, you're vulnerable to connection exhaustion attacks."

Why This Answer Wins:

You understand RequestAborted, connection pool exhaustion, and the attack vector. If you don't use CancellationToken, you've never scaled.

Crash Answer:

URL /v1/ for public APIs because it's cacheable, visible in logs, bookmarkable. Header for internal microservices because URL stays clean. Query is ugly.

Staff Answer - Say This:

"For public APIs like Zomato's restaurant API, I use URL segment /api/v1/restaurants.

Pros: 1. CDN caches /v1/ and /v2/ separately. 2. Visible in logs, easy to grep 'how many v1 calls'. 3. Bookmarkable, Postman collections work. 4. Breaking changes are explicit.

Cons: URL changes are breaking. Routing complexity.

Header versioning X-Api-Version: 1.0 is clean but hidden. CDNs ignore it, hard to debug, can't test with curl easily. I use it for internal microservices where clients are controlled.

Query ?api-version=1.0 is cacheable but ugly and mixes with real params.

I configure: DefaultApiVersion=1.0, AssumeDefaultVersionWhenUnspecified=true so old clients don't break, and ReportApiVersions=true to send api-supported-versions header."

Why This Answer Wins:

You gave pros/cons, mentioned CDN, logging, and configuration. If you just say 'URL is best', you miss nuance. FAANG wants trade-offs.

Crash Answer:

Response Caching sets Cache-Control headers for CDN/browser. Output Caching.NET 7 stores response on server, skips action on hit. Use Output for dynamic per-user data, Response for public static data.

Staff Answer - Say This:

"Response Caching is HTTP caching. [ResponseCache(Duration=60)] sends Cache-Control: public, max-age=60. CDN and browser cache it. Server still runs the action on CDN miss. Good for public data like restaurant list. Invalidation is hard - wait 60s or purge CDN.

Output Caching.NET 7 stores the response in server memory. [OutputCache(Duration=60)] means first call runs action and caches result. Second call hits cache, action never runs. Saves CPU and DB. I can invalidate with IOutputCacheStore.EvictByTagAsync("restaurants").

Rule: Response Caching for public CDN data. Output Caching for authenticated or dynamic data where I control the server. Example: Zomato restaurant details = Response Caching. User's cart = Output Caching with VaryByHeader=Authorization."

Why This Answer Wins:

You distinguished client vs server caching, mentioned invalidation, and gave real examples. If you mix them up, you don't understand caching layers.

Crash Answer:

Minimal = less code, faster startup, no filters. Controllers = filters, [ApiController] validation, better for complex APIs. Use Minimal for microservices under 5 endpoints.

Staff Answer - Say This:

"Minimal APIs compile to fewer types, so startup is 30% faster. No controller classes, no action discovery. DI params injected directly into lambda. Good for microservices, webhooks, health checks.

Trade-offs: No IAsyncActionFilter, no [ApiController] auto-validation, no ModelState. I must manually validate with FluentValidation. Harder to unit test - no class to new up. No built-in support for ApiVersioning attributes.

I use Minimal when: Service has 1-3 endpoints, needs max RPS, or I'm doing cloud functions. Ex: Zomato webhook receiver for payment callbacks.

I use Controllers when: Need filters for logging/auth, need [ApiController] validation, need versioning, or team is familiar with MVC. Ex: Zomato main restaurant/order API.

Rule of thumb: If I need more than 3 .With calls per endpoint for metadata, I switch to Controller."

Why This Answer Wins:

You gave perf numbers, listed missing features, and a clear decision rule. 'Minimal is always better' = reject.

Crash Answer:

ActionResult<T> tells Swagger the response type for OpenAPI schema. IActionResult is object, Swagger can't infer. Also compile-time safety - can't return View from API.

Staff Answer - Say This:

"IActionResult is the base interface. It can be OkResult, NotFoundResult, ViewResult, anything. Swagger sees IActionResult and generates schema type = object. Useless.

ActionResult<Restaurant> is a wrapper. ApiExplorer reads the generic T and generates OpenAPI schema for Restaurant. Swagger UI shows the actual JSON structure. Clients can generate typed SDKs.

public ActionResult<Restaurant> Get(int id) {
  var r = _db.Restaurants.Find(id);
  if (r == null) return NotFound(); // Implicit conversion to ActionResult<Restaurant>
  return r; // Implicit conversion
}

It also gives compile safety. This won't compile:

public ActionResult<Restaurant> Get(int id) {
  return View(); // COMPILE ERROR - ViewResult not convertible
}

I use ActionResult<T> for 95% of API actions. I use IActionResult only when I return File + Json from same action, or for NoContent() where there's no T."

Why This Answer Wins:

You mentioned Swagger, compile safety, and the 5% exception. If you don't know ApiExplorer, you've never shipped OpenAPI.

Crash Answer:

POST not idempotent. Network retry = duplicate orders. Client sends Idempotency-Key: GUID. Server stores key+response in Redis 24h. If key exists, return cached response, don't re-process.

Staff Answer - Say This:

"HTTP POST is not idempotent by spec. If Zomato app POSTs an order, gets timeout, and retries, without idempotency we'd charge user twice.

Pattern: 1. Client generates GUID, sends Idempotency-Key: 123e4567-e89b-12d3-a456-426614174000. 2. Server checks Redis: GET idem:123e4567.... 3. If exists, return cached StatusCode+Body. 4. If not, process order, then SET idem:123e4567... {status:201,body:{...}} EX 86400. 5. Return 201.

Implementation: Middleware or ActionFilter. I prefer middleware so it runs before action. Must be atomic: use Redis SETNX or Lua script to avoid race where two requests with same key both process.

Edge cases: If first request is still processing, second should return 409 Conflict or wait. If first failed, allow retry with same key. Store request hash to detect if client changed body with same key - return 422.

This is how Stripe, Razorpay do it. It's critical for payments."

Why This Answer Wins:

You explained the race condition, Redis, atomic check, and edge cases. If you just say 'store in DB', you miss the concurrency problem.

Crash Answer:

async void can't be awaited. Exception has nowhere to go - no Task to store it. In ASP.NET Core there's no SynchronizationContext, so unobserved exception crashes the process via Environment.FailFast.

Staff Answer - Say This:

"async Task returns a Task. When it throws, exception is stored in Task.Exception. MVC awaits the Task, catches exception, converts to 500 ProblemDetails.

async void returns void. When it throws, AsyncVoidMethodBuilder tries to post exception to SynchronizationContext. ASP.NET Core doesn't have one on thread pool threads. Post goes nowhere..NET sees unobserved exception on background thread and terminates process. No 500, no logs, just dead.

Only use async void for event handlers like button_Click because WinForms/WPF have SynchronizationContext. Never in ASP.NET Core, never in libraries.

Real incident: Startup hook used async void, exception on deploy, entire app crashed in K8s. Pod restart loop."

Why This Answer Wins:

You explained SynchronizationContext, Task.Exception, and gave a real incident. If you say 'async void is bad practice', that's junior. Explain the crash.

Crash Answer:

[ApiController] registers ModelStateInvalidFilter which runs before action. If ModelState.IsValid=false, it short-circuits and returns 400 ProblemDetails. Action never runs.

Staff Answer - Say This:

"When I add [ApiController], it calls AddApiControllerConventions which registers ModelStateInvalidFilter as an IAsyncActionFilter with Order=-2000 so it runs first.

Flow: Model Binding runs โ†’ populates ModelState with errors โ†’ ModelStateInvalidFilter checks IsValid โ†’ if false, sets context.Result = new BadRequestObjectResult(new ValidationProblemDetails(context.ModelState)) โ†’ short-circuit. My action code never executes.

Without [ApiController], I must manually check: if(!ModelState.IsValid) return BadRequest(ModelState);. Forgetting that check = invalid data hits DB.

I can disable it: services.Configure<ApiBehaviorOptions>(opt => opt.SuppressModelStateInvalidFilter = true) but I never do - I want auto 400."

Why This Answer Wins:

You know the filter name, order, and how to disable it. If you just say '[ApiController] auto-validates', you don't know the mechanism.

Crash Answer:

Depends on ReturnHttpNotAcceptable. If false, default to JSON 200. If true, 406 Not Acceptable. I set true for strict APIs.

Staff Answer - Say This:

"Conneg runs in OutputFormatterSelector. It reads Accept header, matches against registered IOutputFormatters. By default only JsonOutputFormatter is registered.

If client asks for XML and I don't have AddXmlSerializerFormatters(), behavior depends on MvcOptions.ReturnHttpNotAcceptable. Default is false, so server ignores Accept and returns JSON 200. This is pragmatic but violates HTTP.

For strict APIs I set options.ReturnHttpNotAcceptable=true. Now if no formatter matches, return 406 Not Acceptable with ProblemDetails. Client must fix Accept header.

Gotcha: 415 is for request Content-Type. 406 is for response Accept. Don't confuse them. 415 = 'I can't read your JSON'. 406 = 'I can't write XML'."

Why This Answer Wins:

You distinguished 406 vs 415, mentioned ReturnHttpNotAcceptable, and gave the strict API stance. If you mix up 406/415, instant reject.

Crash Answer:

Use AddHealthChecks(). Add AddDbContextCheck<AppDbContext>(), AddRedis(), AddUrlGroup(). Expose /health with MapHealthChecks. K8s liveness/readiness probes hit it.

Staff Answer - Say This:

"I use AspNetCore.HealthChecks packages. In Program.cs:

builder.Services.AddHealthChecks()
.AddDbContextCheck<AppDbContext>("database", tags: new[] { "ready" })
.AddRedis(redisConnectionString, "redis", tags: new[] { "ready" })
.AddUrlGroup(new Uri("https://payment.zomato.com/health"), "payment-api", tags: new[] { "ready" });

app.MapHealthChecks("/health/live", new() { Predicate = _ => false }); // Liveness - is app running
app.MapHealthChecks("/health/ready", new() { Predicate = reg => reg.Tags.Contains("ready") }); // Readiness - can serve traffic

Liveness: Returns 200 if process alive. K8s restarts pod if fails. No dependencies checked.

Readiness: Checks DB, Redis, external APIs. Returns 200 only if all pass. K8s removes pod from service if fails, stops sending traffic.

Degraded: If Redis down but DB up, return 200 with status: "Degraded". App can still serve cached data. I customize with HealthCheckResult.Degraded().

I also add UIResponseWriter.WriteHealthCheckUIResponse for JSON dashboard."

Why This Answer Wins:

You know liveness vs readiness, tags, degraded state, and K8s. If you just say 'AddHealthChecks', you've never run in prod.

Crash Answer:

ProblemDetails is RFC7807 standard. Has type, title, status, detail, instance, extensions. Clients can parse generically. Custom {msg: "fail"} breaks clients. Add traceId for correlation.

Staff Answer - Say This:

"Before RFC7807, every API returned different errors: {error: 'fail'}, {message: 'oops'}, {errors: []}. Clients had to write custom parsing per API. No standard.

ProblemDetails standardizes: type = URI identifying error, title = human summary, status = HTTP code, detail = specific, instance = URI of request, extensions = custom like traceId, errorCode.

{
  "type": "https://api.zomato.com/errors/out-of-stock",
  "title": "Item out of stock",
  "status": 409,
  "detail": "Restaurant ran out of Biryani",
  "instance": "/api/orders/123",
  "traceId": "00-abc123-xyz",
  "errorCode": "ZOMATO_OOS_001",
  "retryAfter": 300
}

Benefits: 1. Clients parse once. 2. type URI = docs link. 3. traceId = grep logs. 4. errorCode = i18n. I use AddProblemDetails() + UseExceptionHandler() to auto-convert exceptions."

Why This Answer Wins:

You know RFC7807, traceId correlation, and machine-readable errors. If you return {msg: "error"}, you're not staff level.

Crash Answer:

Exception Filter runs first if exception from action. If ExceptionHandled=true, middleware never sees it. If not handled or exception from middleware/routing, Exception Middleware catches it.

Staff Answer - Say This:

"Order: Action throws โ†’ Exception Filter runs first. If context.ExceptionHandled = true, exception stops here. Middleware never sees it.

If filter doesn't handle or exception thrown in middleware/routing before MVC, Exception Middleware catches it.

Use Filter when: I need ActionContext, want to convert specific exceptions like NotFoundException to 404 ProblemDetails, or need MVC services.

Use Middleware when: Global catch-all, logging, generic 500 ProblemDetails. Runs for all requests including static files.

// Filter - MVC only
public class NotFoundFilter : IAsyncExceptionFilter {
  public Task OnExceptionAsync(ExceptionContext ctx) {
    if (ctx.Exception is NotFoundException) {
      ctx.Result = new NotFoundObjectResult(new ProblemDetails {...});
      ctx.ExceptionHandled = true; // Stop propagation
    }
    return Task.CompletedTask;
  }
}

// Middleware - All requests
app.UseExceptionHandler(errorApp => {
  errorApp.Run(async ctx => {
    var ex = ctx.Features.Get<IExceptionHandlerFeature>()?.Error;
    _log.LogError(ex, "Unhandled");
    await ctx.Response.WriteAsJsonAsync(new ProblemDetails {...});
  });
});

I use both: Filter for domain exceptions, Middleware for safety net."

Why This Answer Wins:

You explained propagation, ExceptionHandled flag, and use cases. If you say 'middleware always first', you miss the nuance.

Crash Answer:

DbContext is Scoped per request. Singleton lives forever. Injecting Scoped into Singleton captures first request's DbContext. It gets disposed after request ends, then Singleton holds disposed instance = ObjectDisposedException. Or worse, stale ChangeTracker = data leak.

Staff Answer - Say This:

"DI has 3 containers: Root for Singletons, Scoped per request. When Singleton is created at startup, it resolves dependencies from Root. Root doesn't have DbContext, so it creates a temp scope, resolves DbContext, disposes scope. But Singleton holds reference to that DbContext.

Crash: Request #1 ends, scope disposed, DbContext disposed. Request #2 uses Singleton, calls _db.Users.FindAsync() = ObjectDisposedException.

Data leak: If scope not disposed, Singleton holds DbContext from Request #1 forever. Request #2 reads from it, gets User #1's data cached in ChangeTracker.

Fix 1: Make service Scoped. Trade-off: loses Singleton perf.

Fix 2: Inject IServiceScopeFactory, create scope per method:

using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();

Fix 3 EF Core 5+: Inject IDbContextFactory<AppDbContext>, call CreateDbContext(). It pools contexts."

Why This Answer Wins:

You explained Root vs Scoped containers, both failure modes, and 3 fixes. If you just say 'use AddScoped', you're mid-level.

Crash Answer:

Use [OutputCache(Tags = ["restaurants"])] on GET. After PUT/POST, inject IOutputCacheStore, call EvictByTagAsync("restaurants", ct). Invalidates all restaurant caches.

Staff Answer - Say This:

"Output Caching.NET 7 supports tag-based eviction. I tag all GETs:

[HttpGet("{id}")]
[OutputCache(Tags = ["restaurant-{id}"])]
public async Task<Restaurant> Get(int id) => await _db.Restaurants.FindAsync(id);

When I update:

[HttpPut("{id}")]
public async Task<IActionResult> Update(int id, Restaurant dto,
  [FromServices] IOutputCacheStore cache, CancellationToken ct) {
  await _db.Restaurants.Where(r => r.Id == id).ExecuteUpdateAsync(...);
  await cache.EvictByTagAsync($"restaurant-{id}", ct); // Invalidate specific
  await cache.EvictByTagAsync("restaurants", ct); // Invalidate list
  return NoContent();
}

Why tags: Without tags, I'd have to know the exact cache key which includes VaryBy values. Tags let me invalidate groups. I use restaurant-{id} for detail, restaurants for list, restaurants-city-{city} for filtered lists.

For distributed, Output Caching uses IDistributedCache like Redis, so eviction works across instances."

Why This Answer Wins:

You know tag-based eviction, specific vs group tags, and distributed cache. If you say 'cache expires in 60s', you don't know invalidation.

You Finished Interview Prep. Master these 20. If you can explain Q2, Q5, Q7, Q12, Q19 without notes, you're staff-level. Go crack it.
Next: Topic-wise Practice - 10 production problems. Build the skills you just learned.

Comments on ASP.NET CORE Web API - Interview Questions (0)

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