ASP.NET Core interview questions: building real APIs
Questions built around problems you hit when running APIs in production. Each answer gives the reasoning interviewers look for, with a short code example where it helps.
Intermediate · 12 questions · Tap a question to see the answer.
Q1. Why does the order of middleware matter? Give an example of a wrong order.
Middleware runs in the order it is added for the request, and in reverse for the response. Each component decides whether to call the next one.
A common bug is calling
UseAuthorization()beforeUseAuthentication()(or beforeUseRouting()when you call it explicitly): authorization then runs without a signed-in user and every protected endpoint returns 401. Exception handling should be first so it can catch errors from everything after it.app.UseExceptionHandler("/error"); app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers();Q2. When would you choose minimal APIs over controllers?
Minimal APIs suit small services and microservices: less ceremony, fast startup, and they support Native AOT. Endpoint filters, route groups and
TypedResultscover validation, shared configuration and OpenAPI.Controllers still fit large APIs that benefit from conventions, model-binding attributes and action filters, or teams already organised around them. Both run on the same routing and can live in one app.
var orders = app.MapGroup("/orders").RequireAuthorization(); orders.MapGet("/{id:int}", async Task<Results<Ok<Order>, NotFound>> (int id, AppDbContext db) => await db.Orders.FindAsync(id) is { } o ? TypedResults.Ok(o) : TypedResults.NotFound());Q3. How do you read typed configuration, and what is the difference between
IOptions,IOptionsSnapshotandIOptionsMonitor?Bind a section to a class with
services.Configure<T>(config.GetSection("..."))(orAddOptions<T>().BindConfiguration(...), which also lets you add validation).IOptions<T>is read once and never changes.IOptionsSnapshot<T>is scoped and re-reads per request, so it picks up reloaded config.IOptionsMonitor<T>is a singleton that always returns the current value and raisesOnChange, so it is the one to use inside singletons.builder.Services.AddOptions<SmtpOptions>() .BindConfiguration("Smtp") .ValidateDataAnnotations() .ValidateOnStart();Q4. Walk through how JWT bearer authentication works in an ASP.NET Core API.
The client signs in with an identity provider (for example Microsoft Entra ID) and receives a signed access token. It sends the token in the
Authorization: Bearer <token>header.AddAuthentication().AddJwtBearer()validates the signature with the provider's published keys, then checks issuer, audience and expiry. The claims becomeHttpContext.User, and[Authorize]or policies decide access. Keep tokens short-lived, and never put secrets in a JWT, because its payload is only encoded, not encrypted.Q5. How do you return consistent error responses for unhandled exceptions in an API?
Register
AddProblemDetails()and useUseExceptionHandler(). Unhandled exceptions then become RFC 7807 Problem Details JSON with the right status code, and no stack trace leaks in production.For known exceptions, implement
IExceptionHandler(ASP.NET Core 8+) to map, say, aNotFoundExceptionto 404. Log the exception once there instead of try/catch in every action.builder.Services.AddProblemDetails(); builder.Services.AddExceptionHandler<NotFoundHandler>(); ... app.UseExceptionHandler();Q6. Compare in-memory caching, distributed caching and output caching.
IMemoryCachestores objects in the process: fastest, but each server has its own copy and it is lost on restart. Distributed caching (IDistributedCache, typically Redis) is shared by all instances and survives restarts, at the cost of a network hop and serialization.Output caching caches whole HTTP responses for endpoints, with policies and tag-based invalidation.
HybridCache(.NET 9+) combines a local and a distributed layer and protects against cache stampedes.Q7. How would you protect a public API from too many requests?
Use the built-in rate limiting middleware:
AddRateLimiterwith a fixed window, sliding window, token bucket or concurrency limiter, partitioned by user, API key or IP address, and return 429 when the limit is hit.In a multi-instance deployment the built-in limiter is per instance, so add a gateway-level limit as well (Azure API Management or Front Door) for a global limit.
builder.Services.AddRateLimiter(o => o.AddFixedWindowLimiter("api", l => { l.PermitLimit = 100; l.Window = TimeSpan.FromMinutes(1); })); ... app.UseRateLimiter(); app.MapGet("/items", GetItems).RequireRateLimiting("api");Q8. How do you run background work in ASP.NET Core, and what are the risks?
Implement
BackgroundServiceand register it withAddHostedService. Use aChannel<T>as an in-process queue when requests need to hand off work.Risks: in-process work is lost if the app restarts or scales in, and hosted services are singletons, so they must create a scope to use scoped services such as a
DbContext. For work that must not be lost, use a durable queue (Azure Service Bus, Storage Queues) and a separate worker or Azure Functions.Q9. What are health checks used for, and what is the difference between liveness and readiness?
AddHealthChecks()exposes endpoints that orchestrators and load balancers poll. Liveness answers 'is the process alive?': if it fails, restart it. Readiness answers 'can it serve traffic now?': if it fails, stop sending requests but don't restart.Put dependency checks (database, Redis) on the readiness endpoint only; otherwise a database outage makes the platform restart every healthy instance.
builder.Services.AddHealthChecks() .AddCheck("self", () => HealthCheckResult.Healthy()) .AddSqlServer(connStr, tags: ["ready"]); app.MapHealthChecks("/health/live", new() { Predicate = c => c.Name == "self" }); app.MapHealthChecks("/health/ready", new() { Predicate = c => c.Tags.Contains("ready") });Q10. A browser app on another domain gets a CORS error calling your API, but Postman works. Why?
CORS is enforced by browsers, not servers. Postman ignores it. The browser blocks the response unless the API returns
Access-Control-Allow-Originfor that origin, and for non-simple requests it first sends an OPTIONS preflight.Configure a named policy with the exact allowed origins, methods and headers, and call
UseCorsbefore authorization. Don't combineAllowAnyOriginwithAllowCredentials; it is unsafe and the framework rejects it.builder.Services.AddCors(o => o.AddPolicy("web", p => p .WithOrigins("https://app.example.com") .AllowAnyHeader() .AllowAnyMethod())); ... app.UseCors("web");Q11. An API endpoint that lists 50 orders runs 51 SQL queries. What is happening and how do you fix it?
This is the N+1 problem: one query for the orders, then one query per order when a navigation property (such as
Customer) is loaded lazily inside a loop or during serialization.Load what you need in one round trip: project with
Selectinto a DTO (best, since it fetches only the needed columns), or useInclude. UseAsNoTracking()for read-only queries and check the SQL with logging.var list = await db.Orders .AsNoTracking() .Select(o => new OrderDto(o.Id, o.Total, o.Customer.Name)) .ToListAsync(ct);Q12. Where should connection strings and API keys live for local development and for production?
Locally, use User Secrets (
dotnet user-secrets), which keeps them outside the repository. Never commit them toappsettings.json.In Azure, prefer managed identity so there is no secret at all (for Azure SQL, Storage, Key Vault). For the secrets that remain, store them in Azure Key Vault and load them with the Key Vault configuration provider, or reference them from App Service settings.
Original questions written to explain concepts; not taken from any certification exam. Spotted a mistake? See our corrections policy.