ASP.NET Core

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.

  1. 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() before UseAuthentication() (or before UseRouting() 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();

    Official documentation

  2. 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 TypedResults cover 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());

    Official documentation

  3. Q3. How do you read typed configuration, and what is the difference between IOptions, IOptionsSnapshot and IOptionsMonitor?

    Bind a section to a class with services.Configure<T>(config.GetSection("...")) (or AddOptions<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 raises OnChange, so it is the one to use inside singletons.

    builder.Services.AddOptions<SmtpOptions>()
        .BindConfiguration("Smtp")
        .ValidateDataAnnotations()
        .ValidateOnStart();

    Official documentation

  4. 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 become HttpContext.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.

    Official documentation

  5. Q5. How do you return consistent error responses for unhandled exceptions in an API?

    Register AddProblemDetails() and use UseExceptionHandler(). 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, a NotFoundException to 404. Log the exception once there instead of try/catch in every action.

    builder.Services.AddProblemDetails();
    builder.Services.AddExceptionHandler<NotFoundHandler>();
    ...
    app.UseExceptionHandler();

    Official documentation

  6. Q6. Compare in-memory caching, distributed caching and output caching.

    IMemoryCache stores 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.

    Official documentation

  7. Q7. How would you protect a public API from too many requests?

    Use the built-in rate limiting middleware: AddRateLimiter with 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");

    Official documentation

  8. Q8. How do you run background work in ASP.NET Core, and what are the risks?

    Implement BackgroundService and register it with AddHostedService. Use a Channel<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.

    Official documentation

  9. 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") });

    Official documentation

  10. 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-Origin for 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 UseCors before authorization. Don't combine AllowAnyOrigin with AllowCredentials; 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");

    Official documentation

  11. 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 Select into a DTO (best, since it fetches only the needed columns), or use Include. Use AsNoTracking() 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);

    Official documentation

  12. 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 to appsettings.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.

    Official documentation

Original questions written to explain concepts; not taken from any certification exam. Spotted a mistake? See our corrections policy.