Skip to content
Web and Mobile Development

ASP.NET Web API interview questions and answers

84 questions, 31 with code. About 6 h 5 min to read them all.

By TestLab · Updated

Plan for an interview
84 questions

Track what you know and what to revise.

Sign in to track progress

You can open a few full answers without signing in. Sign in to read all

1

Define ASP.NET Web API and explain the scenarios where it is typically employed.

ASP.NET Core Web API is a framework within the ASP.NET Core ecosystem designed specifically for building HTTP services that return data rather than views. It enables developers to expose backend functionality over standard HTTP protocols, making it the primary choice for constructing RESTful services consumed by web clients, mobile applications, and other backend systems.

Read full answer

Applies toASP.NET Core Web API on .NET 8

Core Definition and Architecture

ASP.NET Core Web API is a subset of ASP.NET Core optimized for generating HTTP responses serialized typically as JSON. Unlike MVC controllers which often return HTML views, API controllers focus on data exchange using standard HTTP verbs like GET, POST, PUT, and DELETE. It integrates deeply with the middleware pipeline, handling routing, model binding, and content negotiation automatically. This architecture supports both controller-based patterns and Minimal APIs, providing flexibility for different service sizes.

Typical Use Cases

  • RESTful Services: Exposing resources via standard URI patterns and HTTP methods for stateless communication.

  • Mobile and SPA Backends: Serving data to Angular, React, iOS, or Android clients that require JSON payloads.

  • Microservices: Building lightweight, independently deployable services that communicate over HTTP within a distributed system.

  • Third-Party Integrations: Providing public endpoints for external developers to interact with your platform securely.

  • Server-to-Server Communication: Allowing backend systems to exchange data programmatically without a user interface.

Key Characteristics in .NET 8

Modern development distinguishes between classic ASP.NET Web API 2 (legacy, .NET Framework) and ASP.NET Core Web API. In .NET 8, the framework emphasizes: - Controller-based APIs: Using [ApiController] and ControllerBase to enable automatic model validation and attribute routing. - Minimal APIs: Introduced in .NET 6 and matured in .NET 8, this pattern uses MapGet, MapPost, and other mapping methods without controllers, suitable for smaller services or microservices. - Problem Details: Errors are returned following RFC 9457 (application/problem+json) using Problem() or ValidationProblem(), replacing the older RFC 7807 format. - Automatic 400 Responses: With [ApiController], invalid model state automatically triggers a 400 Bad Request without manual checks. - Content Negotiation: Responses can be serialized as JSON, XML, or other formats based on the client’s Accept header.

Best Practices and Security

  • Authentication: Secure endpoints using JWT bearer tokens or ASP.NET Core Identity, ensuring UseAuthentication() precedes UseAuthorization() in the middleware pipeline.

  • CORS: Configure CORS explicitly using policies; never allow any origin together with credentials enabled.

  • Versioning: Use the Asp.Versioning.Mvc package for API versioning; avoid the deprecated Microsoft.AspNetCore.Mvc.Versioning.

  • Documentation: In .NET 8, templates typically configure Swashbuckle (AddSwaggerGen) for OpenAPI documentation, though .NET 9 shifts toward Microsoft.AspNetCore.OpenApi.

  • Rate Limiting: Enable built-in rate limiting via AddRateLimiter to protect resources from abuse.

  • HTTP Clients: When calling downstream services, use IHttpClientFactory instead of instantiating HttpClient directly to avoid socket exhaustion.

2

What are the primary architectural distinctions between ASP.NET Web API, Windows Communication Foundation (WCF), and ASP.NET MVC regarding their intended use cases and communication protocols?

ASP.NET Web API is designed specifically for building HTTP-focused services that return data (typically JSON/XML), while WCF supports multiple transport protocols for distributed enterprise systems, and ASP.NET MVC is tailored for rendering HTML views for web applications.

Read full answer

Applies to.NET Framework 4.8 (classic distinction) and .NET 8 (modern unification)

Classic Architecture Differences

In the .NET Framework era, these technologies served distinct roles. ASP.NET Web API used the ApiController base class to handle HTTP requests and return serialized data. ASP.NET MVC used the Controller base class to render Razor views for browsers. WCF used [ServiceContract] interfaces to expose operations over SOAP, TCP, or HTTP, relying on configuration-heavy web.config settings. Web API 2 specifically utilized WebApiConfig for routing, distinct from MVC’s RouteConfig.

Protocol and Message Semantics

Web API adheres strictly to HTTP semantics: GET and PUT are idempotent, POST creates resources, and status codes like 201 Created or 404 Not Found convey outcomes. WCF wraps messages in SOAP envelopes with WS-* standards for security and reliability, adding overhead. MVC focuses on HTML output, though it can return JSON via JsonResult. Error handling in modern APIs uses Problem Details (RFC 9457) via Problem(), whereas WCF uses SOAP Faults. In classic Web API, errors were often returned as HttpResponseMessage or custom objects.

Modern Unification in .NET 8

In ASP.NET Core (including .NET 8), Web API and MVC are unified into a single framework. Controllers inherit from ControllerBase (for APIs) or Controller (for views), sharing the same pipeline. Minimal APIs offer an alternative without controllers. WCF is not supported for service hosting on .NET Core/8; existing WCF services should be migrated to HTTP APIs or gRPC. OpenAPI documentation uses Swashbuckle in .NET 8 (AddSwaggerGen), replacing earlier WCF metadata endpoints.

Selection Guidelines and Best Practices

For new development, choose ASP.NET Core Web API on .NET 8. Use [ApiController] for automatic model validation (returning 400) and attribute routing. Secure endpoints with JWT bearer authentication, ensuring UseAuthentication precedes UseAuthorization. Configure CORS explicitly rather than allowing any origin with credentials. Return errors as Problem Details using Problem() for RFC 9457 compliance. Avoid starting new projects with WCF or classic Web API 2 unless maintaining legacy systems.

Comparison Summary

Feature ASP.NET Web API (Classic) ASP.NET MVC (Classic) WCF
Primary Output JSON/XML Data HTML Views SOAP/Message
Transport HTTP Only HTTP HTTP, TCP, MSMQ
Base Class ApiController Controller Service Contract
.NET 8 Status Unified with MVC Unified with API Legacy/Client Only

This table clarifies the historical distinctions while highlighting the current unified state. Understanding these differences is crucial for maintaining legacy enterprise systems while designing modern cloud-native solutions.

3

Describe the core principles of REST architecture and explain how ASP.NET Web API supports building services that follow those principles.

REST (Representational State Transfer) is an architectural style for distributed hypermedia systems built on HTTP, defined by Roy Fielding’s dissertation, and ASP.NET Web API is Microsoft’s framework for implementing HTTP services that follow it. In practice, REST models an application as a set of resources identified by URIs, manipulated through standard HTTP verbs, with messages that are self-descriptive and usually JSON-encoded.

Read full answer

Applies toASP.NET Core Web API on .NET 8 (controller-based APIs using `[ApiController]` and `ControllerBase`). The classic ASP.NET Web API 2 on .NET Framework 4.8 is the deprecated legacy predecessor; the concepts below apply to both, but the APIs named are the modern .NET 8 ones.

The REST Architectural Constraints

A service is only truly RESTful when it respects these constraints:

  • Client–server: Clients and servers evolve independently; the UI (client) is separated from data storage and business logic (server).

  • Statelessness: Each request contains everything the server needs to process it. No server-side session state is relied on between requests, which improves scalability and reliability.

  • Cacheable: Responses explicitly declare whether they are cacheable (via Cache-Control, ETag, Last-Modified), letting clients and intermediaries reuse them.

  • Uniform interface: Resources are identified by URIs (e.g. /orders/42), representations are JSON or XML, messages are self-descriptive (media types, headers), and hypermedia links guide transitions (HATEOAS).

  • Layered system: Clients cannot tell whether they connect directly to the server or to an intermediary (load balancer, proxy, gateway).

  • Code on demand (optional): Servers may temporarily extend client behaviour by sending executable code, such as JavaScript.

REST Compared with RPC and SOAP

Aspect REST RPC (e.g., gRPC, JSON-RPC) SOAP
Entry point Resource URI (/orders/42) Action/method name (/GetOrder) Endpoint with WSDL
Verbs Standard HTTP methods Custom methods Mostly POST
Message format JSON, XML, any media type Usually JSON/Protobuf Enforced XML envelope
Idempotency Explicit per verb Not inherent Not inherent
Typical use Public HTTP APIs High-performance internal calls Enterprise contracts

How ASP.NET Core Web API Maps to REST

  • Resources become controllers: a controller class such as OrdersController groups the operations for a resource, decorated with [ApiController] and an attribute route like [Route("api/[controller]")].

  • HTTP verbs map to action methods: [HttpGet], [HttpPost], [HttpPut], [HttpPatch] and [HttpDelete] select the action based on the request method, so the same URI (/orders/42) behaves differently for GET and DELETE.

  • Content negotiation: the framework reads the Accept header and serializes responses as JSON by default (System.Text.Json), falling back to other formatters when configured.

  • Meaningful status codes: ActionResult<T> and IActionResult let actions return 200 OK, 201 Created with a Location header, 204 No Content, 400, 404, 409 Conflict or 500, matching REST’s self-descriptive message requirement.

  • Standardized errors: AddProblemDetails() plus Problem() and ValidationProblem() emit application/problem+json (RFC 9457), and with [ApiController] an invalid model automatically produces a 400 validation error.

HTTP Semantics That Make It Work

  • GET is safe and idempotent: repeated calls have no side effects and must not modify state.

  • PUT is idempotent: sending the same representation repeatedly yields the same result.

  • DELETE is idempotent: deleting a resource twice leaves it deleted.

  • POST is neither safe nor idempotent: each call creates a new subordinate resource and should return 201 Created.

Best Practices and Common Pitfalls

  • Use plural nouns for resource collections (/orders, /customers/7/orders) and keep verbs out of URIs; the HTTP method already carries the action.

  • Version the API from the start with the Asp.Versioning.Mvc package; Microsoft.AspNetCore.Mvc.Versioning is the old, deprecated package.

  • Always serve over HTTPS, authenticate with JWT bearer or ASP.NET Core Identity, and configure CORS explicitly — never combine “allow any origin” with credentials.

  • Return errors consistently as Problem Details rather than ad-hoc JSON, and use the correct status code instead of blanket 200 OK responses.

  • A common pitfall is calling an API “RESTful” while it tunnels actions through GET or POST (for example, POST /orders/cancel/42); that is RPC over HTTP, not REST.

4

What are the essential steps and components required to implement a standard controller class in an ASP.NET Core Web API project?

To create a basic Web API controller in ASP.NET Core, you define a class that inherits from ControllerBase and decorate it with the [ApiController] attribute to enable automatic model validation and attribute routing. Actions typically return ActionResult<T> to convey both data and HTTP status codes, ensuring proper semantics like 200 for success or 404 for missing resources.

Read full answer

Applies toASP.NET Core Web API on .NET 8

Core Structure and Attributes

The controller class must derive from ControllerBase rather than Controller if you do not need view rendering support, which reduces overhead. Applying [ApiController] at the class level enforces API-specific behaviors, such as automatic 400 responses for invalid model states and binding source parameter inference. Routing is configured using [Route("api/[controller]")], which dynamically inserts the controller name into the URL path, ensuring consistent endpoint addressing.

Action Methods and HTTP Verbs

Each public method maps to an HTTP verb using attributes like [HttpGet], [HttpPost], [HttpPut], and [HttpDelete]. GET, PUT, and DELETE operations should be idempotent, while POST creates new resources. Return types should be ActionResult<T> to allow flexibility; for instance, returning Ok(product) yields a 200 status, while NotFound() yields a 404. When creating resources, return CreatedAtAction to provide a 201 status with a Location header pointing to the new resource.

Error Handling and Problem Details

Errors should be returned using the Problem Details format (RFC 9457), which standardizes error responses across the API and replaces RFC 7807. In Program.cs, register this support via builder.Services.AddProblemDetails(). Within the controller, use Problem(), ValidationProblem(), or specific helpers like BadRequest() and Conflict() to generate compliant payloads. With [ApiController], model validation failures automatically trigger a 400 response without explicit checks.

Minimal APIs Alternative

For simpler scenarios, Minimal APIs allow defining endpoints directly in Program.cs using MapGet, MapPost, etc., without controller classes. You can use TypedResults to maintain strong typing similar to ActionResult<T>. Choose controllers for larger applications requiring organization and filters, and Minimal APIs for microservices or rapid prototyping.

Code Example

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet("{id}")]
    public ActionResult<Product> GetProduct(int id)
    {
        var product = FetchProduct(id);
        if (product is null) return NotFound();
        return product;
    }

    [HttpPost]
    public async Task<ActionResult<Product>> CreateProduct(Product product)
    {
        // ModelState invalid returns 400 automatically due to [ApiController]
        var created = await SaveProductAsync(product);
        return CreatedAtAction(nameof(GetProduct), new { id = created.Id }, created);
    }
}

Note that AddProblemDetails() should be called in Program.cs to ensure error responses follow RFC 9457.

Best Practices and Common Pitfalls

  • [Authorize]: Secure endpoints using JWT bearer authentication or ASP.NET Core Identity, ensuring UseAuthentication precedes UseAuthorization in the middleware pipeline.

  • HttpClient: Never instantiate HttpClient directly; inject IHttpClientFactory for external HTTP calls to avoid socket exhaustion.

  • CORS: Configure CORS policies explicitly; never combine AllowAnyOrigin with AllowCredentials.

  • Versioning: Use the Asp.Versioning.Mvc package for API versioning, avoiding the deprecated Microsoft.AspNetCore.Mvc.Versioning.

  • Validation: Rely on automatic model validation provided by [ApiController] rather than manual ModelState.IsValid checks.

5

Explain how request routing works in ASP.NET Web API, including how incoming URLs are matched to controller actions or endpoint handlers.

Routing is the middleware process that matches an incoming HTTP request’s URL path and method to a specific controller action or endpoint handler. In ASP.NET Core Web API on .NET 8, attribute routing is the standard approach for controller-based APIs, with route templates declared directly on controllers and actions using attributes such as [Route], [HttpGet], and [HttpPost].

Read full answer

Applies toASP.NET Core Web API on .NET 8

How Routing Works

  • The routing middleware evaluates the request path against registered route templates in order.

  • For controller-based APIs, the framework scans assemblies for [Route] and HTTP verb attributes at startup and builds a route table.

  • When a route matches, model binding maps URL segments, query strings, headers, and the request body to action parameters.

  • If no route matches, the framework returns 404; if the path matches but the HTTP method does not, it returns 405.

  • With [ApiController] applied, an invalid model state automatically returns a 400 response as Problem Details.

Attribute Routing on Controllers

  • [Route("api/[controller]")] sets a base path, where [controller] is replaced by the controller class name without the “Controller” suffix.

  • HTTP verb attributes ([HttpGet], [HttpPost], [HttpPut], [HttpDelete]) select the method and can carry their own template.

  • Route parameters use {id} syntax and bind to action parameters of the same name.

  • Optional parameters use {id?}, and constraints like {id:int} or {slug:alpha} restrict matches.

  • Token replacement also supports [action] for the method name.

Minimal APIs as an Alternative

  • Minimal APIs define routes directly on the WebApplication using MapGet, MapPost, MapPut, and MapDelete with a string pattern.

  • Patterns support the same {parameter} syntax and route constraints.

  • TypedResults provides strongly-typed results (e.g., TypedResults.Ok, TypedResults.Created) and is the recommended style for Minimal APIs.

  • Controller-based and Minimal API routes coexist in the same route table and follow the same matching rules.

Route Precedence and HTTP Semantics

  • Endpoint routing ranks matching candidates by route precedence first — literal segments outrank parameters, and constrained parameters ({id:int}) outrank unconstrained ones ({id}) — and uses registration order only as a tiebreaker, so a specific template wins even when registered after a general one.

  • Ambiguous matches, where two actions match with equal precedence and order, throw an AmbiguousMatchException at runtime.

  • Use POST for non-idempotent creation (return 201 with a Location header), PUT for idempotent replacement, and DELETE for idempotent removal (return 204 No Content).

Code Example

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductService _service;

    public ProductsController(IProductService service) => _service = service;

    // GET api/products
    [HttpGet]
    public ActionResult<IEnumerable<Product>> GetAll() => Ok(_service.GetAll());

    // GET api/products/5
    [HttpGet("{id:int}")]
    public ActionResult<Product> GetById(int id) => _service.GetById(id) is { } p ? Ok(p) : NotFound();

    // POST api/products -> 201 Created with Location header
    [HttpPost]
    public ActionResult<Product> Create([FromBody] ProductDto dto)
    {
        var product = _service.Create(dto);
        return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
    }

    // PUT api/products/5 (idempotent)
    [HttpPut("{id:int}")]
    public IActionResult Update(int id, [FromBody] ProductDto dto) => NoContent();

    // DELETE api/products/5 (idempotent) -> 204 No Content
    [HttpDelete("{id:int}")]
    public IActionResult Delete(int id) => NoContent();
}

Common Pitfalls

  • Do not mix attribute routing and convention-based routing inconsistently; attribute routing is preferred for APIs.

  • Add constraints such as :int to route parameters to make templates specific and avoid AmbiguousMatchException between overlapping templates.

  • Never return 200 for resource creation; use 201 with CreatedAtAction or CreatedAtRoute so clients can follow the Location header.

  • Remember that GET, PUT, and DELETE must be idempotent; only POST is non-idempotent.

6

Explain the routing mechanism that decides which controller action handles an incoming request in an ASP.NET Core Web API.

ASP.NET Core Web API maps requests to actions with attribute routing: each action is decorated with an HTTP-verb attribute and an optional route template, and the framework matches the request URL, HTTP method and route constraints against those templates to select exactly one action.

Read full answer

Applies toASP.NET Core Web API on .NET 8 (controller-based APIs using `[ApiController]` and `ControllerBase`).

How It Works

Routing runs inside the endpoint-routing middleware pipeline: UseRouting() resolves the matching endpoint, authorization and other middleware act on that endpoint, and the endpoint is executed by the MVC action invoker.

  1. The request URL is compared against every registered route template (controller template + action template).

  2. Candidates whose HTTP method does not match the verb attribute ([HttpGet], [HttpPost], …) are eliminated.

  3. Route constraints such as {id:int} are validated.

  4. Remaining candidates are scored by specificity — literal segments beat parameters, more constraints beat fewer, and required parameter lists must be bindable.

  5. The best match wins; no match at all produces a 404 Not Found, and two truly equal matches throw AmbiguousMatchException, so templates must be unambiguous.

Route Templates

  • [Route("api/[controller]")] on the controller class applies to every action in it; the [controller], [action] and [area] tokens are replaced at startup from the controller and action names.

  • Verb attributes carry their own segment, e.g. [HttpGet("{id:int}")], which is appended to the class template.

  • Parameters are optional when suffixed with ? ({id:int?}), and defaults can be set inline ({culture=en}).

  • app.MapControllers() in Program.cs registers the endpoints for attribute-routed actions. Conventional routing via MapControllerRoute("{controller}/{action}/{id?}") exists but is aimed at MVC views; APIs rely on attribute routing, and [ApiController] enforces it.

HTTP Method Attributes

[HttpGet], [HttpPost], [HttpPut], [HttpPatch], [HttpDelete], [HttpHead] and [HttpOptions] both constrain the verb and contribute the route. Semantically, GET, PUT and DELETE are idempotent, while POST is not, so POST is used for creation and other non-repeatable operations. Successful creation returns 201 Created with a Location header, typically produced with CreatedAtAction(...); full updates and deletions return 204 No Content.

Parameter Binding

With [ApiController], binding sources are inferred: simple types bind from route values and query string, complex types from the body via [FromBody], and the framework also infers [FromQuery], [FromRoute], [FromHeader] and [FromForm] when it can. Explicit attributes override inference. Invalid or missing model state automatically produces a 400 response with application/problem+json (RFC 9457) — no manual ModelState check is needed.

Minimal APIs as the Alternative

Minimal APIs expose the same routing engine through MapGet, MapPost, MapPut and MapDelete on IEndpointRouteBuilder, returning TypedResults for statically typed results. There is no action-selection step: one delegate is bound to one route and verb, which removes ambiguity but also removes class-level template composition.

Best Practices and Common Pitfalls

  • Use plural resource nouns and verbs, not RPC-style action names: api/products/{id} rather than api/products/GetProductById.

  • Add constraints such as :int, :guid, :minlength(3) or :regex(...) so malformed URLs fail routing with a 404 instead of reaching your action.

  • Avoid two actions with the same template and verb; differentiate them with distinct templates or constraints, otherwise routing becomes ambiguous.

  • Return proper status codes: 200, 201 with Location, 204, 404 and 400 validation errors, and let AddProblemDetails() plus Problem() / ValidationProblem() shape all error payloads.

  • Keep async all the way through the action and any data-access or IHttpClientFactory calls; blocking inside an action wastes thread-pool threads.

7

Explain what content negotiation means in an ASP.NET Core Web API and how the framework decides which format to use for a response.

Content negotiation is the process by which an ASP.NET Core Web API and its client agree on the media type used for the response body, based on the Accept header the client sends and the output formatters registered on the server. The framework selects the first formatter that can produce a representation the client accepts, and sets the Content-Type header accordingly.

Read full answer

Applies toASP.NET Core Web API on .NET 8 (controller-based APIs using `[ApiController]`; Minimal APIs use explicit `IResult` types and do not perform automatic content negotiation).

How It Works

When a client sends a request, the framework performs the following steps:

  1. The client includes an Accept header listing the media types it can handle, optionally with quality values (q), e.g. Accept: application/json, application/xml;q=0.8.

  2. The action returns an object (e.g. Ok(product) or an ActionResult<T>).

  3. The framework’s output-formatter selector examines the registered IOutputFormatter instances in the order they were added.

  4. The first formatter whose supported media types intersect with the client’s Accept list is chosen.

  5. That formatter serializes the object and writes the response, setting the matching Content-Type header.

If the client sends no Accept header or sends /, the first registered formatter (JSON by default) is used. If the client requests a specific media type that no formatter supports, the framework returns 406 Not Acceptable.

Key Components

  • Accept header: declares the client’s preferred media types; quality values let the client express relative preference.

  • Output formatters: SystemTextJsonOutputFormatter (JSON, default) and XmlDataContractSerializerOutputFormatter / XmlSerializerOutputFormatter (XML, added explicitly). They implement IOutputFormatter.

  • Input formatters: the reverse path; they deserialize the request body into action parameters during model binding.

  • Content-Type response header: indicates the actual media type of the response body.

  • Produces attribute: restricts an action to a specific set of media types; if the client asks for something outside that set, the framework returns 406 Not Acceptable.

Default Behaviour and Configuration

JSON is the default format in .NET 8, provided by System.Text.Json. XML support is not enabled by default; you add it in Program.cs with AddXmlSerializerFormatters() or AddXmlDataContractSerializerFormatters(). These formatters are part of the shared framework, so no extra NuGet package is required. Once registered, XML participates in negotiation like any other formatter.

You can also restrict an action to a single format with [Produces("application/json")]. This both documents the contract in OpenAPI and forces a 406 response when a client requests an unsupported type.

JSON vs XML Support

Aspect JSON XML
Default in .NET 8 Yes, via System.Text.Json No, must be added explicitly
Serializer System.Text.Json (JsonSerializerOptions) XmlSerializer or DataContractSerializer
Typical media type application/json application/xml
Common use Modern web and mobile clients Legacy enterprise or SOAP-adjacent systems

Best Practices and Common Pitfalls

  • Return objects, not serialized strings: return Ok(product) and let the formatter serialize it. Returning a pre-serialized string pins the response to one format and defeats negotiation.

  • Use [Produces] to document and constrain formats, which also improves OpenAPI output.

  • Do not rely on the browser Accept header: browsers often send Accept: text/html, which an API cannot produce; test with tools such as curl or Postman.

  • Validate the Accept header carefully: accepting arbitrary client-supplied media types can leak format choices you did not intend to expose.

  • Prefer JSON unless XML is required by an integration partner; JSON is faster, smaller, and the default for modern clients.

  • Minimal APIs handle this differently: they return IResult objects (e.g. Results.Json(product)) that explicitly set Content-Type, so there is no automatic negotiation based on Accept.

8

What measures do you put in place to protect an ASP.NET Web API from unauthorized access and common security threats?

Securing a Web API layers authentication and authorization on top of transport security, input validation, rate limiting, and safe configuration so only trusted callers reach protected endpoints and errors reveal nothing useful to attackers. Think defense in depth: no single control is sufficient on its own.

Read full answer

Applies toASP.NET Core Web API on .NET 8

Authentication and Authorization

  • Authentication: prove who the caller is. Use JWT bearer tokens (AddJwtBearer) for service-to-service or SPA scenarios, or ASP.NET Core Identity for user accounts. The token travels in the Authorization: Bearer header.

  • Authorization: decide what an authenticated caller may do. Apply [Authorize] on controllers or actions, and prefer policies for claims-based checks over hard-coded roles.

  • Token handling: validate signatures, expiry, and issuer; rotate signing keys and keep token lifetimes short to limit the impact of a leaked token.

  • Status codes: an unauthenticated request returns 401; an authenticated caller lacking permission returns 403.

HTTPS and CORS

  • Enforce HTTPS with UseHttpsRedirection() plus a reverse-proxy or production redirection rule. This applies on both Windows and Linux hosts.

  • Consider HTTP Strict Transport Security (HSTS) so clients only ever connect over HTTPS after the first visit.

  • Configure CORS explicitly with a named policy. Never combine AllowAnyOrigin() with AllowCredentials(); browsers reject that combination. List exact origins, methods, and headers.

  • CORS is enforced by the browser, so it protects web clients; server-to-server callers still need authentication.

Rate Limiting and Input Validation

  • Use the built-in rate limiting middleware (AddRateLimiter, .NET 7+) to throttle clients and blunt denial-of-service or credential-stuffing attacks. A throttled request returns 429.

  • Choose limits per endpoint based on cost: login and token endpoints need tighter limits than read-only queries.

  • [ApiController] automatically returns 400 when model binding or data annotations fail, so business logic never runs on invalid input.

  • Validate and encode any data you reflect back, and never trust route or query values.

Secrets and Error Handling

  • Keep keys, connection strings, and signing secrets out of code. Use user secrets in development and environment variables or a vault in production.

  • Never log secrets or tokens, and ensure your logging provider redacts sensitive headers.

  • Return errors as Problem Details (application/problem+json, RFC 9457) via AddProblemDetails() and Problem(). This standardizes error payloads and prevents stack traces from leaking.

  • When calling other services, create HttpClient through IHttpClientFactory (named or typed clients) and use async all the way; never instantiate a new HttpClient per request.

Common Pitfalls

  • Calling UseAuthorization() before UseAuthentication() means policies never see the user.

  • Using AllowAnyOrigin() with credentials silently breaks in browsers.

  • Returning raw exceptions or validation details in production exposes internals; Problem Details keeps payloads consistent.

  • Treating CORS as a security boundary; it is a browser feature, not authentication.

  • Using weak or hardcoded JWT signing secrets; store them in a vault and rotate them.

9

What options are available for hosting an ASP.NET Web API application, and how are they configured?

An ASP.NET Core Web API application runs inside a web server that accepts HTTP requests and forwards them into the application pipeline. The default and recommended choice is Kestrel, a cross-platform server built into ASP.NET Core, which in production is normally placed behind a reverse proxy rather than exposed directly to the internet.

Read full answer

Applies toASP.NET Core Web API on .NET 8

Kestrel: The Default Server

  • Cross-platform: Kestrel runs on Windows, Linux and macOS, and is the server the .NET 8 templates configure automatically.

  • Built in: No extra package is needed; it ships as part of Microsoft.AspNetCore.App.

  • Features: Supports HTTP/1.1, HTTP/2 and HTTP/3 (HTTP/3 requires explicit enablement and platform support for QUIC), TLS, and Unix domain sockets on Linux.

  • Usage: For most APIs you call builder.Build() and app.Run() with no extra setup; Kestrel binds to the URLs from ASPNETCORE_URLS, launchSettings.json or the --urls command-line argument.

Running Behind a Reverse Proxy

In production, Kestrel should sit behind a reverse proxy:

  • IIS (Windows only): Uses the ASP.NET Core Module (ANCM) to forward requests. Since .NET Core 3.0 the default is in-process hosting, where requests execute inside the IIS worker process.

  • nginx or Apache (Linux): Terminate TLS, serve static files and forward to Kestrel on a local port or Unix socket.

  • Why: The proxy provides port sharing, request filtering, TLS certificate management and protection against edge-case protocol attacks that Kestrel does not fully guard against on its own.

Windows-Only Alternatives

  • HTTP.sys: A kernel-mode HTTP driver available only on Windows. Choose it over Kestrel when you need Windows authentication at the server level or when several apps must share a single port without a reverse proxy. Configure it with builder.WebHost.UseHttpSys().

  • IIS out-of-process: The older model where ANCM launches Kestrel as a separate process; still supported but in-process hosting is preferred on .NET 8.

Containers and Cloud

  • Docker: Package the app in a Linux or Windows container; Kestrel listens on the exposed port. This is the most common deployment shape on Kubernetes (AKS, EKS, GKE).

  • Azure App Service: Manages the web server and reverse proxy for you; you deploy the compiled app or a container image.

  • Azure Container Apps / AWS ECS: Managed container hosting with built-in scaling and no server administration.

Code Example

Endpoints and limits are configured in appsettings.json or directly in code. The following shows programmatic Kestrel configuration:

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.UseKestrel(options =>
{
    options.ListenAnyIP(5000);                       // HTTP endpoint
    options.ListenAnyIP(5001, listenOptions =>       // HTTPS endpoint
    {
        listenOptions.UseHttps();
    });
    options.Limits.MaxRequestBodySize = 10 * 1024 * 1024; // 10 MB
});

builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();

Best Practices and Common Pitfalls

  • Never expose Kestrel directly to the public internet; always put a hardened reverse proxy in front.

  • Use HTTPS everywhere; terminate TLS at the proxy and call app.UseForwardedHeaders() (with ForwardedHeadersOptions configured) so X-Forwarded-For and X-Forwarded-Proto are applied and the app computes correct URLs.

  • Set explicit limits for request body size, keep-alive timeouts and concurrent connections to resist DoS attacks.

  • Do not block threads; hosting threads are shared, so use async/await all the way through controllers.

  • Add health checks with MapHealthChecks so load balancers and orchestrators can probe the app.

  • Keep secrets out of configuration files; use user secrets in development and managed identities or a secret store in production.

10

Can you explain what OWIN is and describe how it connects to ASP.NET Web API?

OWIN (Open Web Interface for .NET) is a specification that defines a standard contract between web servers and .NET web applications, decoupling the two so frameworks like ASP.NET Web API can run on any compliant host, not only IIS. Its relationship to Web API is that it provides an alternative hosting pipeline—OWIN self-hosting—that lets a Web API 2 application run outside of System.Web and IIS, for example inside a console app or Windows service.

Read full answer

Applies toASP.NET Web API 2 on .NET Framework 4.8 (OWIN is a classic ASP.NET concept and is not used by ASP.NET Core)

What OWIN Is

  • Specification: OWIN is a specification, not a library or product. It is documented at owin.org and exists to break the tight coupling between .NET web apps and the web server.

  • Motivation: The motivation mirrors Rack in Ruby or WSGI in Python: without OWIN, ASP.NET Web API was bound to System.Web.dll and the IIS request pipeline, making it hard to host elsewhere or to compose lightweight middleware.

  • Purpose: OWIN defines how a server passes a request to an application and how application components (middleware) chain together, leaving each side free to evolve independently.

The OWIN Application Delegate

  • Application delegate: The entire spec centres on one delegate signature: Func<IDictionary<string, object>, Task>.

  • Environment dictionary: The IDictionary<string, object> is the OWIN environment dictionary, carrying the request, response, headers, server variables and metadata as key-value pairs.

  • Middleware chaining: Each middleware component receives the environment, can inspect or modify it, and calls the next delegate in the chain. The final delegate is the application itself, which produces the response.

How Web API Fits Into the OWIN Pipeline

  • Traditional hosting: In traditional hosting, Web API 2 runs through IIS using Global.asax, WebApiConfig, and the ApiController base class tied to System.Web.

  • OWIN integration: With OWIN, you build an HttpConfiguration, register routes on it, and plug it into the OWIN pipeline with app.UseWebApi(config) from the Microsoft.AspNet.WebApi.Owin package.

  • Request flow: From that point the OWIN host receives raw HTTP, hands it to Web API’s HttpServer, and Web API selects the controller and action exactly as it would under IIS. The controllers themselves do not change.

Katana: Microsoft’s OWIN Implementation

  • Katana: Katana is Microsoft’s official set of OWIN components and hosts.

  • Key packages: Key NuGet packages include Microsoft.Owin (core types like IAppBuilder), Microsoft.Owin.Host.SystemWeb (runs OWIN middleware inside the IIS/System.Web pipeline), and Microsoft.Owin.SelfHost (runs OWIN in your own process).

  • Startup: An OWIN application starts from a startup class marked with [assembly: OwinStartup(typeof(Startup))] containing a Configuration(IAppBuilder app) method where middleware and Web API are wired up.

OWIN Hosting vs Traditional IIS Hosting

Aspect IIS / System.Web OWIN Self-Host
Process IIS worker process (w3wp) Your own console app or Windows service
Dependency Requires System.Web.dll and IIS Only OWIN + Web API OWIN packages
Startup Global.asax / WebApiConfig [OwinStartup] + Configuration
Lifecycle Managed by IIS Managed by your application
Typical use Standard web deployment Background services, testing, migration paths

OWIN and ASP.NET Core: When It Still Matters

  • Core does not use OWIN: ASP.NET Core does not use OWIN. It ships its own built-in middleware pipeline and Kestrel server, which were inspired by OWIN but are a separate, first-party design.

  • Modern recommendation: On .NET 8, new APIs should be built with ASP.NET Core middleware, not OWIN. OWIN remains relevant only for maintaining or migrating existing ASP.NET Web API 2 applications on .NET Framework 4.8.

  • Incompatibility: Do not attempt to mix OWIN components into an ASP.NET Core pipeline; they are incompatible.

  • Legacy hosting: You are maintaining a legacy Web API 2 codebase and need to host it outside IIS.

  • Middleware extension: You want to add lightweight OWIN middleware (logging, authentication) in front of Web API without moving to Core yet.

  • Greenfield guidance: For greenfield work, choose ASP.NET Core on .NET 8 instead, since OWIN receives no new features and the ecosystem has moved to Core’s native middleware model.

11

Compare hosting an ASP.NET Web API service in IIS versus running it as a self-hosted process, and explain when you would choose each approach.

In Web API 2, IIS hosting runs your API inside the IIS worker process and gives you the full IIS feature set (process recycling, Windows authentication, request filtering), while self-hosting runs the API in your own process using HttpSelfHostServer backed by HttpListener, giving you full control over the server lifetime but none of the IIS infrastructure. The choice determines how requests reach your controllers, who manages the process, and which platform services are available.

Read full answer

Applies toASP.NET Web API 2 on .NET Framework 4.8

How IIS Hosting Works

  • Your API is deployed as an ASP.NET application with a web.config file and runs inside w3wp.exe, the IIS worker process.

  • Requests flow through the IIS integrated pipeline; HttpControllerHandler routes them to the Web API dispatch pipeline and your ApiController classes.

  • Configuration lives in web.config (routes can also be registered in WebApiConfig.Register called from Global.asax).

  • IIS manages the process lifecycle: it starts the app on the first request (or immediately with “Always Running”), recycles on schedule or memory thresholds, and restarts on crashes.

How Self-Hosting Works

  • You create an HttpSelfHostConfiguration with your routes, then instantiate HttpSelfHostServer and call OpenAsync().

  • The server runs in whatever process you own: a console application, a Windows service, or a background task.

  • Under the hood it uses HttpListener, which listens on a base address such as http://localhost:8080/.

  • You are responsible for starting and stopping the server, handling exceptions, and keeping the process alive.

Key Differences

Aspect IIS Hosting Self-Hosting
Process IIS worker process (w3wp.exe) Your own process (console app, Windows service)
Lifetime management IIS (recycling, idle timeout, health monitoring) Your code (OpenAsync/CloseAsync)
Configuration web.config + WebApiConfig Code only (HttpSelfHostConfiguration)
Authentication Full IIS modules (Windows, certificate, digest) Only what you implement (e.g., basic over HTTPS)
Request filtering / security Built in (requestFiltering, IP restrictions) None; you must add them
Static files, compression, caching Built in Not available
Platform Windows with IIS Windows (HttpListener); cross-platform only via Mono
Deployment IIS site / virtual directory Copy executable + config
Startup trigger First request or app pool start Explicit OpenAsync() call

When to Use Each

  • Choose IIS hosting for production, internet-facing APIs: you get process recycling, rapid-fail protection, SSL termination, Windows authentication, logging, and integration with existing IIS infrastructure at no extra code.

  • Choose self-hosting for internal tools, prototypes, test harnesses, or scenarios where IIS is unavailable or undesirable (for example, a utility that must run without an IIS dependency, or side-by-side versions of Web API on one machine).

  • Self-hosting is also useful when you need the API to start before any HTTP request arrives, or when you want to host multiple independent Web API instances in one process.

Best Practices and Common Pitfalls

  • Do not use self-hosting for public-facing production APIs unless you replicate IIS features yourself (request filtering, DDoS protection, TLS termination, process health monitoring).

  • Run self-hosted endpoints over HTTPS; HttpListener supports HTTPS only with a certificate bound to the port via netsh.

  • Dispose the server properly: wrap HttpSelfHostServer in a using or call CloseAsync() on shutdown, otherwise ports stay bound.

  • Remember HttpListener requires administrator privileges to register non-localhost prefixes on Windows.

  • Do not mix the two models in one project: IIS hosting references Microsoft.AspNet.WebApi.WebHost; self-hosting references Microsoft.AspNet.WebApi.SelfHost, and their configuration types are not interchangeable.

  • Note: in ASP.NET Core this distinction disappears — Kestrel is the built-in cross-platform server and IIS acts as a reverse proxy or in-process host. This answer covers classic Web API 2 only.

12

What steps are needed to enable cross-origin requests in an ASP.NET Core Web API so browsers can call it from a different origin?

CORS (Cross-Origin Resource Sharing) is configured by registering a CORS policy in the dependency-injection container with AddCors, then activating it in the request pipeline with UseCors, either globally or per controller, action, or endpoint.

Read full answer

Applies toASP.NET Core Web API on .NET 8 (controller-based APIs using `[ApiController]`; the same packages and middleware apply to Minimal APIs).

What CORS Solves

Browsers block a script at https://app.example.com from calling an API at https://api.example.com unless the API explicitly allows it. CORS is the browser-enforced mechanism in which the server sends response headers such as Access-Control-Allow-Origin that grant that permission. It is browser-side protection: non-browser clients (curl, Postman, mobile apps) do not enforce it, so CORS is never a substitute for authentication or authorization.

Configuring a Policy

A policy is a named bundle of three permissions:

  • Allowed origins: the exact schemes, hosts and ports that may call the API, e.g. https://app.example.com. Use WithOrigins for an explicit allow-list.

  • Allowed headers: request headers the browser may send, set with AllowAnyHeader or WithHeaders.

  • Allowed methods: HTTP verbs permitted, set with AllowAnyMethod or WithMethods.

Call AllowCredentials() only when the API relies on cookies or bearer tokens sent by the browser. Note that the framework rejects combining AllowAnyOrigin() with AllowCredentials(), because that would let any site send authenticated requests; you must list explicit origins instead.

Applying the Policy

Order in Program.cs matters: place UseCors after UseRouting and before UseAuthentication/UseAuthorization, so the CORS check happens before authentication and endpoint execution. A default policy applied via UseCors() covers every endpoint; a named policy applied via UseCors("PolicyName") can be overridden per endpoint.

For controller-based APIs, add [EnableCors("PolicyName")] to a controller or action method to apply a specific policy, and [DisableCors] where a cross-origin call must be refused. In Minimal APIs the equivalent is RequireCors("PolicyName") on a route group or endpoint. Keep the two styles distinct: attribute-based selection belongs to controllers, RequireCors to Minimal API endpoint metadata.

Preflight Requests

For requests that are not “simple” (methods other than GET/POST/HEAD, custom headers, non-simple content types), the browser first sends an OPTIONS preflight request. ASP.NET Core’s CORS middleware intercepts it and returns the permission headers without executing the endpoint, provided the requested method, header and origin match the policy. A policy that omits a needed header or verb causes the real request to fail in the browser, even when the endpoint itself would accept it.

Code Example

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

// Register a named CORS policy with an explicit origin allow-list
builder.Services.AddCors(options =>
{
    options.AddPolicy("Frontend", policy =>
    {
        policy.WithOrigins("https://app.example.com", "https://admin.example.com")
              .AllowAnyHeader()
              .AllowAnyMethod()
              .AllowCredentials(); // allowed only with explicit origins
    });
});

var app = builder.Build();

app.UseRouting();
app.UseCors("Frontend");          // must run before auth and endpoint execution
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

app.Run();
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;

public record Product(int Id, string Name);

[ApiController]
[Route("api/[controller]")]
[EnableCors("Frontend")]         // applies the named policy to all actions here
public class ProductsController : ControllerBase
{
    [HttpGet("{id:int}")]
    public ActionResult<Product> Get(int id) => Ok(new Product(id, "Sample"));
}

Best Practices and Common Pitfalls

  • Prefer explicit origins: AllowAnyOrigin in production weakens the API; keep a short, configurable allow-list read from appsettings.json or configuration.

  • Never combine AllowAnyOrigin with AllowCredentials: it is rejected by design; use WithOrigins whenever cookies or browser-sent tokens are involved.

  • Check the middleware order: UseCors placed after UseAuthorization or before UseRouting produces broken or missing headers.

  • Do not treat CORS as a security control: it constrains browsers, not clients; protect endpoints with JWT bearer authentication or ASP.NET Core Identity.

  • Debug from the browser, not Postman: a 200 in Postman does not prove the CORS headers are present; inspect the preflight OPTIONS response and the Access-Control-Allow-Origin header in browser devtools.

13

Explain what attribute routing is and what benefits it brings to an ASP.NET Core Web API.

Attribute routing is a routing model in which each URI template is declared directly on a controller or action method using attributes such as [Route], [HttpGet] and [HttpPost], instead of being defined once in a global route table. It improves a Web API by giving precise, co-located control over the exact URLs each action serves, which makes RESTful, hierarchical and versioned resource URIs far easier to express and maintain.

Read full answer

Applies toASP.NET Core 8.0 Web API (attribute routing is supported in all ASP.NET Core versions; this answer targets .NET 8 with controller-based APIs).

How It Works

With attribute routing, the route template is attached to the controller class and/or its actions, and the framework matches an incoming request by combining the templates:

  • Controller-level template: [Route("api/[controller]")] applies to every action in the controller. The [controller] token is replaced at runtime with the controller name minus the Controller suffix, so ProductsController becomes api/products.

  • Action-level template: [HttpGet("{id:int}")] adds a parameter segment to the verb-specific template. Templates on actions are relative to the controller template.

  • HTTP verb attributes: [HttpGet], [HttpPost], [HttpPut], [HttpPatch] and [HttpDelete] both constrain the HTTP method and can carry a template, so a single action can be reached only through the intended verb.

  • Route values and constraints: {id} becomes a route value bound to an action parameter; :int, :min(1), :guid, :regex() and others restrict matches, so GET /api/products/abc simply returns 404 instead of reaching the action and failing.

Key Features

  • Multiple routes per action: one action can be decorated with several [HttpGet]/[Route] attributes, exposing the same logic under different URIs.

  • Token replacement: [controller] and [action] keep templates in sync with naming, and [area] supports area-based APIs.

  • Hierarchical, resource-oriented URIs: nested templates such as api/customers/{customerId}/orders/{orderId} map naturally to actions, which is awkward with a single conventional route.

  • Co-location: the route lives next to the code it dispatches to, so reading or changing a controller shows its contract immediately.

  • Works with API versioning: the Asp.Versioning.Mvc package composes version segments (for example [Route("api/v{version:apiVersion}/[controller]")]) with attribute templates. The older Microsoft.AspNetCore.Mvc.Versioning package is deprecated and replaced by Asp.Versioning.Mvc.

Attribute Routing vs. Conventional Routing

Aspect Attribute routing Conventional routing
Where defined On controllers and actions Once in Program.cs via MapControllerRoute
URL shape Explicit per action Inferred from a shared pattern
Best fit RESTful APIs, nested resources, versioned URIs MVC views, predictable CRUD conventions
Multiple URIs per action Directly supported Requires extra route entries
Readability Contract visible on the action Centralized, less discoverable

A controller-based API can mix both, but a single action uses one model: when [Route] or a verb attribute is present, that action is not reachable through conventional routes.

Benefits in Short

  • Precise, predictable URLs: the exact URI an action serves is visible on the action, which keeps the API contract honest and easy to review.

  • Clean REST semantics: verbs and templates live together, so GET /api/products/5, POST /api/products, PUT /api/products/5 and DELETE /api/products/5 all read as one resource model.

  • Better maintainability: adding or renaming an action does not require touching a central route table, and code reviews show routing changes in context.

  • First-class support for versioning and nested resources: hierarchical and versioned URIs are declared where they belong instead of being encoded in a global pattern.

Best Practices and Common Pitfalls

  • Prefer one base template on the controller plus verb-specific templates on actions; avoid repeating the full path on every action.

  • Use constraints (:int, :min(1)) so malformed segments never reach your code, and keep route parameter names identical to action parameter names for model binding.

  • Keep templates stable once clients depend on them; changing a published route is a breaking change, so version new shapes with Asp.Versioning.Mvc instead of editing existing templates.

  • Remember that [ApiController] makes attribute routing effectively the norm for APIs and turns invalid model state into an automatic 400 with Problem Details (application/problem+json, RFC 9457) — register it with AddProblemDetails() and return Problem()/ValidationProblem() for manual errors.

  • Do not rely on route order to disambiguate overlapping templates; two actions matching the same verb and URI cause an AmbiguousMatchException at request time.

15

Can you explain what IHttpActionResult represents in ASP.NET Web API and how the framework uses it to produce action results?

IHttpActionResult is an interface in classic ASP.NET Web API 2 that represents the outcome of a controller action, encapsulating the logic needed to build the final HttpResponseMessage. Instead of constructing response messages directly, an action returns an IHttpActionResult, and the Web API pipeline calls its ExecuteAsync method to generate the HTTP response. This makes actions cleaner and far easier to unit test. Note that in ASP.NET Core this interface no longer exists; it was replaced by IActionResult and ActionResult<T>.

Read full answer

Applies toASP.NET Web API 2 on .NET Framework 4.8

What It Is

  • IHttpActionResult lives in the System.Web.Http namespace and defines a single method: Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken).

  • Controller classes inherit from ApiController, whose helper methods (Ok, NotFound, BadRequest, Created, Content, and so on) return ready-made IHttpActionResult implementations.

  • The framework invokes ExecuteAsync after the action completes, turning the result into the HttpResponseMessage that is serialized and sent to the client.

How the Pipeline Uses It

  1. The action method executes and returns an IHttpActionResult instance.

  2. Web API’s ApiControllerActionInvoker receives that result.

  3. It calls ExecuteAsync, which constructs status code, headers, and content.

  4. The resulting HttpResponseMessage passes through message handlers and formatters before reaching the client.

Because the response-building logic is separated from the action decision, you can assert on the result type in tests without needing an HTTP server or mocking HttpResponseMessage.

Common Result Types

Helper method Status code Typical use
Ok(value) 200 Successful GET or PUT returning a body
Ok() 200 Successful request with no body
Created(uri, value) 201 POST created a resource, with Location header
NotFound() 404 Requested resource does not exist
BadRequest() / BadRequest(ModelState) 400 Invalid request payload or model state
Unauthorized() 401 Caller is not authenticated
StatusCode(HttpStatusCode.NoContent) 204 Success with no body (common for DELETE)
InternalServerError(exception) 500 Unexpected server failure

Custom Results

  • You can implement IHttpActionResult directly to encapsulate repeated response logic, such as a standardized error format or a special header.

  • A custom implementation receives the controller context in ExecuteAsync, so it can use Request, Configuration, or Url to build links and content.

  • Prefer returning existing helper methods when they cover the scenario; reserve custom implementations for behavior the built-in helpers cannot express.

Best Practices and Pitfalls

  • Prefer helper methods like Ok and NotFound over manually constructing HttpResponseMessage; they are shorter and express intent.

  • Return Task<IHttpActionResult> for async actions and await all I/O; never block on async code.

  • Use Created for POST so clients receive the Location header pointing to the new resource; use 204 for DELETE.

  • Validate ModelState explicitly in Web API 2; unlike ASP.NET Core’s [ApiController], Web API 2 does not return 400 automatically.

  • Do not carry this pattern into ASP.NET Core: on .NET 8 use ActionResult<T> or IActionResult with [ApiController], and return Problem Details via Problem() for errors. IHttpActionResult is specific to the legacy .NET Framework pipeline.

16

What are the standard approaches for accessing query string parameters within an ASP.NET Core Web API endpoint?

In ASP.NET Core, query string values are primarily retrieved through model binding, where action method parameters are automatically populated from the request URL. You can also access the raw collection directly via the HttpContext when dynamic keys are needed. This mechanism works consistently across controller-based endpoints and Minimal APIs.

Read full answer

Applies toASP.NET Core Web API on .NET 8

Model Binding Mechanism

When a GET request arrives, the framework inspects the route and query string to match parameter names. - Primitive Types: Simple types like int, string, or Guid are bound automatically if names match. - Complex Types: Classes default to binding from the request body; add [FromQuery] to bind their properties from the query string. - Nullable Types: Use int? or string to handle missing values gracefully without throwing exceptions. - Explicit Binding: The [FromQuery] attribute forces the binder to look only at the query string, avoiding ambiguity with route data.

Accessing Request.Query Directly

Sometimes parameter names are dynamic or unknown at compile time. In such cases, inspect the IQueryCollection. - Controller Context: Inside a controller, use Request.Query["key"] which returns a StringValues object. - Minimal API Context: Within a map delegate, access HttpContext.Request.Query directly from the context parameter. - StringValues: This structure handles multiple values for a single key (e.g., ?ids=1&ids=2) without requiring arrays explicitly.

Controller vs Minimal API Syntax

The approach differs slightly depending on whether you use traditional controllers or Minimal APIs. - Controllers: Parameters are defined in the method signature of a ControllerBase derived class. - Minimal APIs: Parameters are captured in the lambda arguments of MapGet, MapPost, etc. - TypedResults: Minimal APIs can return Results.Ok() with metadata for OpenAPI documentation.

Validation and Security Considerations

Query strings are user input and must be treated as untrusted data. - Problem Details: Configure AddProblemDetails() in Program.cs to ensure all errors follow RFC 9457 format. - Model Validation: With [ApiController], invalid model state automatically returns a 400 Problem Details response. - Sensitive Data: Never pass secrets, tokens, or PII in query strings as they are logged in server logs and browser history.

Code Example

// Controller-based approach
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet("search")]
    public IActionResult Search(
        [FromQuery] string? category,
        [FromQuery] int? page = 1,
        [FromQuery] int? pageSize = 10)
    {
        if (pageSize > 100)
            return Problem("PageSize exceeds maximum", statusCode: 400);

        return Ok(new { Category = category, Page = page });
    }
}

// Minimal API approach
app.MapGet("/products/search", (string? category, int? page = 1) =>
{
    return Results.Ok(new { Category = category, Page = page });
});

Common Pitfalls

  • Missing Values: Non-nullable value types (e.g., int) cause a 400 if the query parameter is missing; use nullable types instead.

  • Binding Sources: Without [FromQuery], simple types on a GET are bound from route data and query string; a name matching both causes an ambiguity error.

  • Collection Binding: To bind lists, use repeated keys (?id=1&id=2) or comma-separated values depending on binder configuration.

17

Describe how the request and response message classes are used when building an HTTP API.

In ASP.NET Web API 2, the two message classes are HttpRequestMessage and HttpResponseMessage, both from System.Net.Http. They give you a single, strongly typed object model for everything an HTTP exchange contains: method, URI, headers, body, and status, and they are the foundation on which controllers, message handlers, and filters operate.

Read full answer

Applies toASP.NET Web API 2 on .NET Framework 4.8. (In ASP.NET Core Web API on .NET 8 the equivalents are `HttpRequest`/`HttpResponse` accessed through `ControllerBase.HttpContext`, with `ActionResult<T>` as the preferred return type — the two stacks are distinct and should not be mixed.)

What the Classes Represent

  • HttpRequestMessage: represents the incoming request. Its key members are Method (HttpMethod), RequestUri, Headers (HttpRequestHeaders), Content (HttpContent), and Properties, a per-request dictionary used to share state between handlers, filters and controllers.

  • HttpResponseMessage: represents the outgoing response. Its key members are StatusCode (HttpStatusCode), ReasonPhrase, Headers (HttpResponseHeaders), Content (HttpContent), IsSuccessStatusCode, and RequestMessage, which links the response back to the request that produced it.

  • Both are immutable in intent but mutable in practice: you construct a response, set its parts, and hand it back; the framework never reuses a request or response object across calls.

Reading Request Data

Extension methods in System.Net.Http.Formatting make the request easy to interrogate without parsing raw bytes:

  • Request.GetQueryNameValuePairs(): parsed query string as key/value pairs.

  • Request.GetRouteData(): the matched route values, useful in handlers where no controller context exists.

  • Request.Headers.GetValues(...) / Request.Headers.TryGetValues(...): safe header lookup, since duplicate or missing headers otherwise throw.

  • Request.Content.ReadAsAsync<T>(): deserialises the body via the configured formatters; in a controller you normally let model binding do this through an action parameter instead.

  • Request.Properties: the right place to stash request-scoped data (correlation id, resolved tenant) so handlers and filters can read it later.

Building Responses

Responses are usually created with the Request.CreateResponse(...) extension methods, which run content negotiation against the request’s Accept header and pick a MediaTypeFormatter:

Approach When to use
Request.CreateResponse(status, value) Default choice: negotiated, formatter-serialised body.
new HttpResponseMessage(status) { Content = ... } Full manual control, e.g. StreamContent or StringContent with an explicit media type.
Request.CreateErrorResponse(status, message) / CreateErrorResponse(status, ModelState) Consistent error bodies from validation failures.
IHttpActionResult (e.g. Ok(...), NotFound(), BadRequest(...)) Web API 2’s simpler, more testable wrapper; use it unless you need direct header/status control.

Because CreateResponse uses the request’s Accept header, the same action can return JSON or XML without action-level branching.

Code Example

using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public interface IProductRepository
{
    Product GetById(int id);
    Task AddAsync(Product product);
}

public class ProductsController : ApiController
{
    private readonly IProductRepository _repository;

    public ProductsController(IProductRepository repository)
    {
        _repository = repository;
    }

    // GET api/products/5
    public HttpResponseMessage Get(int id)
    {
        var product = _repository.GetById(id);

        if (product == null)
        {
            // 404 with a negotiated body
            return Request.CreateResponse(
                HttpStatusCode.NotFound, "Product not found");
        }

        // 200 OK, body serialised by content negotiation
        var response = Request.CreateResponse(HttpStatusCode.OK, product);

        // Full control over response headers when you need it
        response.Headers.CacheControl =
            new System.Net.Http.Headers.CacheControlHeaderValue
            {
                Public = true,
                MaxAge = TimeSpan.FromMinutes(5)
            };

        return response;
    }

    // POST api/products
    public async Task<HttpResponseMessage> Post([FromBody] Product product)
    {
        if (!ModelState.IsValid)
        {
            // 400 with validation details
            return Request.CreateErrorResponse(
                HttpStatusCode.BadRequest, ModelState);
        }

        await _repository.AddAsync(product);

        // 201 Created with a Location header pointing at the new resource
        var created = Request.CreateResponse(HttpStatusCode.Created, product);
        created.Headers.Location = new Uri(Url.Link("DefaultApi",
            new { id = product.Id }));

        return created;
    }
}

Best Practices and Pitfalls

  • Prefer IHttpActionResult for ordinary actions and reach for HttpResponseMessage only when you must set headers, stream content, or return a status the helpers do not cover.

  • Always set Location on 201 Created so clients can navigate to the created resource; Url.Link keeps the route template in one place.

  • Do not read the request body twice: ReadAsAsync consumes the stream; in controllers, use model binding instead of manual reads.

  • Do not share HttpRequestMessage across requests: its Properties bag and disposal semantics are per-request; reuse causes stale state and ObjectDisposedExceptions.

  • Return Task<HttpResponseMessage> for async work and avoid .Result/.Wait(), which can deadlock in the request synchronisation context.

  • Keep handlers stateless: put per-request state in Request.Properties rather than static fields.

18

What mechanisms allow you to mandate HTTPS encryption for specific endpoints within an ASP.NET Core Web API application?

In ASP.NET Core Web API you can mandate TLS on specific endpoints by decorating individual actions or controllers with the [RequireHttps] attribute (or registering that attribute as a global filter), while UseHttpsRedirection and UseHsts enforce HTTPS across the whole application. Per-endpoint attributes are the mechanism to use when only some endpoints must be encrypted.

Read full answer

Applies toASP.NET Core Web API on .NET 8

Attribute-Level Enforcement

  • [RequireHttps] (Microsoft.AspNetCore.Mvc) is an authorization filter. For safe verbs (GET/HEAD) it short-circuits the request with a RedirectResult, returning 307 Temporary Redirect to the HTTPS scheme; for other verbs (POST, PUT, PATCH, DELETE) it throws RequireHttpsException, which the framework surfaces as 400 Bad Request — non-safe verbs are never redirected.

  • Scope: apply it to a single action, to a whole controller class, or register it once as a global filter with services.AddControllers(o => o.Filters.Add(new RequireHttpsAttribute())) when you want attribute semantics everywhere.

  • Minimal APIs: [RequireHttps] is an MVC filter, so it suits controller-based APIs; for Minimal APIs rely on redirection middleware plus authorization rather than this attribute.

Global Middleware Enforcement

  • UseHttpsRedirection() redirects every HTTP request to HTTPS regardless of verb. It is configured through HttpsRedirectionOptions: RedirectStatusCode (default 307) and HttpsPort (default unset, in which case the port is discovered from the server’s bound HTTPS addresses).

  • Ordering: call app.UseHsts() before app.UseHttpsRedirection(), and place both early in the pipeline — before static files and routing — so requests are upgraded before endpoint execution.

  • Combining the two: middleware gives a global baseline, and [RequireHttps] is still useful when a specific endpoint needs a stricter guarantee than the rest of the API.

HSTS

  • Strict-Transport-Security is a browser-side instruction, not server-side enforcement: browsers refuse plain-HTTP requests to the host for the duration of max-age, which mitigates SSL-stripping attacks. Non-browser clients (curl, Postman, most mobile HTTP stacks) ignore the header, so HSTS complements redirection and attribute checks but never replaces them.

  • Defaults: HstsOptions uses a one-year MaxAge, IncludeSubDomains = false, Preload = false, and ExcludedHosts containing localhost, so localhost responses do not carry the header during development.

Code Example

// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpsRedirection(options =>
{
    options.RedirectStatusCode = StatusCodes.Status307TemporaryRedirect;
    options.HttpsPort = 443; // set explicitly in production; in dev use the launch port (e.g. 7001)
});
builder.Services.AddControllers();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseHsts(); // skipped in dev to avoid browser-cached HSTS on localhost
}

app.UseHttpsRedirection();
app.UseAuthentication(); // must come before UseAuthorization
app.UseAuthorization();
app.MapControllers();
app.Run();
// Per-endpoint enforcement
[ApiController]
[Route("api/[controller]")]
public class SensitiveController : ControllerBase
{
    // Only this action mandates HTTPS; other actions on the controller do not
    [HttpGet("data")]
    [RequireHttps]
    public IActionResult GetData() => Ok(new { Value = "secure" });
}

Best Practices and Common Pitfalls

  • Dev certificates: run dotnet dev-certs https --trust so local HTTPS redirects are not blocked by certificate errors.

  • Proxies and load balancers: enable UseForwardedHeaders so the app sees the original scheme; otherwise HTTP-to-HTTPS redirect loops occur behind a TLS-terminating proxy.

  • Choose the right tool: middleware for a global policy, a global [RequireHttps] filter for attribute-style enforcement everywhere, and per-endpoint [RequireHttps] only when individual endpoints must differ.

  • Testing: verify GET over HTTP returns 307 to https, and that POST over HTTP to a [RequireHttps] action returns 400 rather than being silently redirected.

  • HSTS is sticky: once a browser caches the header for max-age, reverting to HTTP is painful for that host, which is why it stays disabled in development.

19

How does model binding operate within ASP.NET Web API, and what distinctions exist between it and the model binding process in ASP.NET MVC?

Model binding is the framework mechanism that maps HTTP request data to action method parameters or model properties. In ASP.NET Core Web API on .NET 8, the default binding source for complex types is the request body, and the same default applies to ASP.NET Core MVC; the practical distinction comes from the [ApiController] attribute, which enables automatic model-state validation and source inference, whereas classic MVC actions typically rely on manual checks and form/route data.

Read full answer

Applies toASP.NET Core Web API and ASP.NET Core MVC on .NET 8

Core Mechanism

Model binding extracts data from route values, query strings, headers, and the request body. The system uses value providers to locate data and model binders to convert it into .NET types. In Web API, content negotiation determines the formatter used for body binding, typically JSON via System.Text.Json. If conversion fails, the model state becomes invalid, triggering error handling pipelines.

Web API vs MVC Differences

In ASP.NET Core, MVC and Web API share the same underlying model-binding infrastructure. The differences are driven by conventions and the [ApiController] attribute rather than separate engines.

Feature ASP.NET Core MVC (without [ApiController]) ASP.NET Core Web API ([ApiController])
Default binding source for complex types Body (but value providers also check route, query, and form) Body (inferred by attribute)
Model-state validation Manual ModelState.IsValid check required Automatic HTTP 400 with Problem Details
Typical response Views or JSON JSON/XML; Problem Details on error
Attribute convention [Controller] marker optional [ApiController] enables inference and automatic 400

Binding Source Attributes

Developers can override defaults using source-specific attributes. Use [FromBody] to force body binding for simple types or multiple parameters. [FromQuery] binds from the URL query string, and [FromRoute] uses route template values. For Minimal APIs, parameters are inferred automatically unless specified otherwise using explicit attributes or BindAsync.

Validation and Problem Details

When [ApiController] is applied, invalid model state triggers an automatic HTTP 400 response containing Problem Details (RFC 9457). This replaces manual ModelState.IsValid checks. The response format is application/problem+json, including type, title, status, and errors fields. Developers can customize this via AddProblemDetails() in Program.cs.

Common Pitfalls

  • Assuming [FromBody] is always required for complex types: with [ApiController] it is inferred automatically.

  • Forgetting that [ApiController] also enables attribute routing and automatic 400, so applying it to a view controller can break expected behavior.

  • Binding directly to domain entities, which can lead to overposting and mass-assignment vulnerabilities.

Best Practices

  • Use DTOs: Bind to dedicated Data Transfer Objects rather than domain entities to prevent overposting.

  • Explicit Sources: Mark parameters with [FromQuery] or [FromBody] explicitly for clarity, even if inference works.

  • Minimal APIs: Consider Minimal APIs for lightweight endpoints, but use controllers for complex organization.

  • Security: Never bind sensitive fields directly; use allowlists or view models.

  • Versioning: For API versioning, use the Asp.Versioning.Mvc package; the older Microsoft.AspNetCore.Mvc.Versioning is deprecated.

20

How does ASP.NET Core Web API handle model binding for complex types passed to controller actions?

In ASP.NET Core Web API, model binding automatically maps incoming HTTP request data to complex type parameters in action methods, primarily reading JSON payloads from the request body when the [ApiController] attribute is applied. This process eliminates manual parsing and integrates with validation pipelines to return standardized error responses.

Read full answer

Applies toASP.NET Core Web API on .NET 8

How Model Binding Works

When a request reaches a controller action, the model binding system inspects the action parameters. For complex types (classes with multiple properties), the default binding source is the request body. The framework uses an input formatter, typically SystemTextJsonInputFormatter in .NET 8, to deserialize the JSON payload into the target object. If the parameter is a simple type (like int or string), the system checks route data and query strings instead.

The request body stream can be read only once, so at most one parameter per action may be bound from the body. Any additional complex parameters must come from other sources such as the query string or route values.

Explicit Binding Sources

While complex types default to [FromBody], you can explicitly specify sources using attributes to avoid ambiguity or combine data from multiple locations. - [FromBody]: Reads from the request body (default for complex types in API controllers). Only one parameter per action may use it. - [FromQuery]: Parses key-value pairs from the URL query string. - [FromRoute]: Extracts values from the route template (e.g., {id}). - [FromForm]: Reads multipart form data, useful for file uploads.

Automatic Validation and Problem Details

With [ApiController], the framework automatically returns a 400 Bad Request response if model validation fails, without requiring manual ModelState.IsValid checks. This response follows RFC 9457 Problem Details format when configured. To enable this globally, register the service in Program.cs using builder.Services.AddProblemDetails(). Controllers can also return custom problem responses using the Problem() helper method, ensuring consistent error serialization across the API.

Note that a null complex-type parameter is not automatically rejected unless validation attributes such as [Required] are applied to the DTO.

Minimal APIs Alternative

In .NET 8, Minimal APIs offer a lightweight alternative where binding occurs directly on the MapPost delegate. Parameters are bound using the same underlying engine, allowing complex types to be declared directly in the lambda signature. TypedResults can be used to maintain explicit HTTP status codes and OpenAPI metadata similar to controller-based approaches.

Best Practices

  • Use DTOs: Bind to Data Transfer Objects rather than database entities to prevent overposting and decouple schemas.

  • Validate Explicitly: While automatic validation helps, use data annotations like [Required] or [StringLength] on DTO properties.

  • Secure Deserialization: Configure JsonOptions to limit deep nesting or excessive payload sizes, which could otherwise lead to denial of service.

  • Avoid [FromBody] on GET: GET requests should not have bodies; bind simple types from route or query instead.

21

What approaches are available for customizing how objects are serialized to and from JSON in an ASP.NET Core Web API?

In ASP.NET Core Web API you customize JSON serialization by configuring System.Text.Json options globally, applying serialization attributes to models, writing custom converters, and—when using Minimal APIs—using the dedicated HTTP JSON options configuration. These approaches let you control naming, null handling, reference preservation, and format-specific parsing without leaving the framework.

Read full answer

Applies toASP.NET Core Web API on .NET 8

Global Configuration

The most common way to change serialization behavior for every controller is to configure JsonSerializerOptions once at startup. In an MVC-style API you call AddJsonOptions on the MVC builder; the same options object is used for both request and response bodies.

  • Naming policy: PropertyNamingPolicy = JsonNamingPolicy.CamelCase writes property names as camelCase, matching typical JavaScript conventions.

  • Null handling: DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull omits null-valued properties from responses, reducing payload size.

  • Reference preservation: ReferenceHandler = ReferenceHandler.Preserve emits $id/$ref metadata so object graphs round-trip correctly, but it changes the JSON shape and should be used only when clients expect it.

  • Strictness: AllowTrailingCommas = true and ReadCommentHandling = JsonCommentHandling.Skip make the parser tolerant of commas and comments in incoming JSON.

  • Security: MaxDepth limits nesting depth and NumberHandling controls how numbers are parsed, helping to mitigate denial-of-service attacks.

For Minimal APIs there is no MVC builder, so you use ConfigureHttpJsonOptions instead; it configures the same JsonSerializerOptions for the minimal API pipeline.

Per-Type Attributes

When only a few models need special treatment, attributes from System.Text.Json.Serialization avoid global side effects.

  • [JsonPropertyName]: renames a single property in the JSON payload, useful when the server name differs from the contract name.

  • [JsonIgnore]: excludes a property from serialization entirely, often used for internal fields or write-only values.

  • [JsonConverter]: attaches a custom converter to one property or class, overriding the global converter list.

  • [JsonExtensionData]: captures unknown JSON fields in a dictionary, enabling forward-compatible APIs that ignore extra fields sent by newer clients.

  • [JsonNumberHandling]: controls number parsing and writing per property, for example allowing strings to be read as numbers.

Attributes are ideal for model-specific rules, but they scatter configuration across the domain, so keep them limited to DTOs.

Custom Converters

For formats the built-in serializer does not support—such as a non-standard date string, an immutable type, or a polymorphic discriminator—you implement JsonConverter<T>. It gives full control over Read and Write, letting you parse and format values exactly as required. Converters can be registered globally (so they apply everywhere) or attached to a single type with [JsonConverter]. A typical use case is a DateOnly value that must always appear as yyyy-MM-dd regardless of culture.

Minimal APIs vs Controller-Based APIs

Both styles ultimately configure System.Text.Json, but the entry point differs:

Approach Configuration method Scope
Controller-based (AddControllers) AddJsonOptions All MVC controllers and Razor Pages
Minimal APIs (MapGet, MapPost, etc.) ConfigureHttpJsonOptions Minimal API endpoints only

If you mix both in the same app, configure each separately; they do not share options automatically.

Best Practices and Pitfalls

  • Prefer global options for cross-cutting concerns such as naming policy and null handling; reserve attributes for model-specific exceptions.

  • Keep DTOs separate from domain entities so serialization attributes do not leak into business logic.

  • Avoid ReferenceHandler.Preserve in public APIs unless clients understand the $id/$ref metadata; it changes the JSON shape and can break existing consumers.

  • Newtonsoft.Json is not deprecated, but it is no longer the default. Use AddNewtonsoftJson() only when migrating legacy contracts or when a feature is missing from System.Text.Json, and plan to move to the built-in serializer.

  • Validate payload size and depth with MaxDepth and request-body size limits to prevent denial-of-service from deeply nested or oversized JSON.

  • Test round-trips in unit tests: serialize and deserialize your DTOs to ensure renaming or ignoring properties does not silently break clients.

  • Document breaking changes when you change naming policy or null handling, because clients may rely on the previous shape.

22

Can you explain what media type formatters do in ASP.NET Web API 2?

A media type formatter is a class that serializes CLR objects into an HTTP response body and deserializes an HTTP request body back into CLR objects, choosing the wire format (JSON, XML, form-encoded data) from the request’s Accept and Content-Type headers. They are the engine behind content negotiation in ASP.NET Web API 2.

Read full answer

Applies toASP.NET Web API 2 on .NET Framework 4.8 (the `MediaTypeFormatter` type does not exist in ASP.NET Core, which uses input/output formatters instead).

How It Works

When a request arrives, the framework runs content negotiation against the Accept header (and, for request bodies, the Content-Type header) and asks each formatter, in order, whether it can handle the CLR type in the requested media type.

  1. The controller action returns a CLR object (or IHttpActionResult).

  2. IContentNegotiator walks config.Formatters and calls CanWriteType / CanReadType on each formatter.

  3. The first formatter whose SupportedMediaTypes matches the requested media type and that can handle the type wins.

  4. It serializes the object to the response stream (or deserializes the request body into the action parameter).

  5. If no formatter matches, Web API returns 406 Not Acceptable for responses (or 415 Unsupported Media Type for request bodies it cannot parse).

Key members of the abstract MediaTypeFormatter base class (in System.Net.Http.Formatting):

  • SupportedMediaTypes: collection of MediaTypeHeaderValue objects such as application/json; drives media-type matching.

  • SupportedEncodings: encodings (UTF-8, etc.) the formatter can read and write.

  • CanReadType(Type) / CanWriteType(Type): queried during negotiation to check whether the formatter handles a given CLR type.

  • ReadFromStreamAsync(...): deserializes a request body into an object.

  • WriteToStreamAsync(...): serializes an object into the response stream.

  • MediaTypeMappings: lets a formatter claim requests based on query-string parameters or URI extensions rather than headers.

Built-in Formatters

The default HttpConfiguration.Formatters collection contains:

Formatter Media type Notes
JsonMediaTypeFormatter application/json Default and first in the list; JSON.NET-based
XmlMediaTypeFormatter application/xml Data Contract or XmlSerializer based
FormUrlEncodedMediaTypeFormatter application/x-www-form-urlencoded Reads HTML form posts into FormDataCollection
JQueryMvcFormUrlEncodedFormatter application/x-www-form-urlencoded Binds form posts directly to model objects

Order matters: because JSON is first, a request with Accept: application/json, application/xml gets JSON.

Customizing the Formatter Collection

Common configuration in WebApiConfig:

  • JSON-only API: remove XML with config.Formatters.Remove(config.Formatters.XmlFormatter).

  • Pretty-printed JSON: config.Formatters.JsonFormatter.Indent = true; (useful for debugging, not production).

  • Add a custom format: derive a class from MediaTypeFormatter, override CanWriteType and WriteToStreamAsync, then add it with config.Formatters.Add(...). A client sending Accept: text/csv can then receive CSV for the types your formatter supports, while other accepted types fall back to JSON or XML.

  • Browser requests: browsers send Accept: text/html, /, which no default formatter matches; clients get JSON anyway because of the / wildcard, but strict Accept headers can produce a 406.

Best Practices and Common Pitfalls

  • Order the collection deliberately: the first matching formatter wins, so add specialized formatters before the generic JSON/XML ones if they should take precedence for overlapping media types.

  • Be strict in CanWriteType: returning true for every type can break JSON responses for unrelated controllers.

  • Flush and dispose writers correctly: leave the underlying response stream open (the framework owns it) and flush your writer before returning.

  • Do not read the request stream twice: a formatter consumes the request body, so a second read in an action filter or model binder will fail.

  • Prefer JSON for public APIs: it is the default, is the smallest payload, and is what most clients expect; remove the XML formatter if your API never needs it.

23

How do you configure ASP.NET Core Web API to serialize responses into XML, JSON, or alternative media types depending on the client's request?

ASP.NET Core selects a response serializer through content negotiation, choosing the output formatter that matches the client’s Accept header. Controllers default to JSON (System.Text.Json), so XML or custom formats must be registered explicitly in the service collection.

Read full answer

Applies toASP.NET Core Web API on .NET 8

How Content Negotiation Works

When a request arrives, the framework reads the Accept header (honoring quality values such as application/xml;q=0.9) and asks each registered output formatter, in order, whether it can write the returned object for one of the requested media types.

  • Match found: the first capable formatter writes the response and sets the matching Content-Type.

  • No match: by default MvcOptions.ReturnHttpNotAcceptable is false, so the framework falls back to the first formatter that can serialize the object (normally JSON). Set it to true to return 406 Not Acceptable instead.

  • No Accept header or Accept: /: the default formatter (JSON) is used.

Built-in Formatter Options

  • JSON: configured with AddJsonOptions on AddControllers() for controller-based APIs, or ConfigureHttpJsonOptions for Minimal APIs.

  • XML: added with AddXmlSerializerFormatters (uses XmlSerializer) or AddXmlDataContractSerializerFormatters (uses DataContractSerializer). Both live in the shared framework, so no extra NuGet package is required.

  • Minimal APIs: only JSON is negotiated automatically; other formats are produced by returning a custom IResult (for example Results.Text(...) or a hand-written IResult that sets its own media type), which keeps the two programming models distinct.

Adding an Alternative Media Type

For a format that is not built in, derive a class from TextOutputFormatter (text-based formats) or OutputFormatter, add entries to SupportedMediaTypes and SupportedEncodings, override WriteResponseBodyAsync, and register an instance in options.OutputFormatters inside AddControllers(...). Implement the matching TextInputFormatter only if the API must also read that format from request bodies.

Code Example

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers(options =>
    {
        // options.ReturnHttpNotAcceptable = true; // 406 when no formatter matches
        options.OutputFormatters.Add(new CsvOutputFormatter()); // custom media type
    })
    .AddJsonOptions(options =>
    {
        options.JsonSerializer.PropertyNamingPolicy = null; // PascalCase output
    })
    .AddXmlSerializerFormatters(); // registers application/xml formatters

builder.Services.AddSwaggerGen();

var app = builder.Build();

app.UseSwagger();
app.UseSwaggerUI();
app.MapControllers();
app.Run();
[ApiController]
[Route("[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet("{id}")]
    [Produces("application/json", "application/xml", "text/csv")]
    public ActionResult<Product> Get(int id)
    {
        return Ok(new Product { Id = id, Name = "Sample" });
    }
}
public class CsvOutputFormatter : TextOutputFormatter
{
    public CsvOutputFormatter()
    {
        SupportedMediaTypes.Add("text/csv");
        SupportedEncodings.Add(Encoding.UTF8);
        SupportedEncodings.Add(Encoding.Unicode);
    }

    protected override bool CanWriteType(Type? type) =>
        typeof(Product).IsAssignableFrom(type) || base.CanWriteType(type);

    public override async Task WriteResponseBodyAsync(
        OutputFormatterWriteContext context, Encoding selectedEncoding)
    {
        var product = (Product)context.Object!;
        await context.HttpContext.Response.WriteAsync(
            $"Id,Name{Environment.NewLine}{product.Id},{product.Name}",
            selectedEncoding);
    }
}

Best Practices and Security

  • Prefer JSON for public APIs: smaller payloads and the widest client support across web and mobile platforms.

  • Restrict formats per action with [Produces] and document them in OpenAPI; do not enable every format globally.

  • XML is safe from XXE by default in .NET 8 because both built-in XML serializers disable DTD processing and external entity resolution; the real risks are oversized or deeply nested payloads, so keep request size limits and reader quotas configured.

  • Validate and encode all input before serialization, and never echo unvalidated client data into a custom format such as CSV without escaping delimiters and quotes.

  • Classic ASP.NET Web API 2 on .NET Framework 4.8 used global MediaTypeFormatter classes registered in WebApiConfig; ASP.NET Core replaced this with formatters registered per service collection, and the two must not be mixed.

24

Can you explain what DelegatingHandlers are in an ASP.NET Web API application and how they fit into the request-processing pipeline?

A DelegatingHandler is an HttpMessageHandler that wraps another handler (its InnerHandler) and participates in a chain, so each handler can inspect or modify an outgoing request, pass it down the chain, and then inspect or modify the response that comes back. In ASP.NET Core 8 Web API this pattern is used for outbound HTTP calls made through IHttpClientFactory; the server-side inbound pipeline uses middleware instead.

Read full answer

Applies toASP.NET Core 8 Web API (`System.Net.Http`, `Microsoft.Extensions.Http`), with notes on classic ASP.NET Web API 2 on .NET Framework 4.8.

How It Works

DelegatingHandler derives from HttpMessageHandler and adds the InnerHandler property. Handlers are linked in a chain: your handler’s SendAsync runs, calls base.SendAsync(request, cancellationToken) to forward the request to the inner handler, and processes the returned HttpResponseMessage. The innermost handler is HttpClientHandler, which performs the actual network call.

  • Single responsibility per handler: each handler does one cross-cutting job (correlation IDs, authentication tokens, retries, logging, compression) and stays independent of controllers and services.

  • Ordering matters: handlers execute in registration order on the way out and in reverse order on the way back, so a logging handler registered first wraps everything else.

  • Lifetime: handlers added via AddHttpMessageHandler<THandler>() are resolved from dependency injection per HttpClient instance, so they must be stateless or hold only scoped/transient dependencies.

Where They Fit in the Pipeline

Aspect ASP.NET Core 8 (current) Classic Web API 2 (.NET Framework 4.8)
Direction Outbound requests from your API to other services Inbound pipeline before controllers, and outbound
Registration AddHttpClient(...).AddHttpMessageHandler<T>() config.MessageHandlers.Add(new MyHandler()) in WebApiConfig
Server-side alternative Middleware (app.UseXxx) Message handlers are the pipeline; HttpServer is itself a DelegatingHandler
Typical use Correlation IDs, bearer tokens, retries, telemetry Auth, logging, CORS, custom response rewriting

Note the asymmetry: in classic Web API 2, DelegatingHandler was the standard way to write inbound cross-cutting concerns; in ASP.NET Core that role belongs to middleware, and DelegatingHandler is the tool for HttpClient calls.

Lifecycle, Ordering, and State

  • Chain integrity: every handler must call base.SendAsync unless it deliberately short-circuits (for example, returning a cached response); skipping it breaks the chain and no request is ever sent.

  • Per-request state: because handler instances are shared across requests, per-request state must live in HttpRequestMessage.Options (the Properties collection is obsolete in .NET 5+), using an HttpRequestOptionsKey<T>.

  • Registration order: register logging first so it measures the full chain, and place retry handlers outside auth-token handlers so a retried request re-evaluates its token.

Best Practices and Common Pitfalls

  • Prefer IHttpClientFactory registration over constructing HttpClient or handlers manually, which can lead to socket exhaustion and missed handler lifetime management.

  • Do not put business logic in handlers: they are for transport-level concerns; validation and authorization of your own endpoints belong in middleware, filters, or endpoint handlers.

  • Keep handlers stateless: they are shared across requests for the lifetime of a handler instance.

  • Use async all the way: SendAsync is asynchronous; never block on it with .Result or .Wait().

25

How do you create and apply custom action filters in an ASP.NET Core Web API?

An action filter is a cross-cutting filter that runs immediately before and after a controller action method executes, letting you inspect or modify the arguments, short-circuit the request, or wrap the result. In ASP.NET Core you implement it with the IActionFilter or IAsyncActionFilter interface, or more commonly by deriving from the built-in ActionFilterAttribute class, and you register it globally, at the controller level, or on a single action.

Read full answer

Applies toASP.NET Core Web API on .NET 8 (classic ASP.NET Web API 2 on .NET Framework 4.8 has the equivalent `System.Web.Http.Filters.ActionFilterAttribute`, but the code below is not interchangeable between the two).

How the Filter Pipeline Works

ASP.NET Core runs filters in a fixed order for every request:

  1. Authorization filters — determine whether the caller may proceed (IAuthorizationFilter).

  2. Resource filters — wrap the whole model-binding and action stage (IResourceFilter), useful for caching and short-circuiting.

  3. Model binding and action filters — your OnActionExecuting runs, then the action, then OnActionExecuted.

  4. Result filters — wrap execution of the returned IActionResult (IResultFilter).

  5. Exception filters — handle unhandled exceptions (IExceptionFilter).

Action filters specifically target the action stage; use exception filters for error handling and result filters when you need to modify the serialized response.

Implementing an Action Filter

Deriving from ActionFilterAttribute is the simplest approach because it gives you both hooks as virtual methods:

  • OnActionExecuting(ActionExecutingContext context): runs before the action. You can read context.ActionArguments, validate input, modify the arguments dictionary, or short-circuit by assigning context.Result (for example new BadRequestObjectResult(...)), which stops the action from running.

  • OnActionExecuted(ActionExecutedContext context): runs after the action. You can inspect context.Result, replace it, or read context.Exception (already handled by an exception filter, so prefer IExceptionFilter for errors).

  • Order: set the Order property to control execution sequence when several filters apply; lower numbers run first.

  • Multiple applications: whether an attribute can be applied more than once to the same target is controlled by AttributeUsage on the attribute class; the built-in ActionFilterAttribute is already decorated with AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true).

Registering and Applying Filters

Three scopes are available, and they compose:

  • Global: added in Program.cs with builder.Services.AddControllers(o => o.Filters.Add<MyFilter>()) — applies to every action in the API.

  • Controller or action: apply the attribute directly, e.g. [MyLoggingFilter] on a controller class or a single method.

  • [ServiceFilter] / [TypeFilter]: use these when the filter needs constructor dependencies. [ServiceFilter] resolves the filter type from the DI container, so you must register it first (builder.Services.AddScoped<MyFilter>()); [TypeFilter] creates it with ActivatorUtilities and can take constructor arguments inline. Both implement IFilterFactory, which is the interface to implement if you write your own attribute-based filter that needs services.

Built-in Alternatives

Before writing a custom filter, check the built-ins: [ResponseCache] (response caching), [Produces]/[Consumes] (content negotiation), [RequireHttps], and the automatic 400 Problem Details validation response that [ApiController] produces for invalid model state. Cross-cutting concerns such as logging, validation, and error mapping are often better expressed as middleware or with IExceptionHandler plus AddProblemDetails(), leaving action filters for logic that genuinely needs the action’s arguments or result.

Code Example

A logging filter applied to a single action, plus a DI-backed filter registered through [ServiceFilter]:

using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;

// Simple attribute filter — no constructor dependencies
public class LogActionFilterAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        var args = string.Join(", ", context.ActionArguments.Select(a => $"{a.Key}={a.Value}"));
        Console.WriteLine($"Before {context.ActionDescriptor.DisplayName}. Args: {args}");
        base.OnActionExecuting(context);
    }

    public override void OnActionExecuted(ActionExecutedContext context)
    {
        Console.WriteLine($"After {context.ActionDescriptor.DisplayName}");
        base.OnActionExecuted(context);
    }
}

// Filter that consumes a scoped service via [ServiceFilter]
public class AuditFilter : IActionFilter
{
    private readonly ILogger<AuditFilter> _logger;
    public AuditFilter(ILogger<AuditFilter> logger) => _logger = logger;

    public void OnActionExecuting(ActionExecutingContext context)
    {
        // Short-circuit: assigning a result prevents the action from running
        if (!context.ActionArguments.TryGetValue("id", out var id) || id is not int value || value <= 0)
        {
            context.Result = new BadRequestObjectResult("Id must be a positive integer.");
            return;
        }
        _logger.LogInformation("Audit: request for id {Id}", value);
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        if (context.Result is OkObjectResult ok)
            _logger.LogInformation("Audit: returned value {Value}", ok.Value);
    }
}

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet("{id:int}")]
    [LogActionFilter]                       // applied to this action only
    [ServiceFilter(typeof(AuditFilter))]    // resolved from DI
    public ActionResult<string> Get(int id) => Ok($"product-{id}");
}

Registration in Program.cs:

builder.Services.AddScoped<AuditFilter>();              // required for [ServiceFilter]
builder.Services.AddControllers(o =>
{
    o.Filters.Add<LogActionFilterAttribute>();          // global scope
});

Best Practices and Common Pitfalls

  • Prefer async over sync: implement IAsyncActionFilter with a single OnActionExecutionAsync method when the filter does I/O; mixing sync and async filters adds thread-pool overhead.

  • Keep filters stateless and thread-safe: filter instances may be reused across requests, so never store per-request state in fields.

  • Register DI-backed filters as scoped or transient: a singleton filter holding a scoped service causes a captive-dependency bug.

  • Don’t swallow exceptions in OnActionExecuted: that is not its purpose; use IExceptionFilter or IExceptionHandler and return RFC 9457 Problem Details.

  • Order matters: when combining authorization, validation, and logging filters, set Order explicitly so short-circuiting happens at the intended stage.

26

What distinguishes action filters from authorization filters in ASP.NET Core Web API, and when should each type be used?

Both action filters and authorization filters are part of the ASP.NET Core filter pipeline, but they run at different stages and serve different purposes. Authorization filters execute first, before model binding, to decide whether a request is allowed to proceed, while action filters wrap the execution of the action method itself to add cross-cutting behavior such as logging or timing.

Read full answer

Applies toASP.NET Core Web API on .NET 8

Where Each Runs in the Pipeline

ASP.NET Core invokes filters in a fixed order:

  1. Authorization filters

  2. Resource filters

  3. Model binding filters

  4. Action filters

  5. Exception filters

  6. Result filters

Because authorization filters run before model binding, they cannot inspect the bound model or ModelState. Action filters run after model binding succeeds, so they see the validated action arguments and can read or modify them.

Authorization Filters

  • Implement IAuthorizationFilter (synchronous) or IAsyncAuthorizationFilter (recommended for async all the way).

  • The method OnAuthorizationAsync receives an AuthorizationFilterContext.

  • To block a request, set context.Result to an IActionResult such as UnauthorizedResult or ForbidResult; this short-circuits the pipeline so the action never runs.

  • The built-in [Authorize] attribute is itself an authorization filter. For most scenarios, prefer policy-based authorization through the authorization service instead of writing a custom filter.

Action Filters

  • Implement IActionFilter or IAsyncActionFilter.

  • With the synchronous interface, OnActionExecuting runs before the action and OnActionExecuted runs after. With the async interface, call await next() to invoke the action and receive the ActionExecutedContext.

  • They can read or modify context.ActionArguments, inspect the result, add response headers, or short-circuit by setting context.Result in OnActionExecuting.

  • Typical uses: logging, performance timing, telemetry, adding custom headers, caching metadata.

Comparison

Aspect Authorization Filter Action Filter
Pipeline position First, before model binding After model binding, around the action
Sees bound model No Yes
Primary purpose Allow or deny execution Add behavior before and after execution
Short-circuit Via context.Result Via context.Result in OnActionExecuting
Built-in example [Authorize] Logging, response caching

When to Use Each

  • Use authorization filters when you need to decide whether a request should be allowed to reach the action—for example, checking authentication, roles, policies, or custom requirements such as tenant validation. They are the correct place for any decision that must happen before model binding and action execution.

  • Use action filters when you need to observe or modify the action execution itself: logging entry and exit, measuring execution time, manipulating action arguments, adding response headers, or caching results. They are also appropriate for cross-cutting concerns that should run only after the request has been authorized.

  • Never use action filters for security decisions. By the time they run, model binding has already occurred, and an unauthorized request may have already consumed resources or triggered unwanted side effects.

  • For most authorization needs, rely on the built-in [Authorize] attribute and policy-based authorization rather than writing a custom authorization filter. Custom authorization filters are rarely needed unless you must inspect the raw request context before the standard authorization system runs.

Best Practices and Common Pitfalls

  • Use the async interfaces (IAsyncAuthorizationFilter, IAsyncActionFilter) to avoid thread-pool starvation under load.

  • Never put authorization logic in an action filter: it runs too late, after model binding, and bypasses the authorization system.

  • Setting context.Result in an authorization filter prevents the action from running entirely; use this deliberately.

  • For authorization, prefer [Authorize] with policies over custom filters unless you need low-level context access.

  • Apply filters at the right scope: global for cross-cutting concerns, per-controller or per-action with [ServiceFilter] or [TypeFilter] for specific behavior.

  • In Minimal APIs, the equivalent concept is endpoint filters via AddEndpointFilter, not MVC action or authorization filters.

27

How do you build a custom authentication mechanism for an ASP.NET Core Web API?

Custom authentication in ASP.NET Core Web API is built by writing a custom authentication handler that derives from AuthenticationHandler<TOptions>, registering it as a named scheme, and selecting it with [Authorize]. The handler inspects incoming requests, validates credentials however your scenario requires (API keys, signed requests, or an external validation service), and produces an AuthenticateResult that the authorization system then uses.

Read full answer

Applies toASP.NET Core Web API on .NET 8

How It Works

  • AuthenticationHandler<TOptions>: inherit from this class and override HandleAuthenticateAsync() to read credentials and build a ClaimsPrincipal.

  • AuthenticationSchemeOptions: a small options class for settings such as the header name to read.

  • Scheme registration: AddScheme<TOptions, THandler> adds the handler under a name used by [Authorize(AuthenticationSchemes = "...")].

  • Middleware order: app.UseAuthentication() must come before app.UseAuthorization(), otherwise HttpContext.User is never populated.

  • Result types: return Success(ticket) for valid credentials, Fail(message) to reject (401), and NoResult() when your handler does not own the request so other schemes can try.

The same scheme also protects Minimal API endpoints via RequireAuthorization(); controller-based and Minimal APIs share the authentication pipeline.

When to Use a Custom Handler

Scenario Recommended approach
Standard bearer tokens JWT bearer (AddJwtBearer) or ASP.NET Core Identity
API keys in a custom header Custom AuthenticationHandler
HMAC / request signing Custom AuthenticationHandler
Validation against another service Custom handler + IHttpClientFactory

For ordinary token APIs, prefer the built-in JWT bearer scheme; write a custom handler only when built-in schemes cannot express your credential format.

Code Example

public class ApiKeyOptions : AuthenticationSchemeOptions
{
    public string HeaderName { get; set; } = "X-API-Key";
}

public class ApiKeyAuthenticationHandler
    : AuthenticationHandler<ApiKeyOptions>
{
    private readonly IHttpClientFactory _httpClientFactory;

    public ApiKeyAuthenticationHandler(
        IOptionsMonitor<ApiKeyOptions> options,
        ILoggerFactory logger,
        UrlEncoder encoder,
        ISystemClock clock,
        IHttpClientFactory httpClientFactory)
        : base(options, logger, encoder, clock)
    {
        _httpClientFactory = httpClientFactory;
    }

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        if (!Request.Headers.TryGetValue(Options.HeaderName, out var key))
            return AuthenticateResult.NoResult();

        var client = _httpClientFactory.CreateClient("ValidationApi");
        using var request = new HttpRequestMessage(HttpMethod.Get, "/keys/validate");
        request.Headers.Add(Options.HeaderName, key);

        using var response = await client.SendAsync(request);
        if (!response.IsSuccessStatusCode)
            return AuthenticateResult.Fail("Invalid API key");

        var claims = new[] { new Claim(ClaimTypes.Name, key) };
        var identity = new ClaimsIdentity(claims, Scheme.Name);
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, Scheme.Name);

        return AuthenticateResult.Success(ticket);
    }
}
// Program.cs
builder.Services.AddAuthentication("ApiKey")
    .AddScheme<ApiKeyOptions, ApiKeyAuthenticationHandler>("ApiKey", null);

builder.Services.AddHttpClient("ValidationApi", c =>
    c.BaseAddress = new Uri("https://internal-api.example.com"));

builder.Services.AddAuthorization();
builder.Services.AddProblemDetails();
builder.Services.AddControllers();

var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet]
    [Authorize(AuthenticationSchemes = "ApiKey")]
    public ActionResult<IEnumerable<Product>> Get()
        => Ok(Enumerable.Empty<Product>());
}

Best Practices and Pitfalls

  • Async all the way: await every I/O call in the handler; never block on async work.

  • Reuse HttpClient: obtain clients from IHttpClientFactory; never create new HttpClient() per request.

  • Never log secrets: API keys and tokens must not appear in logs or exception messages.

  • Enforce HTTPS: credentials travel in headers and require TLS in production.

  • Fail vs NoResult: Fail rejects known-bad credentials with 401; NoResult lets other schemes attempt authentication.

  • CORS: configure explicitly and never combine AllowAnyOrigin with AllowCredentials.

  • Problem Details: with AddProblemDetails(), invalid models return 400 automatically under [ApiController]; use Problem() for custom error responses.

28

Explain the stages an HTTP request passes through in the ASP.NET Web API 2 pipeline, from the host down to the controller action and back to the client.

The HTTP message lifecycle in ASP.NET Web API 2 is the journey of an HttpRequestMessage from the host, through a chain of message handlers and the routing/controller dispatchers, into a controller action, and then back out as an HttpResponseMessage through the same chain in reverse order.

Read full answer

Applies toASP.NET Web API 2 on .NET Framework 4.8 (classic). This pipeline does not exist in ASP.NET Core, where middleware and filters take its place.

How the Pipeline Is Structured

  • Every stage is an HttpMessageHandler. DelegatingHandler is the base class for handlers that hand work off to an InnerHandler, which is what lets handlers form a chain.

  • HttpServer is the top-level handler and the entry point of the pipeline. Its default inner handler is HttpRoutingDispatcher, whose inner handler is HttpControllerDispatcher.

  • Custom handlers added to HttpConfiguration.MessageHandlers are inserted between HttpServer and HttpRoutingDispatcher, in the order they were registered.

Inbound Stages

  1. Host boundary: the host (IIS/ASP.NET through HttpControllerHandler, or self-hosting/OWIN) receives the request and converts it into an HttpRequestMessage.

  2. HttpServer: receives the message and starts the handler chain.

  3. Custom DelegatingHandler chain: each handler can inspect or modify the request, short-circuit by returning an HttpResponseMessage directly, or call base.SendAsync to continue.

  4. HttpRoutingDispatcher: matches the route, populates request.GetRouteData(), and forwards to HttpControllerDispatcher. If nothing matches, a 404 response starts the return journey here.

  5. HttpControllerDispatcher: uses IHttpControllerActivator to create the controller and calls IHttpController.ExecuteAsync.

  6. Controller pipeline: authentication filters, then authorization filters, then parameter/model binding, then action filters (OnActionExecuting), the action method itself, action filters (OnActionExecuted), and exception filters if an exception propagates. The action returns an IHttpActionResult, an HttpResponseMessage, or a model object that a media-type formatter serializes into the response body.

Outbound Stages

  • The HttpResponseMessage produced by the action flows back through the exact same handlers in reverse order; the code after await base.SendAsync(...) in each handler runs on the way out.

  • HttpServer is the last stop: it hands the finished response to the host, which writes it to the client.

Key Components

Component Responsibility Runs when no route matches?
HttpServer Pipeline entry, host boundary Yes
Custom DelegatingHandlers Cross-cutting concerns over raw messages Yes
HttpRoutingDispatcher Route selection and dispatch Yes (it performs the matching)
HttpControllerDispatcher Controller activation and execution No
Filters (auth, authz, action, exception) Per-action concerns such as authorization and validation No

Best Practices and Modern Replacement

  • Use handlers only for message-level concerns: handlers see HttpRequestMessage/HttpResponseMessage but no HttpContext, no route data, and no controller context on the way in, so logic that needs routing or the authenticated user belongs in filters, not handlers.

  • Mind the registration order: handlers added first execute first on the way in and last on the way out, which matters for concerns such as logging and compression.

  • Prefer short-circuiting early: returning a response from a handler skips routing, controller activation, and filters, which is efficient for failures such as an invalid API key.

  • Remember this stack is legacy: classic Web API 2 is superseded by ASP.NET Core, where HttpServer and DelegatingHandler are replaced by middleware (InvokeAsync) plus MVC filters; new development should target ASP.NET Core 8 or later.

29

How do you set up dependency injection in an ASP.NET Core Web API project?

Dependency injection in ASP.NET Core Web API is provided by the framework’s built-in IoC container: you register services on IServiceCollection in Program.cs, and the container resolves them automatically through constructor parameters in controllers, services, and other components.

Read full answer

Applies toASP.NET Core Web API on .NET 8 (classic ASP.NET Web API 2 on .NET Framework 4.8 is noted separately at the end).

How It Works

ASP.NET Core is built around DI, so almost every framework component (routing, authentication, options, logging) is resolved from the same container you configure. The flow is:

  1. WebApplication.CreateBuilder gives you a WebApplicationBuilder whose Services property is an IServiceCollection.

  2. You register each application service with an interface-to-implementation mapping and a lifetime.

  3. Framework services are added with extension methods such as AddControllers, AddSwaggerGen, AddProblemDetails, AddAuthentication, and AddCors.

  4. At runtime the container constructs your controller, inspecting its constructor and injecting the registered services.

Constructor injection is the preferred form; the container also supports method-level injection in Minimal API route handlers and property injection (via [FromServices]) only for objects the container itself creates.

Service Lifetimes

The lifetime you choose controls how many instances exist and how long they are shared.

Lifetime Registration method When an instance is created
Singleton AddSingleton<TInterface, TImpl>() Once per application; shared by all requests
Scoped AddScoped<TInterface, TImpl>() Once per request (per DI scope); reused within that request
Transient AddTransient<TInterface, TImpl>() Every time it is resolved

Use singleton for stateless or thread-safe shared services (caches, configuration wrappers), scoped for request-scoped work such as Entity Framework DbContext, repositories, and units of work, and transient for lightweight, stateless, short-lived helpers.

Code Example

Registration in Program.cs:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();                 // [ApiController], model binding, Problem Details
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();                  // .NET 8 template; .NET 9+ uses AddOpenApi()

// Application services, one line per registration
builder.Services.AddSingleton(TimeProvider.System); // .NET 8 time abstraction
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddTransient<IEmailSender, SmtpEmailSender>();

// Factory registration when you need to build the instance yourself
builder.Services.AddSingleton<IFeatureFlags>(sp =>
    new FeatureFlags(builder.Configuration.GetSection("Features")!));

// .NET 8 keyed services: register several implementations of one interface
builder.Services.AddKeyedSingleton<ICache, MemoryCache>("local");
builder.Services.AddKeyedSingleton<ICache, RedisCache>("redis");

var app = builder.Build();

app.UseHttpsRedirection();
app.UseAuthentication();   // must come before UseAuthorization
app.UseAuthorization();
app.MapControllers();
app.Run();

Consumption in a controller-based API:

[ApiController]
[Route("api/[controller]")]
public class OrdersController(IOrderRepository repo, TimeProvider clock) : ControllerBase
{
    [HttpGet("{id:int}")]
    public async Task<ActionResult<Order>> Get(int id, CancellationToken ct)
    {
        var order = await repo.GetAsync(id, ct);
        return order is null ? NotFound() : Ok(order);
    }

    [HttpPost]
    public async Task<ActionResult<Order>> Create(Order order, CancellationToken ct)
    {
        order.PlacedAt = clock.GetUtcNow();
        await repo.AddAsync(order, ct);
        return CreatedAtAction(nameof(Get), new { id = order.Id }, order);
    }

    // .NET 8: pick a specific keyed implementation
    [HttpGet("cached/{id:int}")]
    public async Task<ActionResult<Order>> GetCached(
        int id, [FromKeyedServices("redis")] ICache cache, CancellationToken ct)
        => Ok(await cache.GetOrLoadAsync(id, () => repo.GetAsync(id, ct)));
}

Minimal APIs resolve registered services from the same container, so the two approaches stay consistent:

app.MapGet("/orders", async (IOrderRepository repo, CancellationToken ct)
    => Results.Ok(await repo.ListAsync(ct)));

Best Practices and Common Pitfalls

  • Depend on abstractions: register against interfaces so implementations can be swapped or mocked in tests.

  • Keep constructors lean: inject only what the class needs; heavy constructors usually signal a class with too many responsibilities.

  • Never inject a scoped service into a singleton: the singleton captures it and it stops being request-scoped. If a singleton needs request-scoped work, inject IServiceScopeFactory and create a scope inside the method.

  • Avoid the service locator pattern: do not call serviceProvider.GetService<T>() inside controllers or business logic; it hides dependencies and breaks testing.

  • Validate registrations in development: builder.Host.UseDefaultServiceProvider(o => { o.ValidateScopes = true; o.ValidateOnBuild = true; }) throws early when a required service is missing or a scope rule is violated.

  • Use typed HTTP clients: call other services through AddHttpClient<IWeatherClient, WeatherClient>() so IHttpClientFactory manages handler lifetimes instead of creating a new HttpClient per request.

  • Register DbContext as scoped with AddDbContext<T> so each request gets its own context and connections are not shared across concurrent requests.

Classic ASP.NET Web API 2 (for contrast)

In classic ASP.NET Web API 2 on .NET Framework 4.8 there is no built-in container. You add a third-party container (Autofac, Unity, Ninject) and wire it into the pipeline by implementing IDependencyResolver and IDependencyScope and assigning it in WebApiConfig via config.DependencyResolver. The lifetime concepts are the same, but the plumbing is manual and entirely framework-provided in ASP.NET Core.

30

Which testing frameworks and libraries are commonly used to test Web API applications?

Several testing frameworks and libraries form a standard stack for testing ASP.NET Core Web API applications. The most widely used combination is xUnit as the test runner, Moq or NSubstitute for isolating dependencies, FluentAssertions for readable assertions, and Microsoft.AspNetCore.Mvc.Testing for in-process integration tests through WebApplicationFactory<T>. Together these let you write fast unit tests for controllers and services plus realistic end-to-end tests that exercise the full HTTP pipeline without deploying the app.

Read full answer

Applies toASP.NET Core Web API on .NET 8

Unit Test Frameworks

  • xUnit: the de facto standard test framework for .NET; supports [Fact] for single-run tests and [Theory] with [InlineData] or [MemberData] for parameterized tests. It integrates with dotnet test, Visual Studio Test Explorer, and CI pipelines.

  • NUnit: a mature alternative with a similar attribute-based model ([Test], [TestCase]) and a rich set of constraints.

  • MSTest: the Microsoft-supported framework, modernized in .NET 8 with the new Microsoft.Testing.Platform; a good choice for teams already invested in the Microsoft ecosystem.

Mocking and Assertions

  • Moq: the most popular mocking library; creates substitutes for interfaces and virtual members so you can test a controller or service in isolation.

  • NSubstitute: a friendlier syntax alternative to Moq (Substitute.For<T>(), Received()), often preferred for readability.

  • FluentAssertions: replaces the built-in Assert class with chainable, human-readable assertions like response.StatusCode.Should().Be(HttpStatusCode.OK).

  • AutoFixture: generates test data automatically, reducing boilerplate in [Theory] tests.

Integration Testing with WebApplicationFactory

  • Microsoft.AspNetCore.Mvc.Testing: provides WebApplicationFactory<TEntryPoint>, which hosts your API in-memory using the real middleware, routing, model binding, validation and filters.

  • Tests call factory.CreateClient() to get an HttpClient and issue real HTTP requests against the app, receiving full HttpResponseMessage objects.

  • Override ConfigureWebHost to swap out services for test doubles, for example replacing a real database context with an in-memory provider or a test database.

  • For top-level statement entry points, Program refers to the auto-generated entry-point class; if it is not visible to the test assembly, add [assembly: InternalsVisibleTo("YourTestProject")] to the web project or declare a public partial class Program { }.

Stubbing External Services

  • WireMock.Net: stands up a local HTTP mock server to simulate third-party APIs your service calls, so integration tests stay deterministic and offline.

  • RichardSzalay.MockHttp: intercepts HttpClient requests in memory; lighter weight than WireMock but only works for requests made through a handler you configure.

Best Practices and Common Pitfalls

  • Run unit tests and integration tests in separate projects; integration tests are slower and may need test containers or a real database.

  • Never create a new HttpClient per test; reuse the one from CreateClient(), and in production code inject IHttpClientFactory instead of constructing clients directly.

  • Keep tests independent and idempotent; clean up test data between runs so tests can execute in any order.

  • Prefer testing through the HTTP layer for integration tests rather than calling controller methods directly, which skips middleware, filters and model binding.

  • Write async test methods returning Task and avoid .Result or .Wait(), which waste threads and can cause thread-pool starvation.

32

How do you combine Entity Framework Core data access layers with an ASP.NET Core Web API application?

Integrating Entity Framework Core with ASP.NET Core Web API requires registering the database context in the dependency injection container and injecting it into controller actions that return strongly-typed results. This approach ensures dependency injection manages the DbContext lifecycle while controllers handle HTTP semantics and serialization.

Read full answer

Applies toASP.NET Core Web API on .NET 8 with Entity Framework Core 8 (SQL Server provider).

Registration and Dependency Injection

The DbContext must be registered in the service container during application startup in Program.cs. Using AddDbContext configures the context with a database provider — SQL Server via the Microsoft.EntityFrameworkCore.SqlServer NuGet package — and sets the default lifetime to scoped, which aligns with the request scope of a Web API application. Connection strings should be retrieved from configuration rather than hardcoded to maintain security and support different environments.

Controller Design and HTTP Semantics

Controllers should inherit from ControllerBase and be annotated with [ApiController] to enable automatic model validation and attribute routing requirements. Actions must use async and await when calling EF Core methods like ToListAsync or SaveChangesAsync to prevent thread pool starvation. Correct HTTP status codes signal resource state: return 200 for successful GETs, 201 Created with a Location header for successful POSTs, 204 No Content for successful DELETEs, and 404 Not Found when a resource does not exist.

Error Handling with Problem Details

Errors should be returned using the Problem Details format (RFC 9457) via the Problem() method. Configure global problem details behavior using AddProblemDetails in Program.cs if custom conventions are needed. When model validation fails, [ApiController] automatically returns a 400 status with a ValidationProblem response containing the specific validation errors. For business logic failures, such as a conflict during update, explicitly return Conflict() or Problem() with a specific status code to ensure clients receive standardized error payloads. Custom exception filters can centralise this logic across the application.

Code Example

// Program.cs
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

// AppDbContext.cs
public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    public DbSet<Product> Products => Set<Product>();
}

// Product.cs
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
}

// ProductsController.cs
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly AppDbContext _context;

    public ProductsController(AppDbContext context)
    {
        _context = context;
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<Product>> GetProduct(int id)
    {
        var product = await _context.Products.FindAsync(id);
        if (product is null)
            return NotFound();
        return product;
    }

    [HttpPost]
    public async Task<ActionResult<Product>> CreateProduct(Product product)
    {
        _context.Products.Add(product);
        await _context.SaveChangesAsync();
        return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
    }
}

Best Practices and Alternatives

Always query asynchronously and avoid blocking calls like .Result or .Wait() to prevent deadlocks. Use AsNoTracking() for read-only operations to reduce EF Core overhead and improve performance. Ensure migrations are applied via dotnet ef database update or programmatically at startup during development. Minimal APIs are a valid alternative in .NET 8; the table below summarises the trade-offs:

Aspect Controller-based API Minimal API
Definition ControllerBase + [ApiController] MapGet/MapPost in Program.cs
Results ActionResult<T>, IActionResult TypedResults for typed results
Best for Large apps needing filters, conventions, validation Small services and lightweight endpoints
34

Explain what action selectors are in ASP.NET Core Web API and how the framework uses them to pick which action handles a request.

Action selectors are attributes (and the supporting infrastructure behind them) that you apply to controller actions so ASP.NET Core can decide which action method should handle an incoming HTTP request. The framework’s IActionSelector implementation examines the HTTP method, route values, and other request data through action constraints, then matches them against the selectors on candidate actions to pick exactly one — or returns a 404 when no action matches.

Read full answer

Applies toASP.NET Core Web API on .NET 8 (controller-based APIs using `[ApiController]` and `ControllerBase`).

How Action Selection Works

When a request arrives, endpoint routing first builds a list of candidate actions from all controllers whose route templates match the URL. Then the action selector narrows that list:

  1. HTTP method matching — an action decorated with [HttpGet] only matches GET requests; [HttpPost] matches POST, and so on. Actions without an HTTP method attribute match any verb.

  2. Route and constraint matching — [Route] templates and route values (such as an id segment) must align with the request URL, and route constraints like :int further filter candidates.

  3. Ambiguity resolution — the selector applies action constraints to disambiguate remaining candidates; if two actions still match indistinguishably, it throws an AmbiguousMatchException rather than guessing.

Key Action Selector Attributes

  • [HttpGet], [HttpPost], [HttpPut], [HttpPatch], [HttpDelete], [HttpHead], [HttpOptions]: constrain an action to one HTTP verb. Each accepts an optional template, e.g. [HttpGet("{id:int}")].

  • [Route]: sets the URL template for an action or a whole controller. On a controller it acts as a prefix for all its actions.

  • [ActionName]: overrides the name used for action selection and link generation, so the method name and the route name can differ.

  • [NonAction]: marks a public method that should not be treated as an action (useful for helper methods on a controller).

  • [NonController]: marks a class that should not be treated as a controller, even if its name ends in Controller.

Action Result and Status Codes

Selecting the action is only the first half; the return type determines the response. With [ApiController], an invalid model state automatically produces a 400 Bad Request with application/problem+json (RFC 9457) details. A well-shaped API returns 200 OK for reads, 201 Created with a Location header for inserts, 204 No Content for successful deletions or updates that return nothing, 404 Not Found when the resource does not exist, and 409 Conflict for duplicate keys.

Best Practices and Common Pitfalls

  • Be specific about verbs: decorating every action with [HttpGet]/[HttpPost] prevents accidental verb overloading and makes the API contract explicit.

  • Avoid ambiguous routes: two actions on the same controller with [HttpGet("{id}")] and no distinguishing constraint will collide at runtime — use constraints such as :int or :guid to disambiguate.

  • Keep GET, PUT and DELETE idempotent: clients rely on being able to retry these safely; never mutate state irreversibly inside a GET.

  • Prefer ActionResult<T>: it gives compile-time type checking for the success case while still allowing error results such as NotFound().

  • Minimal APIs are a separate model: endpoint selection there uses MapGet, MapPost, and friends on IEndpointRouteBuilder, with TypedResults for typed results — not attributes on controller classes.

35

Describe how exception filters are implemented to handle unhandled errors within an ASP.NET Core Web API application.

Exception Filters provide a centralized strategy for catching unhandled exceptions thrown during controller action execution, ensuring clients receive standardized error responses instead of raw stack traces. They run inside the ASP.NET Core MVC pipeline after routing has selected a controller and action, letting you log errors and return consistent Problem Details payloads globally or per-controller.

Read full answer

Applies toASP.NET Core Web API on .NET 8

Role in the Request Pipeline

Exception filters implement the IExceptionFilter interface (or IAsyncExceptionFilter for async) and execute only when an unhandled exception bubbles up from a controller action. Unlike the UseExceptionHandler middleware, which wraps the entire request pipeline and can catch errors during routing, startup, or before an endpoint is reached, exception filters are scoped to MVC action execution. This makes them ideal for handling runtime exceptions thrown by actions or MVC-resolved services while still having access to action context such as the route data and action descriptor. You can register them globally via MvcOptions or apply them locally using [ServiceFilter(typeof(GlobalExceptionFilter))], [TypeFilter(typeof(GlobalExceptionFilter))], or a custom attribute class that implements IExceptionFilter.

Integrating Problem Details

Modern APIs should return errors following RFC 9457 (Problem Details, which obsoletes RFC 7807). In .NET 8 you register the supporting services with builder.Services.AddProblemDetails(), which configures the ProblemDetailsFactory and options used by Problem() and ValidationProblem(). Inside an exception filter you construct a ProblemDetails object manually and must set the ContentType to application/problem+json yourself, since a plain ObjectResult would otherwise default to application/json. The payload includes Status, Title, and an optional Detail, letting clients programmatically distinguish error types instead of parsing free-text messages.

Code Example

The following filter captures exceptions, logs them server-side, and returns a 500 Internal Server Error with a RFC 9457 problem details payload.

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;

public class GlobalExceptionFilter : IExceptionFilter
{
    private readonly ILogger<GlobalExceptionFilter> _logger;

    // Resolved from DI when registered via options.Filters.Add<T>()
    public GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger)
    {
        _logger = logger;
    }

    public void OnException(ExceptionContext context)
    {
        // Log the full exception server-side before returning a generic response
        _logger.LogError(context.Exception, "Unhandled exception occurred");

        var problem = new ProblemDetails
        {
            Status = StatusCodes.Status500InternalServerError,
            Title = "Internal Server Error",
            Detail = "An unexpected error occurred while processing your request."
        };

        context.Result = new ObjectResult(problem)
        {
            StatusCode = problem.Status.Value,
            ContentType = "application/problem+json"
        };
        context.ExceptionHandled = true;
    }
}

Register the filter globally in Program.cs alongside controller services:

builder.Services.AddControllers(options =>
{
    options.Filters.Add<GlobalExceptionFilter>();
});

Best Practices and Pitfalls

  • Avoid Sensitive Data: Never expose exception messages or stack traces in the Detail field in production, as this leaks implementation details to potential attackers.

  • Logging: Always log the full exception server-side with ILogger<T> before returning a generic response to aid debugging without exposing internals.

  • Middleware vs Filters: For comprehensive global handling, UseExceptionHandler middleware is often preferred in .NET 8 because it catches errors outside the MVC pipeline; use filters when you need MVC-specific context like action descriptors or route values.

  • Mark Handled: Setting context.ExceptionHandled = true is crucial; otherwise the exception propagates and may trigger default error handling or terminate the request.

  • Model State: With [ApiController], invalid model state returns 400 automatically via an action filter before exception filters run, so exception filters focus on runtime exceptions rather than validation errors.

  • Async Actions: For async controller actions, prefer implementing IAsyncExceptionFilter with OnExceptionAsync to avoid blocking threads.

36

Why should you use async and await in an ASP.NET Core Web API, and what advantages does it bring over synchronous request handling?

Async/await lets a Web API handle far more concurrent requests with the same thread pool by releasing threads while operations wait on I/O, such as database calls, disk reads or outbound HTTP requests, instead of blocking them. It is the default and recommended way to write ASP.NET Core Web API code on .NET 8, and it works identically on Windows and Linux because it relies on the .NET managed thread pool, not a platform-specific feature.

Read full answer

Applies toASP.NET Core Web API on .NET 8 (controller-based and Minimal APIs); the same principles apply to .NET Framework 4.8 classic Web API 2.

How It Works

Every request in ASP.NET Core is handled on a thread from the shared thread pool. When code awaits an I/O-bound operation, the compiler-generated state machine captures the continuation, returns the thread to the pool, and resumes on a pool thread when the operation completes. The key effects are:

  • No thread is occupied while waiting for a database query, a remote API, or a file to return.

  • The request pipeline is fully asynchronous, so a blocked thread never stalls an earlier middleware stage.

  • Scalability improves without raising ThreadPool settings or adding servers: throughput is limited far less by thread count and far more by downstream service capacity.

Key Benefits

  • Higher throughput under load: the same hardware serves more concurrent requests, because the fixed pool of threads is not exhausted by waiting work.

  • Lower memory and resource pressure: blocked threads each keep a stack and context alive; async requests free those resources during waits.

  • Better responsiveness and tail latency: requests are not queued behind long-running synchronous work holding pool threads.

  • Natural composition: Task and Task<T> chain cleanly with Task.WhenAll, IAsyncEnumerable, streaming endpoints, and background services.

  • First-class framework support: IActionResult, ActionResult<T>, Results, and Minimal API delegates all support async handlers; EF Core, HttpClient via IHttpClientFactory, and file APIs expose async-only or async-preferred methods.

Async Versus Sync: Comparison

Aspect Synchronous Async/await
Thread usage during I/O Blocks a pool thread Thread released to the pool
Scalability Limited by pool size (default based on CPU count) Scales to thousands of concurrent waits
Memory per waiting request Stack and context retained Minimal, only the state machine
Code style Simpler to read, no ConfigureAwait concerns Slightly more complex; avoid .Result/.Wait()
Best suited for Pure CPU-bound work I/O-bound work: DB, HTTP, files, queues

Best Practices and Common Pitfalls

  • Use async all the way: never mix blocking calls such as .Result, .Wait(), or Task.Delay(...).Wait() into an async path — that causes real deadlocks and pool starvation.

  • Prefer async Task<T> over async void, except for fire-and-forget event handlers; async void cannot be awaited and hides exceptions.

  • Use CancellationToken in every handler and pass it down to EF Core, HttpClient, and other async APIs so slow clients can be abandoned cleanly.

  • Offload genuinely CPU-bound work to Task.Run only when needed, and measure it; async does not make CPU work faster.

  • Do not await inside a tight loop when you can batch; use Task.WhenAll for parallel independent calls, but respect downstream rate limits.

  • Measure before claiming wins: benchmark with the expected load profile; async mainly helps when requests actually wait on I/O.

37

What approaches does ASP.NET Core Web API provide for receiving and processing files that a client uploads?

File uploads in ASP.NET Core Web API are handled through model binding of IFormFile parameters on controller actions (or Minimal API handlers), where the framework reads the multipart/form-data request body and exposes each uploaded file as a stream-backed object you can validate and save. You never read the raw request body yourself for typical uploads; instead you rely on the built-in form binding, validate size and content type, and stream the file to storage.

Read full answer

Applies toASP.NET Core Web API on .NET 8

How File Upload Binding Works

  • IFormFile: Binds a single uploaded file from a multipart/form-data request; the parameter is populated automatically on a matching POST.

  • IEnumerable<IFormFile> or IFormFileCollection: Binds multiple files sent under the same form field name.

  • [FromForm]: Explicitly marks the source; with [ApiController] it is usually inferred, but adding it makes intent clear.

  • Content type: The client must send Content-Type: multipart/form-data; boundary=...; sending application/json will not bind files.

Approach Best for Notes
IFormFile parameter Typical single-file uploads Bound automatically from multipart/form-data
IEnumerable<IFormFile> Multiple files in one field Iterate and process each entry
IFormFileCollection / Request.Form.Files All files regardless of field name Full control over the whole collection
Raw HttpRequest.Body stream Very large files, custom protocols You parse multipart/form-data yourself; rarely needed

Validating Uploaded Files

Always validate before saving, because the filename and content type come from the client: - Size: Check file.Length against a limit, and also configure FormOptions.MultipartBodyLengthLimit and Kestrel’s MaxRequestBodySize so oversized requests are rejected early with 413. - Content type: Whitelist MIME types (e.g. image/png, application/pdf) rather than trusting the extension. For stronger assurance, inspect the file’s magic bytes after reading. - Filename: Never use file.FileName directly in a path; generate a new name (e.g. a GUID) and append the sanitized extension.

Storing Files Safely

  • Stream, don’t buffer: Use await file.CopyToAsync(stream) so large files are not loaded entirely into memory.

  • Location: Store outside the web root, or if inside, ensure the folder does not allow script execution. Never store uploads where they can be executed.

  • Return semantics: Return 201 Created with a Location header pointing to the stored resource; return 400 for validation failures and 413 Payload Too Large for oversized files. With [ApiController], invalid models already return 400 automatically. Use Problem() or ValidationProblem() to return RFC 9457 Problem Details.

Minimal APIs Alternative

The same upload works in a Minimal API with app.MapPost and an IFormFile parameter, returning Results.Created or TypedResults.BadRequest. Choose controllers when you need attributes, filters and OpenAPI grouping; choose Minimal APIs for simpler endpoints.

Common Pitfalls

  • Trusting client filenames enables path traversal; always rewrite the name and combine with a fixed folder.

  • Missing size limits lets a client exhaust server memory or disk; set both MultipartBodyLengthLimit and MaxRequestBodySize.

  • Returning the file path directly can leak internal structure; return an opaque identifier or a dedicated download URL instead.

  • Antivirus scanning: for public-facing uploads, scan stored files before they are served or processed further.

  • Blocking the request thread: use async all the way and avoid .Result or .Wait() so upload I/O does not starve the thread pool.

38

Explain how SignalR works and how it is integrated into an ASP.NET Core Web API application.

SignalR is a real-time communication library that lets a server push asynchronous notifications to connected clients, and it integrates into an ASP.NET Core Web API as a set of hub endpoints registered alongside the API’s controller routes.

Read full answer

Applies toASP.NET Core 8.0 with the built-in `Microsoft.AspNetCore.SignalR` framework. The classic, separate ASP.NET SignalR 2.x runtime on .NET Framework 4.8 is legacy and should not be mixed with the ASP.NET Core stack.

How It Works

SignalR abstracts a persistent connection between client and server and negotiates the best available transport:

Transport Direction Role
WebSockets Bidirectional over TCP Preferred transport
Server-Sent Events (SSE) Server to client Fallback when WebSockets is unavailable
Long Polling Simulated bidirectional Final fallback on restricted networks

On top of the transport, SignalR uses a wire protocol — JSON (the default, via System.Text.Json) or the binary MessagePack protocol — to frame messages. Clients and server exchange messages through a hub, a high-level pipeline that lets server-side code invoke methods on clients and clients invoke methods on the server, without manually managing sockets.

Key Components

  • Hub: derive a class from Microsoft.AspNetCore.SignalR.Hub to define server-side methods callable from clients. Hub<T> provides a typed client interface for compile-time safety.

  • IHubContext<T>: inject this into controllers, services or background services to push messages to clients from outside the hub itself.

  • Clients: property on Hub exposing targets such as All, Others, Caller, User, and Group.

  • Groups: named collections of connections, used for topics such as a chat room or a stock ticker symbol.

  • ConnectionId / UserId: each connection has an id; authentication tokens identify users so messages can be targeted.

Integration with a Web API

SignalR lives in the same ASP.NET Core pipeline as the API:

  1. Add the service with builder.Services.AddSignalR(); add AddMessagePackProtocol() (from the Microsoft.AspNetCore.SignalR.Protocols.MessagePack package) if you need the binary protocol.

  2. Map hubs with app.MapHub<THub>("/hubs/...") in the routing configuration.

  3. Protect hubs with authentication and authorization — app.UseAuthentication() must come before app.UseAuthorization(), and hubs should carry an [Authorize] attribute.

  4. Broadcast from a controller by injecting IHubContext<THub> and calling SendAsync, typically after a POST changes state, so API clients and real-time clients stay consistent.

Code Example

// Hub definition
public class ChatHub : Hub
{
    // Called by clients; server can then push to everyone in the room
    public async Task SendMessage(string room, string user, string text)
    {
        await Clients.Group(room).SendAsync("ReceiveMessage", user, text);
    }

    public override async Task OnConnectedAsync()
    {
        var room = Context.GetHttpContext()!.Request.Query["room"];
        await Groups.AddToGroupAsync(Context.ConnectionId, room!);
        await base.OnConnectedAsync();
    }
}
// Program.cs — registration and endpoint mapping
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSignalR();              // real-time hub infrastructure
builder.Services.AddControllers();          // REST API controllers
builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();

var app = builder.Build();

app.UseAuthentication();                    // must precede UseAuthorization
app.UseAuthorization();
app.MapControllers();
app.MapHub<ChatHub>("/hubs/chat");          // hub endpoint beside API routes

app.Run();
// Controller pushing a notification after a state change
[ApiController]
[Route("api/[controller]")]
public class MessagesController : ControllerBase
{
    private readonly IHubContext<ChatHub> _hub;
    private readonly IMessageStore _store;   // app-defined persistence service

    public MessagesController(IHubContext<ChatHub> hub, IMessageStore store)
    {
        _hub = hub;
        _store = store;
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<Message>> Get(int id)
    {
        var message = await _store.GetAsync(id);
        return message is null ? NotFound() : Ok(message);
    }

    [HttpPost]
    public async Task<ActionResult> Post(MessageDto dto)
    {
        var saved = await _store.AddAsync(dto);          // persist first

        await _hub.Clients.Group(dto.Room)               // then notify clients
            .SendAsync("ReceiveMessage", dto.User, dto.Text);

        return CreatedAtAction(nameof(Get), new { id = saved.Id }, saved);
    }
}

Scaling and Best Practices

  • Scale-out: a single server holds connection state in memory. With multiple instances, use the Azure SignalR Service (the recommended approach) or a Redis backplane (the Microsoft.AspNetCore.SignalR.StackExchangeRedis package) so messages reach clients connected to other nodes.

  • Sticky sessions: required when scaling out with a Redis backplane and WebSockets, so a client’s transport stays on one server; Azure SignalR Service handles routing itself, so sticky sessions are not needed there.

  • Authentication: validate tokens on the hub; for browser clients send credentials via cookies or an access token in the query string over HTTPS.

  • CORS: configure CORS explicitly for browser clients and never combine AllowAnyOrigin with AllowCredentials.

  • Common pitfalls: broadcasting before the data is committed can mislead clients; pushing to Clients.All instead of a group leaks data to unrelated users; long-running work inside a hub method blocks the connection thread and should be offloaded to a service.

39

Which techniques are most effective for optimizing the throughput and latency of an ASP.NET Core Web API service?

Optimizing an ASP.NET Core Web API requires minimizing resource contention and network overhead through asynchronous processing, strategic caching, and efficient external communication. The goal is to reduce thread pool starvation and payload size while protecting the service from overload.

Read full answer

Applies toASP.NET Core 8 on .NET 8

Asynchronous Processing

  • Async/await: Every I/O-bound operation—database access, file I/O, and downstream HTTP calls—must use async and await so threads return to the pool during waits.

  • Avoid blocking: Blocking on tasks with .Result, .Wait(), or .GetAwaiter().GetResult() causes thread pool starvation and collapses throughput under load.

  • Return types: Controller actions should return Task<ActionResult<T>> when they await operations; synchronous ActionResult<T> should be reserved for purely in-memory work.

  • Middleware: Middleware and hosted services must also follow the async pattern, because one blocked thread reduces the pool available to all requests.

Caching and Compression

  • Response caching: Register AddResponseCaching() and apply [ResponseCache] to idempotent GET responses. This lets the server or a reverse proxy serve repeated requests without re-executing business logic.

  • Compression: Enable AddResponseCompression() to shrink JSON payloads with Gzip or Brotli. In .NET 8, call UseResponseCompression() before UseRouting() so compression applies to endpoint responses.

  • Distributed caching: Use IDistributedCache with Redis or similar for expensive, shared data so repeated requests avoid recomputation and database load.

Data Access and External Calls

  • Entity Framework Core: Use AsNoTracking() for read-only queries and project directly to DTOs with Select() to avoid transferring unused columns and change-tracking overhead.

  • HTTP clients: Always obtain clients from IHttpClientFactory, preferably as typed clients registered with AddHttpClient<TClient, TImplementation>(). Creating a new HttpClient per request exhausts sockets and degrades latency.

  • Cancellation: Keep all data-access and downstream calls asynchronous and propagate CancellationToken so cancelled requests release resources promptly.

Concurrency and Load Protection

  • Rate limiting: Use the built-in rate-limiting middleware (AddRateLimiter / UseRateLimiter) with policies such as fixed window, sliding window, or token bucket to prevent a few clients from saturating the service.

  • Pagination: Implement pagination (skip/take) with a sensible maximum page size to keep response payloads and query execution time bounded.

  • Timeouts: Set reasonable timeouts on database commands and downstream HTTP calls so failures do not hold threads indefinitely.

Payload and Serialization

  • System.Text.Json: Rely on System.Text.Json and avoid unnecessary indentation or verbose naming in production; options such as PropertyNamingPolicy and DefaultIgnoreCondition reduce payload size.

  • DTOs: Return only the fields the client needs, and prefer 200 OK with small DTOs over full entity graphs.

Observability and Best Practices

  • OpenTelemetry: Use OpenTelemetry metrics and tracing to identify real bottlenecks rather than optimizing guesses.

  • [ApiController]: Keep [ApiController] enabled so invalid models automatically return 400 Problem Details without manual validation code.

  • Middleware ordering: If authentication is used, place UseAuthentication() before UseAuthorization(); this ordering is required and has negligible latency cost.

  • Mixed sync/async: Avoid mixing synchronous and asynchronous code paths, and never return large unbounded collections.

40

What approaches can you use to add caching to an ASP.NET Core Web API, and how are they configured and applied?

Caching in an ASP.NET Core Web API is done at three levels: output caching for complete HTTP responses, cache-response headers for client and CDN caching, and distributed caching via IDistributedCache for application data. Output caching is the primary server-side mechanism in .NET 8, while distributed caching suits data you compute or fetch before building the response.

Read full answer

Applies toASP.NET Core Web API on .NET 8

Output Caching for Full Responses

Output caching stores the serialized response body and headers so identical subsequent requests skip controller execution entirely. It is enabled with AddOutputCache() in the service collection and UseOutputCache() in the middleware pipeline, then applied per-endpoint with the [OutputCache] attribute or a policy name.

  • Base policy: defines the default expiry for all cached endpoints.

  • Named policies: add variations such as a short expiry, VaryByQueryKeys, or VaryByHeader.

  • Varying: cache separately per query key, header, or a custom WithVaryByValue function so user-specific or locale-specific responses do not collide.

Client-Side and CDN Caching with Headers

For caches outside your server (browsers, reverse proxies, CDNs), set Cache-Control, ETag, and Last-Modified headers. The [ResponseCache] attribute emits these headers, but note that the old response-caching middleware was removed in ASP.NET Core 3.0, so in .NET 8 the attribute only sets headers and performs no server-side storage — output caching is its server-side replacement. For conditional requests, compare the incoming If-None-Match header against a computed ETag and return 304 Not Modified when they match.

Distributed Caching for Application Data

When you cache data rather than full responses, use IDistributedCache, typically backed by Redis via the Microsoft.Extensions.Caching.StackExchangeRedis NuGet package. This survives application restarts and scales across multiple instances, unlike IMemoryCache (registered with AddMemoryCache()), which is per-process. The same IDistributedCache pattern works in Minimal APIs — register it in Program.cs and inject it into the route handler.

Approach Best for Shared across instances
Output caching Identical GET responses Not by default (custom IOutputCacheStore required)
Response headers Browser/CDN caching N/A
IDistributedCache Computed data, API results Yes
IMemoryCache Fast, single-instance lookups No

Code Example

// Program.cs
// dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
using Microsoft.AspNetCore.OutputCaching;
using Microsoft.Extensions.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(b => b.Expire(TimeSpan.FromMinutes(10)));
    options.AddPolicy("Short", b => b.Expire(TimeSpan.FromSeconds(30)));
});
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration.GetConnectionString("Redis");
});

var app = builder.Build();
app.UseOutputCache();   // register it early, before UseHttpsRedirection, so redirects are not cached
app.MapControllers();
app.Run();
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OutputCaching;
using Microsoft.Extensions.Caching.Abstractions;

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
}

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private static readonly List<Product> _products =
        new() { new() { Id = 1, Name = "Widget" } };

    private readonly IDistributedCache _cache;

    public ProductsController(IDistributedCache cache) => _cache = cache;

    [HttpGet]
    [OutputCache(PolicyName = "Short")]   // whole response cached for 30 seconds
    public ActionResult<IEnumerable<Product>> Get() => Ok(_products);

    [HttpGet("{id}")]
    public async Task<ActionResult<Product>> GetById(int id)
    {
        var cached = await _cache.GetStringAsync($"product:{id}");
        if (cached is not null)
        {
            var cachedProduct = JsonSerializer.Deserialize<Product>(cached);
            if (cachedProduct is not null) return Ok(cachedProduct);
        }

        var product = _products.FirstOrDefault(p => p.Id == id);
        if (product is null) return NotFound();

        await _cache.SetStringAsync($"product:{id}",
            JsonSerializer.Serialize(product),
            new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
            });
        return Ok(product);
    }
}

Best Practices and Pitfalls

  • Cache only safe methods: apply output caching to GET endpoints; POST, PUT and DELETE mutate state and must not be cached.

  • Vary by authentication and headers when responses differ per user, otherwise one user may receive another’s cached data.

  • Invalidate on writes: expire or remove distributed cache entries when the underlying data changes, or use cache-aside with short expiries.

  • Never cache secrets or personalized data without proper vary rules; output caching stores the full response including headers.

  • Use HTTPS: cached responses containing sensitive data can leak if transported over insecure connections.

  • For development, AddDistributedMemoryCache() avoids needing a Redis server, but switch to Redis before production.

41

How does HttpResponseMessage affect the performance characteristics of a Web API, and when should you use it?

HttpResponseMessage is the low-level HTTP response object in ASP.NET Web API that gives you direct control over the status code, headers, and response body, and its primary performance role is letting you stream content straight to the output stream instead of buffering the entire payload in memory. Used correctly it reduces memory pressure and enables fine-grained caching; used poorly (especially without proper disposal) it causes memory and socket leaks.

Read full answer

Applies toASP.NET Web API 2 on .NET Framework 4.8 (with notes for ASP.NET Core on .NET 8)

How It Works

  • Response envelope: HttpResponseMessage wraps the HTTP status code, response headers, and an HttpContent body. In Web API 2 you return it directly from an ApiController action, often built with the Request.CreateResponse() helper.

  • No action-result abstraction: Returning HttpResponseMessage bypasses the IHttpActionResult abstraction, so you lose the Ok()/NotFound() style helpers and take responsibility for building the response yourself. Content negotiation still runs when you assign an ObjectContent<T>; it is skipped only when you assign raw content such as StreamContent.

  • Formatter behavior: With ObjectContent<T>, Web API runs the configured formatters (JSON/XML). With StreamContent or PushStreamContent you write bytes directly and avoid serialization overhead entirely.

  • Disposable: It implements IDisposable. Web API 2 disposes the response after writing it, but any content you create that wraps a stream or unmanaged resource must be disposed correctly to avoid leaks.

Key Performance Mechanisms

  • Streaming large payloads: With PushStreamContent you write directly to the response stream as data becomes available, so a 2 GB file never lives fully in the managed heap. This is the single biggest performance win HttpResponseMessage offers.

  • Caching headers: You can set Cache-Control, ETag, and Last-Modified precisely, letting clients and CDNs serve cached copies and reducing server load on repeat requests.

  • Avoiding unnecessary negotiation: When the response format is fixed (e.g. a binary download), returning a pre-built ByteArrayContent or stream skips formatter selection.

  • Async compatibility: Actions returning Task<HttpResponseMessage> keep threads free during I/O, improving throughput under load.

Web API 2 vs ASP.NET Core .NET 8

Aspect Web API 2 (.NET Framework 4.8) ASP.NET Core .NET 8
Typical return type HttpResponseMessage or IHttpActionResult ActionResult<T> / IActionResult from ControllerBase
Streaming support PushStreamContent on HttpResponseMessage.Content FileStreamResult or writing to HttpResponse.Body
Problem details Manual HttpResponseMessage construction Problem() / ValidationProblem() with [ApiController]
Outgoing calls new HttpClient() (discouraged) or IHttpClientFactory IHttpClientFactory with named/typed clients only

In ASP.NET Core on .NET 8, HttpResponseMessage is not supported as a controller action return type: MVC has no output formatter for it, so it would be serialized as a plain object with an unintended 200 status. Use ActionResult<T> / IActionResult with the Ok(), CreatedAtAction(), NoContent() and NotFound() helpers instead. In .NET 8, HttpResponseMessage is the type you consume for outgoing HTTP calls made through IHttpClientFactory.

When to Use It

  • Large or unbounded payloads: file downloads, exports, or database blobs that should never be fully materialized in memory.

  • Fixed content types: binary or pre-serialized payloads where content negotiation and formatter selection are wasted work.

  • Header-level control: cache policy, ETag/If-None-Match handling, range requests, or custom headers that the higher-level helpers do not expose.

  • Interoperability with HttpClient: on the client side it is the standard response type for outgoing calls.

Code Example

// ASP.NET Web API 2 (.NET Framework 4.8) - stream a large file without buffering
public async Task<HttpResponseMessage> GetReport(int id)
{
    Stream stream = await _reportService.OpenReportStreamAsync(id);
    var response = Request.CreateResponse(HttpStatusCode.OK);
    response.Content = new StreamContent(stream);
    response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
    response.Content.Headers.ContentDisposition =
        new ContentDispositionHeaderValue("attachment") { FileName = $"report-{id}.pdf" };
    return response; // Web API disposes the response (and the stream) after writing
}

Common Pitfalls

  • Not disposing content: if you create HttpContent that wraps a Stream, ensure it is disposed; a leaked stream pins memory and can exhaust connections.

  • Buffering by accident: loading a large file into a byte[] or StringContent before assigning it defeats the streaming benefit and spikes heap usage.

  • Returning it from an ASP.NET Core controller: this is not supported in .NET 8 (the object is serialized with a 200 status); use ActionResult<T> instead.

  • Blocking on async: always return Task<HttpResponseMessage> and await I/O; calling .Result or .Wait() can cause thread-pool starvation under load.

  • Missing cache headers on large static responses: without Cache-Control or ETag, every client re-downloads the full payload, multiplying server work.

42

What authentication approaches can you use to secure users in a Web API?

Web APIs authenticate callers by validating credentials and issuing proof of identity, with JWT bearer authentication the most common choice for stateless APIs, alongside ASP.NET Core Identity, OAuth 2.0/OpenID Connect, API keys, and certificate-based schemes. The right option depends on who the client is (browser, mobile app, or another service) and whether you need interactive sign-in or machine-to-machine access.

Read full answer

Applies toASP.NET Core Web API on .NET 8

Primary Authentication Schemes

Scheme Best for Key considerations
JWT Bearer Mobile apps, SPAs, service-to-service calls Stateless and scalable; client sends a signed token in the Authorization: Bearer header; configured with the Microsoft.AspNetCore.Authentication.JwtBearer package
ASP.NET Core Identity Applications needing full user management Provides password hashing, lockout, two-factor, and claims; for APIs it usually issues JWT tokens rather than cookies, since cookies require CSRF protection in browsers
OAuth 2.0 / OpenID Connect Delegated sign-in via an external provider Delegates to providers such as Microsoft Entra ID, Auth0, or Duende IdentityServer; OIDC adds an identity layer for sign-in, while OAuth 2.0 alone handles delegated authorization
API Keys Internal or low-risk services Simple secret in a header or query string; easy to implement but weaker because keys are long-lived, hard to revoke per-user, and carry no standard claims
Certificate / Windows auth Intranet or machine-to-machine on Windows Client certificates validate machine or service identity; Windows (Negotiate/Kerberos) suits intranet scenarios on Windows; uncommon for public internet APIs

How JWT Bearer Authentication Works

  1. The client obtains a token from a token endpoint (your API’s login action or an identity provider) by presenting valid credentials.

  2. The API validates the token’s signature, issuer, audience, and expiry on each request using middleware registered in Program.cs.

  3. Valid tokens populate HttpContext.User; the [Authorize] attribute restricts endpoints. Invalid or missing tokens return 401 Unauthorized, while a valid token lacking required claims or roles returns 403 Forbidden, ideally formatted as RFC 9457 Problem Details (application/problem+json).

Best Practices and Common Pitfalls

  • Order matters: UseAuthentication() must run before UseAuthorization(), or [Authorize] sees an empty principal.

  • Always use HTTPS in production; tokens sent over HTTP can be intercepted and reused.

  • Store secrets outside code: put Jwt:Key, Jwt:Issuer, and Jwt:Audience in user secrets, environment variables, or Azure Key Vault, never committed to source control.

  • Keep tokens short-lived (e.g., 15-60 minutes) and use refresh tokens for renewal; long-lived access tokens increase damage if stolen.

  • Validate all standard parameters: issuer, audience, lifetime, and signing key. Skipping any of these weakens security.

  • Do not mix cookie auth for APIs unless the client is a same-site browser app and you add anti-forgery protection; prefer JWT for mobile and third-party clients.

  • Return proper status codes: 401 for missing/invalid tokens, 403 for insufficient claims, and let invalid models return 400 automatically via [ApiController].

43

How do you protect an ASP.NET Core Web API with OAuth2 so that requests are authorized based on tokens issued by an authorization server?

In an OAuth2 architecture the Web API acts as the resource server: it does not issue tokens itself, it validates access tokens sent by clients and authorizes requests with roles, claims or policies. The tokens are obtained from a separate authorization server such as Entra ID, Duende IdentityServer or OpenIddict.

Read full answer

Applies toASP.NET Core Web API on .NET 8 with `Microsoft.AspNetCore.Authentication.JwtBearer` 8.x.

How It Works

  • Authorization server: authenticates the resource owner and issues access tokens (usually JWT bearer tokens) for a given audience and scope.

  • Resource server (your API): validates the token signature, issuer, audience and lifetime, then maps claims to the ClaimsPrincipal.

  • Client: requests a token using an OAuth2 grant type (for machine-to-machine APIs typically client_credentials) and sends it as Authorization: Bearer <token>.

  • The API never sees passwords and never refreshes tokens; it only accepts and validates access tokens.

Configuring the API

Install the bearer package (dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer), then register authentication before authorization in Program.cs: app.UseAuthentication() must come before app.UseAuthorization(). Configure the handler with the authorization server’s authority (its base URL, which exposes the OIDC discovery document) and this API’s audience, and turn off inbound claim mapping so original JWT claim names like sub and scope are preserved. Define policies that require an authenticated user plus specific scope claims, and add Problem Details so errors follow RFC 9457. With [Authorize], the authentication middleware returns 401 Unauthorized for a missing or invalid token, while a user who is authenticated but lacks the required policy gets 403 Forbidden.

Choosing the Authorization Server

  • Entra ID / Azure AD: authority is https://login.microsoftonline.com/{tenant}/v2.0; the audience is your API’s application ID URI, and scopes arrive in the scp claim.

  • Duende IdentityServer or OpenIddict: use these when you must host the authorization server yourself. IdentityServer4 is deprecated for current .NET versions — Duende IdentityServer is its supported successor.

  • Minimal APIs can apply the same policies with RequireAuthorization("Api.Read") and return TypedResults instead of ActionResult<T>; the authentication/authorization configuration is identical.

Code Example

using System;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = builder.Configuration["Jwt:Authority"]; // authorization server base URL
        options.Audience = builder.Configuration["Jwt:Audience"];   // this API's identifier
        options.MapInboundClaims = false; // keep original JWT claim names (e.g. "sub", "scope")
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = builder.Configuration["Jwt:Authority"],
            ValidateAudience = true,
            ValidAudience = builder.Configuration["Jwt:Audience"],
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ClockSkew = TimeSpan.FromSeconds(30)
        };
    });

builder.Services.AddAuthorizationBuilder()
    .AddPolicy("Api.Read", policy =>
        policy.RequireAuthenticatedUser().RequireClaim("scope", "api.read"))
    .AddPolicy("Api.Write", policy =>
        policy.RequireAuthenticatedUser().RequireClaim("scope", "api.write"));

builder.Services.AddProblemDetails(); // RFC 9457 error payloads

var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
{
  "Jwt": {
    "Authority": "https://login.microsoftonline.com/<tenant-id>/v2.0",
    "Audience": "api://my-api"
  }
}
using System.Collections.Generic;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private static readonly List<Product> _products = new();

    [HttpGet]
    [Authorize(Policy = "Api.Read")] // requires the api.read scope
    public ActionResult<IEnumerable<Product>> GetAll() => Ok(_products);

    [HttpPost]
    [Authorize(Policy = "Api.Write")]
    public ActionResult<Product> Create(Product product)
    {
        var created = new Product(_products.Count + 1, product.Name);
        _products.Add(created);
        return CreatedAtAction(nameof(GetAll), new { id = created.Id }, created);
    }

    [HttpGet("{id:int}")]
    [Authorize(Policy = "Api.Read")]
    public ActionResult<Product> GetById(int id) =>
        _products.Find(p => p.Id == id) is { } p ? Ok(p) : NotFound();
}

public record Product(int Id, string Name);

Best Practices and Common Pitfalls

  • Always validate issuer, audience, lifetime and signing key; AddJwtBearer fetches signing keys from the discovery document, but set ValidIssuer/ValidAudience explicitly.

  • Keep ClockSkew small (a few seconds) so expired tokens are actually rejected.

  • Require HTTPS and serve the discovery document over HTTPS so keys cannot be tampered with.

  • Scope claim names and format vary by IdP: Azure AD uses scp, generic OIDC uses scope, and the value is often one space-separated string, so RequireClaim("scope", "api.read") may not match — use RequireAssertion that splits the value when needed.

  • Do not accept any origin with credentials; configure a named CORS policy with an explicit origin list when browsers call the API.

  • Use scopes for authorization, not just roles, and keep write scopes separate from read scopes.

  • Do not create tokens in the API unless it is also the authorization server; mixing the two roles is a common design mistake.

44

Describe how token-based authentication works in an ASP.NET Core Web API, including how tokens are issued, validated, and enforced on endpoints.

Token-based authentication in Web API uses a self-contained JSON Web Token (JWT) that the client obtains once and sends with every request, so the server can verify identity and claims without keeping session state. The API validates the token signature and claims on each call and rejects unauthenticated requests with 401 Unauthorized.

Read full answer

Applies toASP.NET Core Web API on .NET 8 with JWT bearer authentication

How It Works

  • Issue: The client posts credentials (username/password) to a login endpoint. The server validates them against a store such as ASP.NET Core Identity or a database, then signs a JWT containing claims (user id, roles, expiry) with a secret key and returns it as JSON.

  • Send: The client stores the token (securely, e.g. in memory or an httpOnly cookie) and adds it to the Authorization header as Bearer <token> on subsequent calls.

  • Validate: The JWT bearer middleware checks the signature, issuer, audience and expiration on each request. If valid, it builds a ClaimsPrincipal for the request.

  • Enforce: The [Authorize] attribute on controllers or actions blocks requests without a valid token, returning 401.

Code Example

In Program.cs, register authentication before authorization and configure the bearer scheme; validation parameters must match what the issuer signs with.

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
        };
    });

builder.Services.AddAuthorization();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseAuthentication();   // must come before UseAuthorization
app.UseAuthorization();
app.MapControllers();      // registers attribute-routed controllers
app.Run();

The login endpoint validates credentials and returns a signed token; on failure it returns 401, and with [ApiController] invalid model state returns 400 automatically as Problem Details (RFC 9457). Placeholder types and a stubbed validator let the snippet compile.

using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System.Text;

public record LoginRequest(string Username, string Password);
public record TokenResponse(string AccessToken);
public class User { public string Username { get; set; } = string.Empty; }
public class Order { public int Id { get; set; } }

[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
    private readonly IConfiguration _config;

    public AuthController(IConfiguration config) => _config = config;

    [HttpPost("login")]
    public async Task<ActionResult<TokenResponse>> Login(LoginRequest request)
    {
        var user = await ValidateUserAsync(request.Username, request.Password);
        if (user is null)
            return Unauthorized();

        var token = GenerateJwtToken(user);
        return Ok(new TokenResponse(token));
    }

    // Replace with real credential validation, e.g. ASP.NET Core Identity.
    private Task<User?> ValidateUserAsync(string username, string password)
        => Task.FromResult<User?>(new User { Username = username });

    private string GenerateJwtToken(User user)
    {
        var key = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
        var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
        var token = new JwtSecurityToken(
            issuer: _config["Jwt:Issuer"],
            audience: _config["Jwt:Audience"],
            claims: new[] { new Claim(ClaimTypes.Name, user.Username) },
            expires: DateTime.UtcNow.AddHours(1),
            signingCredentials: credentials);
        return new JwtSecurityTokenHandler().WriteToken(token);
    }

    [Authorize]
    [HttpGet("{id:int}")]
    public ActionResult<Order> GetOrder(int id) => Ok(new Order { Id = id });
}

Protecting Endpoints

Clients send the token in the Authorization header:

GET /api/orders/5
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Apply [Authorize] to controllers or actions; role-based access uses [Authorize(Roles = "Admin")]. Minimal APIs are the alternative to controllers in .NET 8 - protect routes with RequireAuthorization() instead of the attribute, and keep the two styles separate.

Best Practices and Pitfalls

  • Never put secrets in code: read Jwt:Key, issuer and audience from configuration or a secrets manager; the key must be at least 256 bits for HS256.

  • Use HTTPS everywhere; tokens in transit are bearer credentials anyone can reuse.

  • Short-lived access tokens (minutes to an hour) plus refresh tokens reduce theft impact.

  • Use UseAuthentication before UseAuthorization; reversing them means authorization never sees the user.

  • Prefer ASP.NET Core Identity for user storage instead of rolling your own credential checks.

  • Return 401 for missing/invalid tokens and 403 for insufficient claims, not 404 or 500.

  • Do not log tokens; they are sensitive credentials.

45

What are the ways an application can call and use the endpoints exposed by a Web API?

A client consumes a Web API by sending HTTP requests to its published endpoints and processing the HTTP responses, usually working with JSON payloads. Any device or program that can speak HTTP can be a client: a browser single-page app, a mobile app, a desktop application, or another backend service.

Read full answer

Applies toASP.NET Core Web API on .NET 8 (server side); .NET 8 clients using `IHttpClientFactory`.

Client Options

  • Browser/SPA apps: JavaScript’s fetch or a library such as Axios calls the API directly over HTTPS.

  • Mobile apps: platform HTTP clients (URLSession on iOS, OkHttp/Retrofit on Android, HttpClient in .NET MAUI) call the API, typically with a bearer token from the platform’s secure storage.

  • Other .NET services: use HttpClient resolved through IHttpClientFactory — never a new HttpClient() per request, which can exhaust sockets and ignore DNS changes.

  • Third-party tools: Postman, curl, or the .NET HTTP REPL are useful for manual testing during development.

How It Works

  1. The client locates the API base address (from configuration, service discovery, or an OpenAPI-generated client).

  2. It builds an HttpRequestMessage with a method (GET, POST, PUT, DELETE), URI, headers (Authorization, Accept), and an optional JSON body.

  3. The API returns a status code: 200 OK, 201 Created with a Location header, 204 No Content, 400 Bad Request with problem details, 401/403, 404, 409, or 500.

  4. The client deserializes the JSON response body into strongly typed objects and handles errors explicitly.

.NET Client with IHttpClientFactory

Register a named client with a base address and default headers, then inject IHttpClientFactory where needed. This pools HttpMessageHandler instances, avoiding socket exhaustion and applying Polly policies (retries, circuit breakers) when configured. You can also define a typed client by deriving from HttpClient and registering it with AddHttpClient<T>, which gives you compile-time safety and easier dependency injection.

Best Practices

  • Always use HTTPS and validate the API’s TLS certificate; never disable certificate validation in production.

  • Send the Authorization header with a JWT bearer token obtained from a trusted identity provider; store tokens in secure storage on the client, never in plain-text config.

  • Treat problem details as the error contract: read application/problem+json (RFC 9457) from error responses for machine-readable details.

  • Honor idempotency: retry safe methods (GET, PUT, DELETE) on transient failures; avoid blind POST retries or add an idempotency key.

  • Respect rate limits: back off when the API returns 429 Too Many Requests, using the Retry-After header.

  • Use async end-to-end and pass a CancellationToken so callers can cancel in-flight requests.

  • Consider generating clients from OpenAPI (e.g., kiota or other OpenAPI client generators) to keep client models in sync with the API.

47

Explain the concept of Swagger and describe the procedure for integrating it into an ASP.NET Core Web API solution.

Swagger is a framework for describing, producing, consuming, and visualizing RESTful web services, typically implemented in ASP.NET Core via the Swashbuckle library to generate OpenAPI specifications and an interactive UI. It enables developers to document endpoints, test HTTP requests, and inspect data schemas without leaving the browser.

Read full answer

Applies toASP.NET Core Web API on .NET 8 (Swashbuckle.AspNetCore); note that .NET 9 replaces this with Microsoft.AspNetCore.OpenApi.

Understanding Swagger and OpenAPI

  • OpenAPI Specification: The language-agnostic standard describing REST APIs in JSON or YAML, formerly known as the Swagger Specification.

  • Swagger UI: The interactive interface generated from the OpenAPI document that allows users to try out API endpoints directly.

  • Swashbuckle: The specific NuGet package for ASP.NET Core that inspects controllers and attributes to generate the OpenAPI document automatically.

Integration Steps in .NET 8

  1. Add the Swashbuckle.AspNetCore package to the project via NuGet.

  2. Register the Swagger generator in the dependency injection container using AddSwaggerGen.

  3. Add the middleware to the HTTP pipeline with UseSwagger and UseSwaggerUI.

  4. Access the documentation at the default /swagger route after launching the application.

Code Example

using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
// Register Swagger generator with version info
builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });
});

var app = builder.Build();

// Enable Swagger only in development for security
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseAuthorization();
app.MapControllers();
app.Run();

Security and Version Considerations

  • Production Security: Restrict Swagger to development environments or protect it with authentication to prevent exposing API structure publicly.

  • JWT Support: Configure Swagger UI to send bearer tokens by adding a security scheme in AddSwaggerGen and enabling it in UseSwaggerUI.

  • .NET 9 Change: In .NET 9, Microsoft introduced Microsoft.AspNetCore.OpenApi using AddOpenApi() and MapOpenApi(), replacing Swashbuckle in default templates.

48

What are the typical reasons an ASP.NET Web API endpoint returns a 404 Not Found status, and how do you troubleshoot and resolve them?

The most frequent cause of a 404 Not Found response in an ASP.NET Core Web API is a routing mismatch: the requested URL and HTTP verb do not map to any controller action or minimal API endpoint. A second, deliberately different cause is an application-level 404, where routing succeeds but the action returns NotFound() because the resource identified by the route data does not exist. Troubleshooting means isolating which of the two you are dealing with, then checking the route template, the verb, the middleware pipeline, and finally the data lookup.

Read full answer

Applies toASP.NET Core Web API on .NET 8

Common Causes

A 404 is produced by the routing system when no endpoint matches the request path and method, or by an action that intentionally returns 404. Typical scenarios include:

  • Wrong URL path: Typos in the request or in the route template, such as requesting /api/values when the template is /api/products.

  • Missing or wrong route attributes: An action without [HttpGet], [HttpPost] or a [Route] template when attribute routing is used, or a mismatch between the template’s literal segments and the request.

  • HTTP verb mismatch: Sending GET to an action decorated only with [HttpPost]. With endpoint routing this normally produces 405 Method Not Allowed, not 404, which is a useful diagnostic signal.

  • Controllers not mapped: app.MapControllers() is missing from Program.cs, so no controller route is registered at all.

  • Resource not found in data: The action executes, looks up the entity by id, and returns NotFound() when it is absent — a correct, expected 404, not a bug.

  • Hosting and proxies: A reverse proxy or PathBase misconfiguration strips or rewrites the prefix, so the app receives a path that matches nothing.

Symptom Likely cause Resolution
Every route returns 404 MapControllers() missing, or routing/endpoint middleware absent Register endpoints and keep UseRouting() before authorization and mapping
One route returns 404, others work Template typo, missing attribute, or wrong verb Compare the template with the request; check for a 405 instead
404 only for a specific id Action returns NotFound() for a missing entity Confirm the data exists; decide whether 404 is the correct semantic
404 only behind a proxy Stripped path prefix or wrong PathBase Align the proxy headers and forwarded path

Troubleshooting Steps

  1. Reproduce the exact request: capture method, path, query string and any proxy hops, so you compare against the real template rather than an assumed one.

  2. Inspect the registered endpoints: in .NET 8 the Swashbuckle-based OpenAPI/Swagger UI generated by AddSwaggerGen and UseSwagger lists every route and verb; a route missing there is not registered.

  3. Check the route template: verify the controller’s [Route("api/[controller]")] prefix and the action’s [HttpGet("{id}")] segment, including constraints such as {id:int}.

  4. Distinguish routing 404 from application 404: set a breakpoint or add logging in the action; if it is hit, the 404 comes from your own NotFound() call, not from routing.

  5. Enable routing diagnostics: raise the log level for Microsoft.AspNetCore.Routing in development to see endpoint-matching failure messages.

  6. Verify the pipeline order: UseRouting() must run before UseAuthorization() and endpoint mapping, and UseAuthentication() must precede UseAuthorization().

Middleware and Pipeline Checks

The order of middleware in Program.cs determines whether a valid request ever reaches an endpoint.

  • Endpoint registration: app.MapControllers() is required for controller-based APIs; minimal APIs register endpoints directly with app.MapGet, app.MapPost and friends.

  • Routing first: with explicit UseRouting(), it must come before authorization and mapping so that endpoint metadata (including authorization data) is available.

  • Fallback handling: to return RFC 9457 Problem Details (application/problem+json) for genuinely unmatched routes, configure AddProblemDetails() and a fallback endpoint that returns Problem(), rather than leaving a bare empty 404 body.

Best Practices and Pitfalls

  • Do not use 404 for validation failures: with [ApiController], invalid model state automatically returns 400 Bad Request; reserve 404 for “no such endpoint” or “no such resource”.

  • Return 404 deliberately, not accidentally: look up the entity and return NotFound() when it is missing, and 200 with the payload when it exists; keep that separate from routing failures.

  • Prefer attribute routing for APIs: [Route] and verb attributes give explicit, discoverable templates; conventional MapControllerRoute is mainly for MVC views.

  • Watch casing in templates: routing matches case-insensitively, but the [controller] token preserves the class name’s casing in the generated URL, so prefer consistent lowercase route literals for predictable generated links.

  • Check the verb before blaming the path: a 405 points straight at a verb mismatch, while a 404 points at the path or at endpoint registration.

49

What approaches and configuration steps should be adopted when implementing versioning for an ASP.NET Core Web API to ensure clients remain compatible during updates?

API versioning lets a service evolve without breaking existing clients by isolating changes behind explicit version identifiers. The recommended approach is to combine an explicit version-selection strategy with metadata reporting so consumers always know which versions exist and which are retiring. API Versioning in ASP.NET Core is configured with the official successor packages, Asp.Versioning.Mvc (and Asp.Versioning.Mvc.ApiExplorer for OpenAPI metadata), which replace the deprecated Microsoft.AspNetCore.Mvc.Versioning package.

Read full answer

Applies toASP.NET Core Web API on .NET 8 using Asp.Versioning.Mvc 6.x and Asp.Versioning.Mvc.ApiExplorer 6.x, with Swashbuckle (Swashbuckle.AspNetCore) for OpenAPI.

Versioning Strategies

The version identifier can be carried in three standard places; the choice depends on client constraints and URL preferences. - URI Path: the version is part of the route, e.g. /api/v1/products. Explicit, cache-friendly, and easy for clients and proxies to reason about, but it changes the URL structure between versions. - Query String: the version is appended as ?api-version=1.0. This is the default reader in Asp.Versioning.Mvc, so it works with no extra configuration, but it clutters URLs and can be stripped or logged inadvertently. - Custom Header: the version is sent in a header such as api-version: 1.0. It keeps URLs clean but requires clients to set headers and complicates ad-hoc browser testing. URI path versioning is generally preferred for public APIs because it is visible, self-describing, and plays well with caching and routing tools; query string and header versioning suit internal or legacy clients that cannot change their URLs.

Configuration and Setup

Configuration is a four-step process: register the versioning services, register the API explorer, apply version attributes to controllers, and enable the middleware/UI integration. 1. Add Asp.Versioning.Mvc and Asp.Versioning.Mvc.ApiExplorer from NuGet. 2. Call AddApiVersioning before AddControllers/AddMvc, because the versioning conventions must be applied to the controller application model while it is being built. 3. Set a default version, decide whether an unspecified version resolves to it, and enable version reporting so response headers advertise supported and deprecated versions. 4. Decorate controllers with [ApiVersion] and include {version:apiVersion} in the route template when using URI path versioning.

OpenAPI Integration

Swagger must expose one document per version instead of merging every endpoint into a single list. In .NET 8 the template uses Swashbuckle (AddSwaggerGen/UseSwagger); from .NET 9 the built-in Microsoft.AspNetCore.OpenApi (AddOpenApi/MapOpenApi) replaces it. With Swashbuckle, inject IApiVersionDescriptionProvider, call SwaggerDoc for each ApiVersionDescription (using its GroupName and ApiVersion), and register one SwaggerEndpoint per group in UseSwaggerUI.

Code Example

using Asp.Versioning;
using Asp.Versioning.ApiExplorer;
using Microsoft.AspNetCore.Mvc;
using Microsoft.OpenApi.Models;
using System.Collections.Generic;

// Program.cs - register before AddControllers
builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true; // emits api-supported-versions / api-deprecated-versions
});
builder.Services.AddVersioningApiExplorer();
builder.Services.AddSwaggerGen();

// ProductsController.cs
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("1.0")]
[ApiVersion("2.0", Deprecated = true)]
public class ProductsController : ControllerBase
{
    [HttpGet]
    public ActionResult<IEnumerable<string>> Get() => Ok(new List<string> { "item1" });
}

Best Practices and Common Pitfalls

  • Deprecation Policy: mark retiring versions with [ApiVersion("1.0", Deprecated = true)] and publish sunset dates so clients can migrate on a schedule.

  • Breaking Changes: never alter an existing version’s contract semantics; ship breaking changes only as a new version and keep the old one working during the deprecation window.

  • Response Headers: keep ReportApiVersions enabled so clients receive api-supported-versions and api-deprecated-versions automatically.

  • Avoid Over-Versioning: version public, client-facing boundaries; internal microservices that evolve together usually do not need independent versions.

  • Cross-Cutting Concerns: apply CORS policies, authentication, and authorization consistently across all versions so a new version does not accidentally bypass security.

  • Minimal APIs: the same model applies to Minimal APIs via the Asp.Versioning.Http package, which adds AddApiVersioning and version-aware MapGet/MapPost overloads; keep the two styles distinct, just as controllers return ActionResult<T> while Minimal APIs use TypedResults.

50

What practices should you follow to design a Web API that adheres to REST architectural principles?

Making a Web API RESTful means modeling it around resources, using standard HTTP semantics correctly, and returning consistent, self-describing responses so clients can interact with it using only the HTTP protocol as intended.

Read full answer

Applies toASP.NET Core Web API on .NET 8

Resource-Oriented Design

  • URLs identify resources (nouns), not actions: /api/products/123, not /api/getProduct.

  • Use path segments for hierarchy: /api/orders/456/items.

  • Let HTTP verbs express the operation: GET retrieves, POST creates, PUT replaces, PATCH partially updates, DELETE removes.

  • GET, PUT and DELETE are idempotent; POST is not, so never use POST for reads or GET for writes.

Status Codes and Response Semantics

  • 200 OK for successful GET/PUT/PATCH returning a body.

  • 201 Created for POST that creates a resource, with a Location header pointing to the new resource.

  • 204 No Content for successful DELETE or updates that return nothing.

  • 400 Bad Request for invalid input (returned automatically by [ApiController] model validation).

  • 401 Unauthorized when unauthenticated, 403 Forbidden when authenticated but not allowed.

  • 404 Not Found when a resource does not exist, 409 Conflict for concurrency or uniqueness violations, 500 Internal Server Error for unexpected failures.

Error Responses with Problem Details

  • Return errors as application/problem+json per RFC 9457 (which obsoletes RFC 7807) by calling AddProblemDetails() in DI and using Problem() or ValidationProblem() in controllers.

  • Include type, title, status, detail and instance; add errors for validation failures. With [ApiController], an invalid model automatically returns 400 as ValidationProblemDetails.

Supporting Practices

  • Versioning: use the Asp.Versioning.Mvc package; Microsoft.AspNetCore.Mvc.Versioning is deprecated.

  • OpenAPI: .NET 8 templates use Swashbuckle (AddSwaggerGen/UseSwagger); from .NET 9 the built-in Microsoft.AspNetCore.OpenApi (AddOpenApi/MapOpenApi) replaces it.

  • Security: JWT bearer or ASP.NET Core Identity, with UseAuthentication before UseAuthorization; configure CORS explicitly and never combine AllowAnyOrigin with credentials.

  • Rate limiting: use the built-in middleware (AddRateLimiter, .NET 7+).

  • Calling other services: resolve IHttpClientFactory and use named or typed clients; never create new HttpClient() per request, and use async all the way.

Common Pitfalls

  • Returning 200 for errors or 500 for client mistakes, which breaks client error handling.

  • Using verbs in URLs or a single POST endpoint for every operation.

  • Creating new HttpClient() per request, which exhausts sockets under load.

  • Returning plain-text errors instead of Problem Details, making errors hard to parse.

  • Allowing any origin while sending credentials over CORS, which browsers reject.

51

Which security vulnerabilities and risks should you keep in mind while building a Web API?

The most common Web API security issues fall into a handful of categories: broken or missing authentication and authorization, unencrypted transport, unvalidated input, excessive trust of clients, poor secrets management, and no protection against abuse. ASP.NET Core gives you built-in middleware for most of these, but the defaults only help if you actually configure and order them correctly.

Read full answer

Applies toASP.NET Core Web API on .NET 8. Most guidance also applies to other ASP.NET Core versions; classic ASP.NET Web API 2 on .NET Framework 4.8 uses different tooling and is not covered here.

Authentication and Authorization

  • Missing or weak authentication: Endpoints with no authentication requirement are open to the internet. Apply AddAuthentication().AddJwtBearer(...) and AddAuthorization(), and place UseAuthentication() before UseAuthorization() in the pipeline — the order matters, because authorization needs an authenticated ClaimsPrincipal.

  • Confused authorization logic: Role checks hard-coded in action bodies drift out of sync with policy requirements. Prefer [Authorize(Policy = "...")] with named policies defined in AddAuthorization, so access rules live in one place.

  • Over-posting / mass assignment: Model binding can populate properties the client should not control. Use [BindNever] on sensitive properties or bind to a dedicated DTO rather than your entity directly.

  • Insecure token handling: Accept tokens only over HTTPS, validate the issuer, audience, lifetime, and signature, and never store long-lived secrets in the browser.

Transport and Cross-Origin Security

  • Plain HTTP: Without TLS, credentials, tokens, and personal data are visible on the wire. Require HTTPS, and in production enable HSTS (UseHsts) plus HTTP-to-HTTPS redirection (UseHttpsRedirection).

  • Mixed or expired certificates: A valid certificate is part of the trust chain; expired or self-signed certs in production break clients and can silently downgrade trust.

  • Misconfigured CORS: Configure CORS explicitly with a named policy, and never combine AllowAnyOrigin() with AllowCredentials() — that combination is explicitly disallowed by the browser and signals a broken design.

Input Validation and Injection

  • Unvalidated input: Never trust query strings, route values, or bodies. With [ApiController], invalid models automatically return a 400 with Problem Details (application/problem+json, RFC 9457), so annotate DTOs with validation attributes and let the pipeline enforce them.

  • SQL injection: Always use parameterized queries or an ORM such as EF Core. Never concatenate user input into raw SQL.

  • Overly permissive model binding: Bind only the properties you need, and reject unexpected fields rather than silently ignoring them.

Business Logic and Abuse

  • No rate limiting: A login or password-reset endpoint without throttling invites brute force and credential stuffing. Use the built-in AddRateLimiter middleware (.NET 7+) with per-endpoint or per-user limits.

  • Insecure direct object references (IDOR): Just because a request is authenticated does not mean the caller may read or write any record. Authorize against the resource owner on the server, e.g. verify the order belongs to the current user before returning it.

  • Missing audit trails: Security incidents are impossible to investigate without logs. Log authentication failures, authorization denials, and state-changing operations — but never log secrets or tokens.

Secrets, Configuration and Best Practices

  • Hard-coded secrets: Connection strings, API keys, and JWT signing keys belong in the secret store (User Secrets locally, Azure Key Vault or an equivalent in production), never in source control.

  • Weak signing keys: Generate a long, random key for JWT signing; do not reuse a short or shared secret across services.

  • Fail closed: When authorization data is missing or ambiguous, deny access rather than allowing it.

  • Return generic errors to clients: Use Problem Details for consistent 400/401/403/404/409/500 responses, and keep stack traces and internal details in server logs only.

  • Keep packages current: Vulnerable dependencies are a leading cause of breaches; scan and update NuGet packages regularly.

  • Test the negative paths: Verify that forbidden requests return 401 or 403, not the data.

Threat Primary mitigation in ASP.NET Core
Unauthenticated access [Authorize] + JWT bearer or Identity
SQL injection Parameterized queries / EF Core
Brute force AddRateLimiter
Data in transit HTTPS, HSTS, UseHttpsRedirection
Mass assignment DTOs, [BindNever]
Secrets in code User Secrets, Key Vault
52

What are the scenarios where ASP.NET Web API is the right choice compared to alternative frameworks?

Choose ASP.NET Core Web API when you need to build production-grade, HTTP-based REST services on .NET that must run cross-platform, integrate with the broader ASP.NET Core stack (authentication, authorization, dependency injection, logging, rate limiting), and expose well-documented, versioned APIs for a variety of clients — web, mobile, and third-party consumers.

Read full answer

Applies toASP.NET Core 8.0 Web API (controller-based, `[ApiController]` + `ControllerBase`); notes on Minimal APIs and .NET 9 are called out separately.

Strengths That Drive the Choice

  • Mature HTTP and REST semantics: first-class support for verbs, content negotiation, status codes (200 OK, 201 Created with a Location header, 204 No Content, 400/404/409/500), and RFC 9457 Problem Details via AddProblemDetails() and Problem() / ValidationProblem().

  • Cross-platform hosting: runs on Windows, Linux, and macOS in Kestrel, IIS, or containers — unlike classic ASP.NET Web API 2, which is tied to .NET Framework 4.8 and Windows/IIS.

  • Strong typing and tooling: controllers returning ActionResult<T>, automatic model validation with [ApiController], and OpenAPI generation (Swashbuckle AddSwaggerGen/UseSwagger in the .NET 8 template; the built-in Microsoft.AspNetCore.OpenApi package with AddOpenApi/MapOpenApi from .NET 9).

  • Unified security stack: JWT bearer authentication, ASP.NET Core Identity, CORS, and the built-in rate limiter (AddRateLimiter, .NET 7+) compose into the same pipeline, with UseAuthentication before UseAuthorization.

  • Performance and ecosystem: high-throughput, low-allocation runtime, plus NuGet libraries for API versioning (Asp.Versioning.Mvc — the replacement for the deprecated Microsoft.AspNetCore.Mvc.Versioning), Entity Framework Core, IHttpClientFactory for outbound calls, and gRPC endpoints side-by-side.

When It Fits Best

  • Public or partner APIs consumed by browsers and mobile apps, where REST + JSON, OpenAPI docs, and explicit versioning matter.

  • Backends for modern front ends (SPA, Blazor, Xamarin/.NET MAUI, native iOS/Android) that need token-based auth and clean resource-oriented endpoints.

  • Microservices and containerized workloads on Linux/Kubernetes, where cross-platform, small footprint images, and cloud-native telemetry are required.

  • Complex, policy-rich services where you want full control over filters, model binding, action results, and middleware — areas where controller-based APIs shine.

When to Prefer an Alternative

Need Better fit Why
Lightweight CRUD service, minimal ceremony Minimal APIs (with TypedResults) Same framework, less code; keep the two distinct — controllers still suit large/complex APIs
Low-latency, contract-first, polyglot RPC gRPC on ASP.NET Core Binary Protobuf, strict schema, bidirectional streaming
Real-time push (chat, live dashboards) ASP.NET Core SignalR Optimized for server-to-client streaming and reconnection
SOAP / WS-* interop with legacy systems WCF on .NET Framework 4.8 (legacy) or CoreWCF for migration WCF is not available on modern .NET; treat as deprecated
Event-driven, scale-to-zero, pay-per-execution Azure Functions Serverless billing and automatic scaling
Team already standardized on another stack Node.js (NestJS/Express), Spring Boot, FastAPI Ecosystem and team skills outweigh framework merits

Decision Guidance

  • If the consumer is a browser or mobile app speaking JSON over HTTPS, ASP.NET Core Web API is the default recommendation on .NET.

  • If the workload is internal, RPC-style, and latency-critical, evaluate gRPC first — you can host both in the same app.

  • If the service is tiny and stable, start with Minimal APIs; migrate to controllers if cross-cutting concerns grow.

  • If you are maintaining an existing Web API 2 solution on .NET Framework 4.8, plan a migration to ASP.NET Core rather than new WCF/SOAP development.

Common Pitfalls

  • Mixing generations: classic Web API 2 (WebApiConfig, IHttpActionResult, Global.asax) cannot be combined with ASP.NET Core 8 patterns; migration is a rewrite of the hosting layer.

  • Ignoring idempotency: treat GET, PUT, and DELETE as idempotent and reserve POST for non-idempotent creation.

  • Weak CORS: never allow all origins together with credentials; configure explicit allowed origins.

  • Blocking calls: use async/await end to end and IHttpClientFactory — never instantiate HttpClient per request.

53

What design approaches would you use to build an ASP.NET Core Web API that handles high and growing load?

A scalable ASP.NET Core Web API is designed around stateless, async, horizontally scalable request handling, where each server can be added or removed without breaking clients, no thread is blocked waiting on I/O, and expensive work is pushed out of the request path through caching, queues, and background processing.

Read full answer

Applies toASP.NET Core Web API on .NET 8 (controller-based APIs with `[ApiController]`; Minimal APIs as an alternative).

Scale Out and Keep Requests Non-Blocking

  • Keep servers stateless: store session or user state in a distributed store (Redis, a database, or tokens issued to the client) rather than in-process memory or HttpContext.Session. Any instance can then serve any request, which is what load balancers and container orchestrators need to add or remove replicas.

  • Prefer externalizing all shared state: distributed cache, distributed rate-limit counters, and a shared data store must live outside the process if you scale out.

  • Avoid sticky sessions: they limit a load balancer’s ability to redistribute traffic and make rolling deployments slower.

  • Use async/await end to end and never call .Result or .Wait() on a Task, which can deadlock or exhaust the thread pool.

  • Call downstream HTTP services through IHttpClientFactory (named or typed clients) so sockets are reused; creating a new HttpClient() per request causes socket exhaustion under load.

  • Offload long-running work to a queue plus a background service (IHostedService/BackgroundService, or a real message broker such as Azure Service Bus, RabbitMQ, or Kafka) and return 202 Accepted immediately.

Reduce the Work per Request

  • Cache aggressively: IMemoryCache for single-instance data, IDistributedCache (Redis) for shared data, and response/output caching for expensive GET responses.

  • Paginate and project: return only the fields and rows clients need; never return an unbounded collection from a GET endpoint.

  • Compress responses: built-in response compression middleware reduces bandwidth for JSON payloads.

  • Use efficient serialization: System.Text.Json is the default and is faster than Newtonsoft.Json for most workloads.

Protect the API Under Load

  • Apply the built-in rate limiting middleware (AddRateLimiter, .NET 7+) with sensible per-client or per-user policies, and return 429 Too Many Requests with a Retry-After header.

  • Use concurrency limiting (e.g. a SemaphoreSlim or a queue-based pattern) for endpoints that hit scarce resources.

  • Configure CORS explicitly; never combine AllowAnyOrigin with AllowCredentials.

  • Secure endpoints with JWT bearer authentication or ASP.NET Core Identity, and order the pipeline UseAuthentication() before UseAuthorization().

Scale the Data Tier

The API is usually only as scalable as its data store, so treat database scaling as part of the design.

Technique When to use Trade-off
Read replicas Read-heavy workloads Replication lag; route writes to primary
Caching query results Same data requested repeatedly Cache invalidation complexity
CQRS / materialized views Complex read models More infrastructure and consistency work
Sharding or partitioning Very large write volume Cross-shard queries are hard
Async queue writes Bursty writes, non-critical path Eventual consistency

Keep the Contract Stable and Observable

  • Version the API with the Asp.Versioning.Mvc package (Microsoft.AspNetCore.Mvc.Versioning is deprecated); URI or header versioning keeps changes from breaking existing clients.

  • Return errors as Problem Details (application/problem+json, RFC 9457) via AddProblemDetails() and Problem()/ValidationProblem(), so clients get a consistent, machine-readable error shape.

  • Emit structured logs, metrics, and distributed tracing, and define health checks (AddHealthChecks) so the platform can route traffic only to healthy instances.

  • Generate OpenAPI documents (Swashbuckle AddSwaggerGen/UseSwagger in .NET 8; the built-in Microsoft.AspNetCore.OpenApi package from .NET 9) so clients and tooling can evolve with the API.

Best Practices and Common Pitfalls

  • Choose controller-based APIs with [ApiController] and ActionResult<T> for complex, policy-driven APIs; choose Minimal APIs with TypedResults for smaller, low-ceremony services — keep the two styles distinct within a single project.

  • Watch out for hidden state: in-process caches, static collections, and IDistributedCache-less rate limiters all break under scale-out.

  • Load test before assuming a design scales; most bottlenecks are the database or a downstream service, not the web tier.

54

Explain how a microservices architecture is structured when the individual services are built with ASP.NET Core Web API, including how the services communicate and how cross-cutting concerns are handled.

A microservices architecture splits a large application into small, independently deployable services, each built as a separate ASP.NET Core Web API that owns one bounded business domain and its own data store. The services collaborate over lightweight protocols — typically HTTPS/REST for synchronous calls and a message broker for asynchronous events — and are fronted by an API gateway that presents a single entry point to client applications.

Read full answer

Applies toASP.NET Core Web API on .NET 8

Core Characteristics

  • Independent deployability: each service has its own CI/CD pipeline and can be released without redeploying the rest of the system.

  • Bounded contexts: a service models one business capability (e.g. Ordering, Catalog, Identity) and exposes only the APIs that capability needs.

  • Decentralized data: each service owns its database; there is no shared schema. Services exchange data through APIs or events, never through cross-service database joins.

  • Polyglot technology: because services communicate over standard protocols, individual teams can choose the store or runtime that fits their service.

  • Elasticity and resilience: instances can be scaled horizontally per service, and failures are contained by retries, circuit breakers and timeouts.

Service Design with ASP.NET Core Web API

Each service is a standard Web API project using the controller style with [ApiController] and ControllerBase, returning ActionResult<T> or IActionResult. Minimal APIs with TypedResults are a valid lighter alternative for small services, but the two styles should not be mixed within one service.

  • HTTP semantics: GET, PUT and DELETE are idempotent; POST creates resources and is not idempotent. A successful creation returns 201 Created with a Location header; idempotent deletes or updates return 200 OK or 204 No Content.

  • Error contracts: errors are returned as Problem Details (application/problem+json, RFC 9457, which replaces RFC 7807) via AddProblemDetails() and Problem() / ValidationProblem(). With [ApiController], an invalid model automatically produces a 400 with a validation problem response.

  • API versioning: use the Asp.Versioning.Mvc package; Microsoft.AspNetCore.Mvc.Versioning is the deprecated predecessor.

  • API documentation: on .NET 8 the project template uses Swashbuckle (AddSwaggerGen / UseSwagger); from .NET 9 the built-in Microsoft.AspNetCore.OpenApi (AddOpenApi / MapOpenApi) replaces it.

Communication Patterns

Aspect Synchronous (REST/gRPC over HTTPS) Asynchronous (message broker / event bus)
Coupling Caller waits for the callee Fire-and-forget, temporal decoupling
Best for Queries, commands needing immediate feedback Integration events, notifications, background work
Failure mode Caller fails if callee is down Broker buffers messages; caller is unaffected
Consistency Strong, immediate Eventual consistency
  • Outbound HTTP calls go through IHttpClientFactory as named or typed clients — never a new HttpClient() per request — and are fully async to avoid blocking threads.

  • Service-to-service discovery is handled by a gateway, reverse proxy or container orchestrator service registry rather than hard-coded host names.

Cross-Cutting Concerns

  • API gateway: routes external traffic, aggregates responses, terminates TLS and applies shared policies, so individual services stay focused on business logic.

  • Authentication and authorization: JWT bearer tokens issued by an identity service (or ASP.NET Core Identity); the pipeline calls UseAuthentication() before UseAuthorization().

  • CORS: configured explicitly per known origin; never combine AllowAnyOrigin with AllowCredentials.

  • Rate limiting: the built-in AddRateLimiter middleware (.NET 7+) applies per-client or per-endpoint quotas.

  • Observability: structured logging, metrics and distributed tracing with a correlation ID propagated across service hops.

  • Resilience: Polly-based retries and circuit breakers for outbound calls, plus per-service health checks exposed for orchestrator probes.

Advantages and Limitations

Microservices improve team autonomy, independent scaling and technology diversity, but they add operational cost: distributed transactions become sagas or outbox patterns, testing spans multiple deployments, and network latency and partial failures must be designed for from the start. A common pitfall is splitting services too finely (“nanoservices”), which turns a monolith’s in-process calls into expensive remote hops.

Best Practices

  • Model service boundaries on business domains, not database tables or layers.

  • Keep one data store per service and publish integration events for cross-service data changes.

  • Prefer asynchronous events for anything that does not need an immediate response.

  • Version APIs from day one and treat the published contract as a public API.

  • Deploy and monitor each service independently, and automate the full pipeline before adding more services.

55

Can you explain the Repository design pattern and describe its role within an ASP.NET Web API architecture?

The Repository pattern establishes an abstraction layer between application business logic and data access mechanisms, treating persistent storage as a collection of domain objects. In ASP.NET Web API, this pattern decouples controllers from specific database technologies, allowing developers to swap data sources or modify query logic without altering endpoint definitions.

Read full answer

Applies toASP.NET Core 8

Core Concept

The pattern defines a contract, usually an interface like IProductRepository, which declares methods for data operations such as retrieval, insertion, or deletion. Concrete classes implement this contract by executing queries against a database context, typically Entity Framework Core. This separation ensures that high-level modules (controllers) do not depend on low-level modules (database details), adhering to the Dependency Inversion Principle and facilitating cleaner architecture.

Dependency Integration

ASP.NET Core includes a built-in Dependency Injection container configured in Program.cs. Repositories are registered as scoped services, meaning one instance is created per HTTP request. This aligns perfectly with the DbContext lifetime, preventing captive dependency issues where a transient service holds a reference to a scoped context. The framework automatically injects the registered implementation into any controller constructor requesting the interface.

Controller Implementation

Controllers inherit from ControllerBase and are marked with [ApiController] to enable automatic model validation and attribute routing requirements. Actions accept the repository interface via constructor injection and return ActionResult<T>. This return type allows precise HTTP status codes, such as returning NotFound() when a record is missing or Ok() with the entity when successful, ensuring clients receive semantically correct responses.

Architecture Considerations

While beneficial, introducing a repository layer over Entity Framework Core can sometimes add unnecessary indirection, since DbContext already implements Unit of Work and Repository behaviors. Custom repositories are most valuable when encapsulating complex queries, sharing data logic across multiple services, or abstracting non-relational data sources. For simple CRUD scenarios, Minimal APIs with direct DbContext access might reduce boilerplate while maintaining clarity.

Code Example

public interface IProductRepository
{
    Task<Product?> GetByIdAsync(int id, CancellationToken ct);
    Task AddAsync(Product product, CancellationToken ct);
}

public class ProductRepository : IProductRepository
{
    private readonly AppDbContext _context;
    public ProductRepository(AppDbContext context) => _context = context;

    public async Task<Product?> GetByIdAsync(int id, CancellationToken ct)
        => await _context.Products.FindAsync(id, ct);

    public async Task AddAsync(Product product, CancellationToken ct)
    {
        _context.Products.Add(product);
        await _context.SaveChangesAsync(ct);
    }
}

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductRepository _repository;
    public ProductsController(IProductRepository repository) => _repository = repository;

    [HttpGet("{id}")]
    public async Task<ActionResult<Product>> Get(int id)
    {
        var product = await _repository.GetByIdAsync(id, HttpContext.RequestAborted);
        return product is null ? NotFound() : Ok(product);
    }
}

// In Program.cs
builder.Services.AddScoped<IProductRepository, ProductRepository>();

Best Practices and Pitfalls

  • Service Lifetime: Register repositories as Scoped to match DbContext. Transient repositories accessing scoped contexts can cause concurrency and disposal exceptions.

  • Async All the Way: Ensure every method in the chain is async to avoid thread pool starvation under high concurrency.

  • Avoid Anemic Repositories: Do not create repositories that merely pass through single DbContext calls; this adds maintenance cost without architectural benefit.

  • Unit Testing: Mock the repository interface to test controller logic in isolation without requiring a live database connection.

56

Can you explain what OWIN middleware is in the context of ASP.NET Web API and how it participates in the HTTP request pipeline?

OWIN middleware in ASP.NET Web API is a chain of small, composable components that sit between the web server and the Web API framework, each one inspecting or modifying an incoming HTTP request and the outgoing response before handing control to the next component. OWIN (Open Web Interface for .NET) decouples the application from the server by defining a simple delegate contract instead of tying Web API to IIS and System.Web.

Read full answer

Applies toASP.NET Web API 2 on .NET Framework 4.8 (OWIN/Katana is legacy; ASP.NET Core on .NET 8 uses its own middleware model instead)

What OWIN Is

  • Specification: OWIN is an open specification (owin.org) that describes a delegate signature between server and application: Func<IDictionary<string, object>, Task>. The dictionary carries the request, response, headers, and environment state.

  • Katana: Microsoft’s implementation of OWIN is called Katana, shipped as the Microsoft.Owin.* NuGet packages.

  • Middleware: An OWIN middleware component is a function that receives the environment dictionary and a next delegate. It can handle the request itself (short-circuit) or call await next() to pass it down the pipeline.

How Web API Uses the OWIN Pipeline

  • In classic Web API 2, the framework normally runs on the IIS integrated pipeline through System.Web. OWIN hosting lets you run the same HttpConfiguration and routes outside IIS, or insert Web API into a custom OWIN pipeline.

  • You create a Startup class with a Configuration(IAppBuilder app) method, build an HttpConfiguration, register routes with config.Routes.MapHttpRoute, and wire Web API into OWIN with app.UseWebApi(config).

  • The order of app.Use... calls defines the middleware order: authentication and CORS middleware typically run before UseWebApi, so they can reject unauthorized requests before routing reaches a controller.

Typical Middleware Components

  • Authentication: Microsoft.Owin.Security packages (OAuth bearer tokens, cookies) validate tokens and set the user principal before Web API runs.

  • CORS: Microsoft.Owin.Cors enables cross-origin requests with app.UseCors.

  • Error handling: Custom middleware can catch exceptions and write a consistent error response.

  • Logging and diagnostics: Middleware can log request paths, durations, and response status codes.

OWIN Hosting vs System.Web Hosting

Aspect System.Web (IIS) OWIN Self-Host
Server IIS only HttpListener or any OWIN server
Package Web API default templates Microsoft.Owin.SelfHost, Microsoft.Owin.Host.HttpListener
Entry point Global.asax / WebApiConfig Startup.Configuration
Process IIS worker process Console app, Windows service, or container
Status today Legacy Legacy; superseded by ASP.NET Core

Best Practices and Deprecation Notes

  • OWIN and Katana are legacy and no longer actively developed. For new HTTP services, use ASP.NET Core on .NET 8, which has a built-in, higher-performance middleware model (IApplicationBuilder, HttpContext, RequestDelegate) and does not use OWIN.

  • If you maintain an existing OWIN-hosted Web API, keep middleware ordered deliberately: diagnostics and exception handling first, then authentication and CORS, then UseWebApi.

  • When OWIN middleware calls downstream HTTP services, do not create a new HttpClient per request — that exhausts sockets; reuse a single long-lived HttpClient (or a small set of preconfigured instances) and stay async all the way.

  • ASP.NET Core provides Microsoft.AspNetCore.Owin as a compatibility adapter if you must interoperate with OWIN components during migration, but new code should target the ASP.NET Core middleware pipeline.

58

Describe how conventional routing maps incoming HTTP requests to controller actions within an ASP.NET Core Web API application.

Conventional routing maps incoming HTTP requests to controller actions by matching the URL path against globally registered route templates at startup and using the extracted route values to identify the controller, action, and parameters.

Read full answer

Applies toASP.NET Core Web API on .NET 8

How Conventional Routing Works

  • Registration happens in Program.cs through MapControllerRoute, which is an extension on IEndpointRouteBuilder (available on the WebApplication instance built by WebApplication.CreateBuilder).

  • A route template mixes literal segments with parameters in braces, for example api/{controller}/{id?}.

  • The routing middleware evaluates registered templates in the order they were added; the first template whose pattern matches the request path wins.

  • The matched route values are used to locate the controller by appending “Controller” to the {controller} segment, to choose the action method, and to supply action parameters.

  • When the template does not include an {action} segment, the action name is inferred from the controller method name, and the HTTP verb is matched by action selector attributes such as [HttpGet] or [HttpPost]; the verb alone does not pick the action.

Conventional Routing Compared with Attribute Routing

Aspect Conventional Routing Attribute Routing
Definition Global templates registered in Program.cs Paths declared with [Route], [HttpGet], etc., on controllers and actions
Flexibility Limited to the registered patterns Customizable per controller or action
Typical use Internal services, simple CRUD endpoints Public APIs, versioning, complex or hierarchical URLs
Precedence Evaluated in registration order Takes precedence for controllers or actions that have route attributes

Route Constraints and Model Binding

  • Inline constraints such as {id:int} or {slug:alpha} validate a segment before the route is considered a match, preventing invalid values from reaching the action.

  • Constraints also disambiguate overlapping routes, for example separating numeric IDs from string slugs.

  • Model binding automatically converts matching route values to action parameters with the same names, performing type conversion and reducing manual parsing.

  • If a constraint fails, routing continues to the next template rather than invoking the action.

Best Practices and Common Pitfalls

  • Prefer attribute routing for public-facing APIs because it gives explicit control over URIs and works cleanly with API versioning packages such as Asp.Versioning.Mvc.

  • Use conventional routing for internal microservices or simple endpoints where a stable, predictable URL pattern is sufficient.

  • Do not mix conventional and attribute routing inside the same controller unless you understand the precedence rules; attribute routes win.

  • Always apply constraints to route parameters to reject malformed input early.

  • Remember to call builder.Services.AddControllers() so controllers are discovered; if you also use attribute routing, call app.MapControllers() in addition to any MapControllerRoute calls.

  • For very lightweight endpoints, consider Minimal APIs with MapGet/MapPost and TypedResults, which bypass controller routing conventions entirely.

59

In ASP.NET Core Web API, what drawbacks does convention-based routing present, and which strategies can be used to resolve them?

Convention-based routing ties URL segments to controller and action names through a global template, which restricts flexibility in API design. These constraints are primarily overcome using attribute routing, which decouples URLs from code structure, or by adopting Minimal APIs for endpoint-focused definitions. Both approaches give precise control over HTTP verbs and URL patterns, which modern RESTful services require.

Read full answer

Applies toASP.NET Core Web API on .NET 8

Core Limitations

Convention-based routing relies on a single global template registered in Program.cs, such as {controller=Home}/{action=Index}/{id?} or an API-specific api/{controller}/{action=Index}/{id?}. This creates several drawbacks: - URL coupling: The URL is derived from the controller and action names, so renaming a class or method breaks the public contract unless the global template is changed. - Verb ambiguity: Without explicit verb attributes, multiple actions on one controller can match the same HTTP method and route, producing ambiguous-match errors or unexpected selection. - Versioning difficulty: Embedding version segments such as /api/v1/products requires extra template tokens or per-controller overrides rather than a declared version policy. - Hierarchy constraints: Nested resources such as /customers/{customerId}/orders do not map cleanly onto a flat {controller}/{action} structure and often force awkward action naming. - Implicit surface: Every public action method is potentially routable unless explicitly excluded, which makes it harder to reason about the exposed API surface.

Attribute Routing

Attribute routing lets developers define routes directly on controllers and actions with [Route], [HttpGet], [HttpPost] and the other verb attributes. This decouples the physical class structure from the public API surface and supports optional parameters, constraints, default values and explicit HTTP method mapping, so each endpoint has a unique, predictable signature. It is the standard approach for [ApiController]-based controllers in ASP.NET Core, and route templates can be combined at controller and action level (for example [Route("api/v1/[controller]")] with [HttpGet("{id:int}")]).

Minimal APIs

Minimal APIs define routes directly in Program.cs with methods such as MapGet, MapPost, MapPut and MapDelete, using lambda handlers instead of controller classes. This removes boilerplate, keeps route definitions and handlers in one place, and fits small services or microservices well. They support TypedResults for strongly typed, OpenAPI-friendly responses, and in .NET 8 they integrate with Swashbuckle (AddSwaggerGen/UseSwagger), which the template ships by default; from .NET 9 the built-in Microsoft.AspNetCore.OpenApi (AddOpenApi/MapOpenApi) replaces Swashbuckle.

Comparison

Aspect Convention-based routing Attribute routing Minimal APIs
Where defined One global template in Program.cs [Route]/verb attributes on controllers and actions MapGet/MapPost calls in Program.cs
URL vs code URL derived from class and method names URL fully decoupled from code names URL bound to an explicit endpoint
REST fit Weak; action-based URLs Strong; resource and verb centric Strong; endpoint centric
Best for Simple, CRUD-mirroring apps Most controller-based APIs Small services, lightweight endpoints

Best Practices and Pitfalls

  • Do not blend models: A controller that has attribute routes cannot also be reached by convention routing — its unattributed actions are simply not routable. Pick one model per controller.

  • Version by policy: Use the Asp.Versioning.Mvc package for API versioning instead of hand-coded route prefixes; Microsoft.AspNetCore.Mvc.Versioning is the old, deprecated package.

  • Apply constraints: Use route constraints such as {id:int} so malformed values fail routing instead of reaching action logic.

  • Prefer attribute routing for APIs: It is the default expectation for [ApiController] controllers and makes the contract explicit and versionable.

  • Match tooling to the version: Configure Swashbuckle on .NET 8 to document attribute routes, and plan to move to the built-in OpenAPI support when upgrading to .NET 9 or later.

60

Explain what route constraints are in an ASP.NET Core Web API and show how you apply them to route parameters.

A route constraint is a rule attached to a route parameter that restricts which URL segments can match that route. If the incoming value does not satisfy the constraint, the framework does not select that endpoint, so the request falls through to another route or produces a 404. Constraints are part of routing, not model validation: they decide which endpoint matches, while validation decides whether a matched request is acceptable.

Read full answer

Applies toASP.NET Core Web API on .NET 8 (controller-based APIs and Minimal APIs share the same routing constraint syntax).

How Route Constraints Work

Constraints are declared inline in a route template using the syntax {parameter:constraint}. During link generation and request matching, routing evaluates the constraint against the route value:

  • {id:int} matches only integer segments such as /api/products/42.

  • {id:int?} makes the parameter optional; the constraint is checked only when a value is supplied.

  • {id:int=1} applies a default when the segment is missing.

  • {name:alpha} matches only letters, so /api/products/abc matches but /api/products/abc1 does not.

  • Multiple constraints can be chained: {id:int:range(1,999)}.

Built-in Constraints

Constraint Example Matches
int {id:int} Integer values
long {id:long} 64-bit integers
bool {active:bool} true / false
guid {id:guid} GUID values
datetime {when:datetime} Date/time values
decimal, double, float {price:decimal} Numeric values
alpha {name:alpha} Letters only
minlength, maxlength, length {slug:minlength(3)} String length
min, max, range {qty:range(1,100)} Numeric bounds
regex {code:regex(^[A-Z]{3}$)} A regular expression
required {name:required} Value must be present

Where You Use Them

In a controller-based API, constraints go in attribute routes on the controller class or an action method. In Minimal APIs, the same templates are passed to MapGet, MapPost, and related endpoint builders, for example app.MapGet("/products/{id:int}", ...), optionally returning TypedResults such as TypedResults.Ok(product). Constraints are especially useful for disambiguating routes such as /products/{id:int} versus /products/{name:alpha}, and for making routes self-documenting.

Custom Constraints

When no built-in constraint fits, implement IRouteConstraint and register it in the constraint map. The registered name can then be used inline like {id:custom}. Prefer this over a permissive route plus manual parsing, because the endpoint still only matches valid URLs.

Code Example

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;

// Simple model so the example compiles
public record Product(int Id, string Name);

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    // Matches GET /api/products/42, but not /api/products/abc
    [HttpGet("{id:int:min(1)}")]
    public ActionResult<Product> GetById(int id)
    {
        var product = _store.Find(p => p.Id == id);
        return product is null ? NotFound() : Ok(product);
    }

    // Matches GET /api/products/abc (letters only), keeping the two routes distinct
    [HttpGet("{name:alpha:minlength(3)}")]
    public ActionResult<Product> GetByName(string name)
    {
        var product = _store.Find(p => p.Name == name);
        return product is null ? NotFound() : Ok(product);
    }

    private static readonly List<Product> _store = new()
    {
        new Product(1, "Widget"),
        new Product(2, "Gadget"),
    };
}

A custom constraint and its registration:

// Only matches even, positive integers, e.g. /api/orders/8
public sealed class EvenIdConstraint : IRouteConstraint
{
    public bool Match(
        HttpContext? httpContext,
        IRouter? route,
        string routeKey,
        RouteValueDictionary values,
        RouteDirection routeDirection)
    {
        return values.TryGetValue(routeKey, out var value)
            && value is not null
            && int.TryParse(value.ToString(), out var id)
            && id > 0
            && id % 2 == 0;
    }
}

// In Program.cs, before building the app
builder.Services.AddRouting(options =>
{
    options.ConstraintMap.Add("even", typeof(EvenIdConstraint));
});

Best Practices and Pitfalls

  • Do not use constraints for validation: a failed constraint removes the route from consideration, which surfaces as a 404, not a 400. Validate input with data annotations or model binding and return ValidationProblem() for bad payloads.

  • Keep routes unambiguous: two action templates such as {id:int} and {name} can overlap; constrain both so routing picks one deterministically.

  • Prefer specific built-ins over regex: {id:int} is clearer and faster than {id:regex(^\d+$)}.

  • Remember optional constraints: with {id:int?}, the constraint is skipped when the segment is absent, so the action must handle a missing or default value.

  • Register custom constraints once in AddRouting/Configure<RouteOptions>; the constraint type is applied per match, so keep Match cheap and side-effect free.

61

Can you explain what a Data Transfer Object is in the context of an ASP.NET Core Web API, and what role it plays?

A Data Transfer Object (DTO) is a simple class whose only job is to carry data across a process or network boundary — in a Web API, between your API and its HTTP clients. It is important because it decouples the shape of your wire contract from your internal domain models, giving you control over serialization, validation, versioning and security while keeping your entities and database concerns private to the server.

Read full answer

Applies toASP.NET Core Web API on .NET 8 (controller-based APIs with `[ApiController]` and `ControllerBase`); the pattern itself applies to all versions.

What a DTO Is

  • Purpose-built container: a DTO has properties only — no business logic, no navigation behaviour, no database attributes.

  • Boundary type: it is what the model binder deserializes the request body into and what the serializer writes into the response body.

  • Distinct from the domain model: an entity such as Product may have internal state, relationships and persistence concerns; a ProductDto exposes only what the client should see.

Why DTOs Matter

  • Contract stability: you can rename or restructure an entity without breaking clients, because the DTO stays the same.

  • Security: prevents over-posting. Accepting a CreateProductDto instead of Product stops clients from setting properties such as Id or IsDeleted that you never intended to expose.

  • Controlled serialization: avoids lazy-loading or reference-loop problems when returning entity graphs, and lets you shape JSON with System.Text.Json attributes such as [JsonPropertyName].

  • Validation in isolation: different DTOs for create, update and read can carry different validation rules — for example [Required] on create but optional on update.

  • Aggregation and flattening: a single response can combine data from several entities or services into one payload, which reduces client round trips.

  • Versioning: when a response changes, you can add ProductV2Dto alongside the existing DTO instead of forcing a breaking change.

DTOs vs. Entities

Aspect Entity DTO
Lives in Domain / data layer API boundary
Contains logic Often yes No, data only
Driven by Database schema Client contract
Risk if returned directly Over-posting, leaky internals, serialization loops None
Changes when Schema changes API contract changes

Common Patterns

  • Per-operation DTOs: ProductCreateDto, ProductUpdateDto, ProductReadDto — each matches one HTTP verb’s needs.

  • Manual mapping for small APIs, or a mapper library such as AutoMapper for larger ones; either way, map explicitly so the transformation is visible and testable.

  • Return ActionResult<T> from controllers so the DTO type appears in the OpenAPI schema while still allowing status codes such as 201 Created.

Best Practices and Pitfalls

  • Do not return entities directly from API actions; it leaks internals and invites over-posting on input models.

  • Keep DTOs in the API layer, not in the domain or data project, so the contract stays an API concern.

  • Use record types when DTOs are immutable — they give value-based equality with almost no ceremony.

  • Validate at the boundary: data annotations plus [ApiController] return a 400 with Problem Details automatically; move cross-field rules to a custom validator when annotations are not enough.

  • Map explicitly and test the mapping — silent mis-mapping between similarly named properties is the most common DTO bug.

62

Describe how AutoMapper is integrated and used within an ASP.NET Web API application.

AutoMapper is a convention-based object-to-object mapping library that removes repetitive manual property assignment when converting between domain entities and DTOs in ASP.NET Core Web API. You define mapping profiles once, register them through dependency injection, and inject IMapper into controllers to transform objects safely.

Read full answer

Applies toASP.NET Core Web API on .NET 8 with AutoMapper 13 and AutoMapper.Extensions.Microsoft.DependencyInjection

What It Solves

Without a mapper, every endpoint needs boilerplate like new UserDto { Id = user.Id, Name = user.Name }. AutoMapper copies matching property names and types automatically, so a CreateMap<User, UserDto>() handles all equal-named properties. This keeps controllers focused on HTTP concerns rather than data transformation.

Approach When to use
Manual mapping One-off conversions, or when property names differ significantly
AutoMapper Repeated entity-to-DTO conversions across many endpoints
ProjectTo<T>() Translating an EF Core IQueryable directly in the database query

How Integration Works

  1. Install the AutoMapper and AutoMapper.Extensions.Microsoft.DependencyInjection NuGet packages.

  2. Call AddAutoMapper in Program.cs to scan assemblies for classes inheriting Profile and register IMapper as a singleton.

  3. Create profile classes that define CreateMap<Source, Destination>() with optional ForMember customizations for non-conventional cases.

  4. Inject IMapper into controllers (or Minimal API handlers) and call Map<T> inside actions.

Code Example

// Program.cs: register AutoMapper (scans the Program assembly for Profile classes)
builder.Services.AddAutoMapper(typeof(Program));

// MappingProfile.cs: group related mappings and customize non-conventional cases
using AutoMapper;

public class MappingProfile : Profile
{
    public MappingProfile()
    {
        CreateMap<User, UserDto>();
        CreateMap<CreateUserDto, User>()
            .ForMember(d => d.CreatedAt, opt => opt.Ignore());
        CreateMap<User, UserSummaryDto>()
            .ForMember(d => d.DisplayName, opt => opt.MapFrom(s => $"{s.FirstName} {s.LastName}"));
    }
}
// UsersController.cs: inject IMapper and map inside actions
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private readonly IMapper _mapper;
    private readonly IUserRepository _repository;

    public UsersController(IMapper mapper, IUserRepository repository)
    {
        _mapper = mapper;
        _repository = repository;
    }

    [HttpGet("{id:int}")]
    public async Task<ActionResult<UserDto>> GetUser(int id)
    {
        var user = await _repository.GetByIdAsync(id);
        if (user is null) return NotFound();
        return Ok(_mapper.Map<UserDto>(user));
    }

    [HttpPost]
    public async Task<ActionResult<UserDto>> CreateUser(CreateUserDto dto)
    {
        var user = _mapper.Map<User>(dto);
        await _repository.AddAsync(user);
        return CreatedAtAction(nameof(GetUser), new { id = user.Id },
            _mapper.Map<UserDto>(user));
    }
}

The same IMapper works in Minimal APIs by injecting it into the handler delegate.

Best Practices and Common Pitfalls

  • Prefer ProjectTo<T>() for reads: mapping after loading entities pulls entire rows into memory; projecting the query lets the database return only selected columns.

  • Map in one direction per profile: separate Entity -> Dto and Dto -> Entity maps; do not make every map bidirectional.

  • Never map across aggregate boundaries in a single call: fetch only what the DTO needs instead of mapping a huge graph.

  • Validate before mapping: AutoMapper copies values blindly; rely on [ApiController] model validation and FluentValidation for input rules.

  • Keep profiles small and test them: call AssertConfigurationIsValid() or run integration tests to catch unmapped members early, especially after renames.

  • Avoid AutoMapper for simple types: mapping a single primitive or a two-property object adds indirection without benefit.

63

What are the primary benefits of implementing IHttpActionResult in Web API 2 controllers?

Using IHttpActionResult in ASP.NET Web API 2 provides a cleaner abstraction over raw HTTP response messages, enabling better testability and composability of action results. It separates the logic of determining what to return from the mechanics of serializing and sending the response.

Read full answer

Applies toASP.NET Web API 2 on .NET Framework 4.8

Core Concept

The IHttpActionResult interface defines a single method, ExecuteAsync, which the framework calls to produce an HttpResponseMessage. Controller helper methods like Ok(), NotFound(), and BadRequest() return concrete implementations of this interface. This shifts responsibility from the developer manually constructing messages to the framework handling content negotiation and status codes based on the result type.

Key Advantages

  • Testability: Actions returning IHttpActionResult are easier to unit test because you can assert the result type without needing an HTTP host or message handler pipeline.

  • Composition: Results can be wrapped or decorated to add cross-cutting concerns, such as logging or custom headers, without changing controller logic.

  • Convenience: Built-in helper methods reduce boilerplate code compared to manually instantiating HttpResponseMessage objects.

  • Content Negotiation: The framework handles formatter selection automatically based on the object passed to helpers like Ok<T>.

Comparison with HttpResponseMessage

Aspect IHttpActionResult HttpResponseMessage
Abstraction High-level result type Low-level HTTP message
Content negotiation Automatic (e.g., Ok<T>) Manual, must use ObjectContent or similar
Header control Limited unless wrapped or custom result Full control over headers and content
Testability Easy to assert result type Requires host or message pipeline
Typical use Most CRUD actions Scenarios needing precise header/stream control

Returning HttpResponseMessage gives complete control over headers and content but couples the action to messaging infrastructure. IHttpActionResult is preferred for most scenarios because it maintains a higher level of abstraction while still allowing access to the response when necessary via ResponseMessage.

Migration to ASP.NET Core

In ASP.NET Core, IHttpActionResult does not exist; it was replaced by IActionResult. Core controllers inherit from ControllerBase and use [ApiController] attributes for automatic model validation and 400 responses. Minimal APIs offer an alternative with endpoint delegates and TypedResults, but controller-based APIs remain the direct successor to Web API 2. Do not attempt to use System.Web.Http types in a Core project, as they belong to different namespaces and runtimes.

Best Practices

  • Use specific helper methods (CreatedAtRoute, Conflict) to convey accurate HTTP semantics rather than generic ResponseMessage.

  • Avoid returning null; always return a valid result implementation to prevent runtime exceptions.

  • For scenarios requiring custom headers, either wrap the result in a custom IHttpActionResult or use ResponseMessage with a manually built HttpResponseMessage.

  • Remember that Web API 2 is legacy; new development should target ASP.NET Core where IActionResult and Problem Details are standard.

64

Compare the various action results returned by ASP.NET Core Web API controllers, such as Ok, BadRequest, and NotFound, and explain when to use each one.

Action results are the typed return values a controller action uses to produce an HTTP response, and the helper methods like Ok, BadRequest, and NotFound are shortcuts that build the corresponding status code, headers, and body. Choosing the right one is what makes an API behave predictably, because each one maps to a specific HTTP status code and content format.

Read full answer

Applies toASP.NET Core Web API on .NET 8 (controller-based APIs using `[ApiController]` and `ControllerBase`).

What an Action Result Actually Is

An action result is any type implementing IActionResult (or ActionResult<T>, which lets the method return either a result object or a data model). The result is not executed inside the action; the framework runs it after the action returns, so it can write to the HttpContext during the response pipeline. ControllerBase exposes a set of virtual helper methods that construct these results, and ActionResult<T> also supports implicit conversion of a model into a 200 OK response. In Minimal APIs (the alternative to controllers), the equivalent helpers live on the Results/TypedResults class and return IResult instead of IActionResult.

Common Action Results and Their Status Codes

Helper method Status code Meaning Typical use
Ok() / Ok(value) 200 OK Request succeeded, body optional Returning a resource or list on a successful GET, or the result of a PUT
Created(uri, value) / CreatedAtAction / CreatedAtRoute 201 Created New resource created POST that inserts an item; must include a Location header pointing at the new resource
Accepted() / AcceptedAtAction 202 Accepted Request accepted, processing unfinished Long-running or queued background work
NoContent() 204 No Content Success with an empty body PUT or DELETE that changes state but returns nothing
BadRequest() / BadRequest(errors) 400 Bad Request Malformed or invalid request Manual validation failures or a semantically wrong payload
Unauthorized() 401 Unauthorized Missing or invalid authentication Rejecting an unauthenticated caller
Forbid() 403 Forbidden Authenticated but not allowed Rejecting a caller who lacks the required role or policy
NotFound() / NotFound(value) 404 Not Found Resource does not exist GET, PUT, or DELETE for a key that has no matching record
Conflict() / Conflict(value) 409 Conflict Request conflicts with current state Duplicate key, optimistic concurrency failure, or an illegal state transition
Problem(...) / ValidationProblem(...) 500 / 400 (defaults) RFC 9457 Problem Details error Returning a standards-compliant error body for server faults and validation errors
StatusCode(int) Any code Escape hatch Any status code without a dedicated helper, e.g. StatusCode(503)

How to Choose the Right One

  • Idempotent verbs: GET, PUT, and DELETE should return the same status for repeated identical requests, so Ok, NoContent, and NotFound fit naturally. POST is not idempotent and is the main consumer of 201 Created.

  • Prefer specific helpers over StatusCode: NotFound() expresses intent far more clearly than StatusCode(404) and keeps status codes consistent across the codebase.

  • Let [ApiController] do the routine work: with this attribute, invalid model state automatically produces a 400 with a Problem Details body, so you do not need to call BadRequest manually for validation failures.

  • Use ActionResult<T> over plain IActionResult: it preserves the success type for OpenAPI and static analysis while still allowing error results, and it keeps the success path as a simple return model;.

  • Return errors as Problem Details: Problem() and ValidationProblem() emit application/problem+json per RFC 9457, which is the recommended error format and is configured once with builder.Services.AddProblemDetails().

Best Practices and Common Pitfalls

  • Always pair Created with a real Location header; returning 201 without one loses the main benefit of the status code.

  • Do not return 200 OK for a missing resource just to avoid an error; 404 Not Found is the correct, cacheable semantic.

  • Keep 401 for authentication and 403 for authorization; swapping them confuses clients that rely on the distinction.

  • Reserve 500 for genuine server faults and let the framework or Problem() produce it, so unexpected exceptions do not leak stack details.

  • Avoid returning raw domain entities when they may contain sensitive fields; map to a response model first.

65

What are the key advantages and potential pitfalls of implementing asynchronous action methods within ASP.NET Core Web API controllers?

Using asynchronous action methods in ASP.NET Core Web API primarily enhances server scalability by releasing request threads back to the pool during I/O-bound operations, though it introduces complexity and potential pitfalls if misused with CPU-bound work or blocking calls.

Read full answer

Applies toASP.NET Core Web API on .NET 8

Core Benefits

The main advantage is improved throughput under high concurrency. When a controller action awaits an I/O operation, such as a database query via Entity Framework Core or an outbound HTTP request made through IHttpClientFactory, the thread handling the request returns to the thread pool immediately instead of sitting idle. This lets the server handle more simultaneous requests with fewer threads, which reduces memory consumption (each thread costs stack space) and context-switching overhead. The scalability gain is most significant for I/O-bound services where waiting time dominates actual execution time, and it applies equally to controller-based APIs and minimal API endpoints, since both can use async delegates.

Potential Risks

Misusing async can degrade performance rather than improve it:

  • CPU-bound work: Converting compute-heavy code to async adds state machine allocation and overhead without freeing any threads, so it can be slower than synchronous code.

  • Blocking on async code: Using .Result or .Wait() on a Task inside an action can cause thread pool starvation, because a request thread is occupied while it waits for a thread that may never become available.

  • Sync/async mixing: Wrapping synchronous calls in async methods hides blocking behaviour from callers and can mislead maintainers about scalability.

  • Complexity: Deeper async call chains make debugging, stack traces and error handling harder, and unhandled exceptions surface differently than in synchronous code.

  • Overuse: Adding async to every method “for consistency” without an I/O boundary adds overhead with no benefit.

When to Use Async

Use async for I/O-bound operations: database queries, file system access, cache calls, and outbound HTTP requests. For CPU-intensive tasks such as image processing or heavy calculations, prefer synchronous methods or offload the work to a background service. If a dependency offers only a synchronous API, wrapping it in Task.Run is acceptable but should be done sparingly, since it still consumes a thread pool thread and merely moves the blocking elsewhere.

Best Practices

  • Avoid blocking calls: Never use .Result or .Wait() inside an async action; await the task instead.

  • Be async all the way: Make every method in the call chain async down to the I/O boundary so no layer silently blocks.

  • Use async Task<ActionResult<T>>: Controller actions should return Task<ActionResult<T>> or Task<IActionResult> and be awaited by the framework, which handles the result normally.

  • Handle exceptions consistently: Unhandled exceptions in async actions produce a 500 response; register AddProblemDetails() and use an IExceptionHandler to return RFC 9457 Problem Details responses.

  • Test asynchronously: Unit tests for async actions must themselves be async and await the result, otherwise they may observe incomplete tasks or report false positives.

  • Measure before optimising: For low-traffic or purely CPU-bound endpoints, synchronous code is often simpler and equally fast; apply async where concurrency and I/O justify it.

66

How would you implement an asynchronous controller action method within an ASP.NET Core Web API project?

Implementing an asynchronous action in ASP.NET Core Web API requires marking the method with the async modifier and returning a Task<ActionResult<T>>. This pattern releases the request thread back to the pool while waiting for I/O-bound operations, such as database queries or external HTTP calls, significantly improving server scalability under load.

Read full answer

Applies toASP.NET Core Web API on .NET 8

How Asynchronous Actions Work

When an action awaits a task, the ASP.NET Core runtime suspends execution and returns the request thread to the pool. When the I/O operation completes, the method resumes on an available thread-pool thread. ASP.NET Core has no synchronization context, so continuations do not need to return to a specific thread and ConfigureAwait(false) is generally unnecessary. This matters for I/O-bound workloads: blocking threads with .Result or .Wait() wastes thread-pool capacity and can cause starvation under load.

Minimal API Alternative

In .NET 8, Minimal APIs provide a lighter syntax without controllers. You can define an async endpoint directly on the WebApplication instance using MapGet; the delegate is async and returns a type compatible with IResult. For explicit return types and better OpenAPI metadata, use TypedResults (for example Results.Ok<T>()). Controllers suit larger applications; Minimal APIs fit microservices or simple endpoints.

Code Example

The examples show both approaches. The controller uses IHttpClientFactory for outbound calls; register the named client in Program.cs with builder.Services.AddHttpClient("ExternalApi", c => c.BaseAddress = ...).

using System.Net;
using System.Net.Http.Json;
using Microsoft.AspNetCore.Mvc;

public record Product(int Id, string Name);

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IHttpClientFactory _httpClientFactory;

    public ProductsController(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<Product>> GetProductAsync(int id, CancellationToken cancellationToken)
    {
        var client = _httpClientFactory.CreateClient("ExternalApi");
        var response = await client.GetAsync($"products/{id}", cancellationToken);

        if (response.IsSuccessStatusCode)
        {
            var product = await response.Content.ReadFromJsonAsync<Product>(cancellationToken);
            return Ok(product);
        }

        if (response.StatusCode == HttpStatusCode.NotFound)
        {
            return NotFound();
        }

        return Problem(statusCode: (int)response.StatusCode, title: "External Service Error");
    }
}
// Minimal API equivalent in Program.cs
using System.Net.Http.Json;
using Microsoft.AspNetCore.Http;

app.MapGet("/products/{id}", async (int id, IHttpClientFactory factory, CancellationToken ct) =>
{
    var client = factory.CreateClient("ExternalApi");
    var response = await client.GetAsync($"products/{id}", ct);
    return response.IsSuccessStatusCode
        ? Results.Ok(await response.Content.ReadFromJsonAsync<Product>(ct))
        : Results.NotFound();
});

Best Practices and Common Pitfalls

  • Avoid Async Void: Never use async void for controller actions; the framework cannot catch or handle exceptions thrown from them.

  • Do Not Block: Avoid .Result or .Wait() on tasks inside async actions; blocking wastes thread-pool threads and can cause starvation under load.

  • Use Cancellation Tokens: Accept CancellationToken and pass it to downstream operations so client disconnects and timeout policies are respected.

  • IHttpClientFactory: Use IHttpClientFactory (named or typed clients) for outbound HTTP instead of creating HttpClient per request, avoiding socket exhaustion.

  • Error Handling: Return errors as Problem Details with Problem() or ValidationProblem() rather than letting unhandled exceptions produce generic 500 responses.

67

Explain the concept of content negotiation and describe its purpose within a Web API solution.

Content negotiation is the mechanism by which a client and server determine the most appropriate representation format for a resource exchanged over HTTP. In ASP.NET Core Web API, this process is driven by content negotiation, where the server inspects the client’s Accept header to select the correct output formatter, ensuring data is serialized in a mutually understood structure like JSON or XML.

Read full answer

Applies toASP.NET Core Web API on .NET 8

Core Concept

The primary goal of content negotiation is interoperability across diverse clients. A single API endpoint often serves web browsers, mobile applications, and third-party services that may prefer different data formats. Instead of maintaining separate endpoints for each format, the API uses negotiation to return the same logical data serialized differently based on request headers. This adheres to REST principles by strictly separating resource identification from its representation, allowing the URI to remain constant while the payload structure varies.

Mechanism in ASP.NET Core

When a controller action returns an object, the ASP.NET Core runtime does not immediately serialize it. Instead, the object is passed to the output formatter selector, which iterates through the registered OutputFormatters collection. This selector matches formatters against the Accept header values provided by the client. The first formatter that supports the requested media type and can serialize the object type is chosen. In Minimal APIs, the same negotiation logic applies when returning objects directly or using TypedResults, ensuring consistency across programming models. If no match is found, the server typically returns the default format rather than an error, unless strict compliance is configured.

HTTP Headers Involved

Several headers drive this process during the request lifecycle: - Accept: Indicates the media types the client can handle, such as application/json or application/xml. Quality values (q=) indicate preference priority when multiple formats are supported. - Content-Type: Used during request body deserialization to tell the server how the incoming payload is formatted, enabling input negotiation. - Accept-Encoding: Specifies content codings like gzip or br, which are handled separately by the response compression middleware rather than formatters.

Default Behavior and Configuration

In .NET 8, the default template configures SystemTextJsonOutputFormatter as the primary serializer. XML support is not included by default and must be explicitly added to the service collection. Developers can customize serialization options globally rather than per-controller, ensuring consistent behavior across the application.

Feature Default in .NET 8 Configuration Method
JSON Formatter Enabled AddControllers().AddJsonOptions()
XML Formatter Disabled AddXmlSerializerFormatters()
Model Binding Uses Content-Type [ApiController] enables automatic 400
Error Format Problem Details (RFC 9457) AddProblemDetails()

Best Practices

  • Stick to JSON: Unless legacy integration requires XML, JSON is the standard for modern Web APIs and reduces the potential attack surface.

  • Avoid Over-Negotiation: Supporting too many formats increases maintenance complexity and testing burden without providing significant value to most consumers.

  • Secure Serialization: Ensure sensitive data is excluded from serialization regardless of the format chosen, using attributes like [JsonIgnore].

  • Versioning Strategy: If format changes are breaking, use API versioning via the Asp.Versioning.Mvc package rather than relying solely on content negotiation.

  • Explicit Returns: Use ActionResult<T> to allow returning specific status codes like 200, 204, or 404 while still benefiting from automatic serialization.

Common Pitfalls

  • Assuming JSON: Clients might send Accept: text/html expecting a view, which can confuse API consumers. Ensure your API returns consistent types regardless of header anomalies.

  • Ignoring 406: By default, ASP.NET Core falls back to JSON rather than returning 406 Not Acceptable. If strict compliance is needed, custom logic is required to enforce header matching.

  • Input/Output Mismatch: Ensure the Content-Type of the POST body matches what the input formatters expect, otherwise model binding fails before negotiation occurs, resulting in a 400 Bad Request.

68

What approaches let you make an ASP.NET Core Web API action always respond with a chosen media type instead of relying on content negotiation?

To force a specific content type, you bypass ASP.NET Core’s content negotiation by explicitly setting the response’s Content-Type header and writing a body already formatted for that media type, typically by returning a ContentResult through the Content helper or by assigning HttpContext.Response.ContentType directly. The Content-Type header on the response is what ultimately tells the client how to interpret the payload.

Read full answer

Applies toASP.NET Core Web API on .NET 8

How Content Negotiation Normally Works

By default, ASP.NET Core selects an output formatter based on the request’s Accept header and the formatters registered in the app. In .NET 8 the default JSON formatter uses System.Text.Json, and XML formatters are not registered unless you add them. This means a plain return Ok(value) respects what the client asks for, which is usually the correct REST behavior. Forcing a type overrides that contract, so it should be done deliberately rather than as a blanket setting.

Ways to Force a Content Type

  • Content helper method: return Content(serializedBody, "application/json") returns a ContentResult with the media type baked in. You are responsible for serializing the object yourself.

  • Set Response.ContentType: Assign HttpContext.Response.ContentType = "application/xml" and return the body via Content, PhysicalFile, or write directly. This works inside any action or middleware.

  • [Produces] attribute: [Produces("application/json")] declares and restricts the allowed response types for an action or controller; negotiation can only pick from those types (returning 406 Not Acceptable otherwise), and it also documents the contract in OpenAPI/Swagger.

  • Formatter registration: In Program.cs you can remove all output formatters except one, so negotiation can only ever pick that type across the whole API.

  • File responses: For downloads use return File(bytes, "application/pdf"), which sets both the content type and disposition.

Code Example

using System.Text.Json;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class ReportsController : ControllerBase
{
    // Forces JSON even if the client sends Accept: application/xml
    [HttpGet("summary")]
    public ActionResult<SummaryDto> GetSummary()
    {
        var summary = new SummaryDto { Total = 42 };
        // ContentResult sets Content-Type explicitly; you serialize manually
        return Content(JsonSerializer.Serialize(summary), "application/json");
    }

    // Forces XML by setting the header directly
    [HttpGet("detail")]
    public IActionResult GetDetail()
    {
        var xml = "<detail><total>42</total></detail>";
        Response.ContentType = "application/xml";
        return Content(xml);
    }

    // Declares a single supported type; negotiation cannot choose another
    [Produces("application/json")]
    [HttpGet("items")]
    public ActionResult<List<Item>> GetItems()
    {
        return Ok(new List<Item>());
    }
}

Best Practices and Pitfalls

  • Prefer negotiation when possible: Forcing a type breaks clients that rely on Accept, and it can hurt caching and interoperability. Use it for endpoints with a fixed contract, such as file downloads or webhook callbacks.

  • Serialize before setting the type: The Content helper only accepts a string, so you must serialize the object yourself before calling it; passing an object is a compile-time error. It does not run an output formatter.

  • Keep Produces for documentation: On its own, [Produces] restricts negotiation but does not serialize; pair it with the default formatters rather than using it as the only enforcement mechanism.

  • Status codes still matter: Forcing content type does not change HTTP semantics; still return 200, 204, 404, or 400 as the operation warrants, and let [ApiController] handle model-validation 400s as Problem Details.

  • Minimal APIs alternative: With Minimal APIs you can use Results.Content(body, contentType: "application/json") or TypedResults to achieve the same explicit control without a controller class.

69

How can you secure an ASP.NET Core Web API so that requests are authenticated by validating a signed token and authorized based on the claims it contains?

Claims-based authentication verifies a signed security token (usually a JWT issued by an identity provider or your own login endpoint) and builds a ClaimsPrincipal from the claims inside it, so authorization decisions are made from those claims rather than from a local user store.

Read full answer

Applies toASP.NET Core Web API on .NET 8, using `Microsoft.AspNetCore.Authentication.JwtBearer` 8.x and `System.IdentityModel.Tokens.Jwt` 8.x.

How It Works

  1. A client authenticates (login endpoint, Identity Server, Entra ID, etc.) and receives a JWT that contains claims such as sub, email, and role.

  2. The client sends the token on every request: Authorization: Bearer <token>.

  3. The JWT bearer middleware validates the signature, issuer, audience, and lifetime; if validation fails it returns 401 Unauthorized.

  4. On success, the middleware creates a ClaimsPrincipal and ClaimsIdentity, which HttpContext.User exposes to controllers and the authorization middleware.

  5. [Authorize] and authorization policies evaluate those claims; failing a policy returns 403 Forbidden.

Configuring Authentication and Authorization

  • AddAuthentication + AddJwtBearer: registers JWT bearer as the default authentication scheme and sets TokenValidationParameters (valid issuer, audience, signing key, lifetime validation).

  • MapInboundClaims = false: keeps the original claim names from the token instead of mapping sub to ClaimTypes.NameIdentifier; with this setting, use RequireClaim("role", "Admin") rather than RequireRole("Admin"), which looks for the mapped ClaimTypes.Role.

  • AddAuthorization with policies: the modern, preferred way to express requirements beyond a single role check, e.g. policy.RequireClaim("department", "Sales").

  • Middleware order: UseAuthentication() must come before UseAuthorization(), which comes before MapControllers().

Issuing Tokens

A login endpoint validates credentials (ideally through ASP.NET Core Identity), creates a ClaimsIdentity, signs a JwtSecurityToken with SigningCredentials, and returns the serialized token with 200 OK. Use short-lived access tokens plus refresh tokens; the signing key must come from configuration/secrets, never from source code, and the API must be served over HTTPS.

Code Example

// Program.cs
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails(); // RFC 9457 error responses

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        var jwt = builder.Configuration.GetSection("Jwt");
        options.MapInboundClaims = false; // keep original claim names
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = jwt["Issuer"],
            ValidateAudience = true,
            ValidAudience = jwt["Audience"],
            ValidateLifetime = true,
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(jwt["SigningKey"]!))
        };
    });

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminOnly", p => p.RequireClaim("role", "Admin"));
});

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
// Login endpoint that issues the token
// LoginRequest, LoginResponse and CheckCredentialsAsync are app-defined.
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;

[ApiController]
[Route("api/auth")]
public class AuthController(IConfiguration config) : ControllerBase
{
    [HttpPost("login")]
    public async Task<ActionResult<LoginResponse>> Login(LoginRequest request)
    {
        var user = await CheckCredentialsAsync(request.UserName, request.Password);
        if (user is null)
        {
            return Unauthorized(); // 401 when authentication fails
        }

        var claims = new List<Claim>
        {
            new("sub", user.Id),
            new("email", user.Email),
            new("role", user.Role)
        };

        var key = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(config["Jwt:SigningKey"]!));
        var token = new JwtSecurityToken(
            issuer: config["Jwt:Issuer"],
            audience: config["Jwt:Audience"],
            claims: claims,
            expires: DateTime.UtcNow.AddMinutes(15),
            signingCredentials: new SigningCredentials(
                key, SecurityAlgorithms.HmacSha256));

        return Ok(new LoginResponse(new JwtSecurityTokenHandler().WriteToken(token)));
    }
}
// Protected endpoint
// Order is an app-defined record.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
    [HttpGet("{id:int}")]
    [Authorize(Policy = "AdminOnly")]
    public ActionResult<Order> Get(int id)
    {
        var userId = User.FindFirst("sub")?.Value; // read a claim
        return Ok(new Order(id, userId));
    }
}

Best Practices and Common Pitfalls

  • Validate everything: keep issuer, audience, lifetime, and signature validation on; a common mistake is disabling ValidateIssuer/ValidateAudience to make samples work.

  • Key management: store the signing key in user secrets, environment variables, or Key Vault; rotate it and prefer asymmetric keys (RSA/ECDsa) when third parties validate tokens.

  • Clock skew: the default 5-minute ClockSkew can extend token lifetime; tighten it if short expiry matters.

  • 401 vs 403: unauthenticated or invalid token → 401; authenticated but insufficient claims → 403. With [ApiController], invalid request models automatically return 400 with Problem Details.

  • Minimal APIs: the same AddAuthentication/AddAuthorization pipeline applies; protect endpoints with RequireAuthorization("AdminOnly") and return TypedResults for typed OpenAPI responses.

  • Don’t roll your own crypto: prefer ASP.NET Core Identity plus an external identity provider for real user stores, and use IHttpClientFactory when the API calls the token endpoint of another service.

70

How does Cross-Site Request Forgery (CSRF) protection function in ASP.NET Core Web API, and under which authentication scenarios is it mandatory?

CSRF protection in ASP.NET Core Web API relies on anti-forgery tokens, but it is only mandatory when the API authenticates requests with cookies that browsers attach automatically. APIs that use token-based authentication such as JWT in the Authorization header are not vulnerable to classic CSRF, because a malicious site cannot force the victim’s browser to send that custom header (and a cross-site request carrying a custom header triggers a CORS preflight the attacker cannot satisfy).

Read full answer

Applies toASP.NET Core Web API on .NET 8

How Antiforgery Tokens Work

The antiforgery system implements a double-submit pattern. When IAntiforgery is invoked, it generates two cryptographically linked tokens: a cookie token, stored in an antiforgery cookie, and a request token (also called the field token), which the server returns to the page or client. On a subsequent unsafe request, the client must send the request token back in a header (default RequestVerificationToken) or as a form field, while the browser automatically attaches the cookie token. The middleware validates that the two tokens are present and match; if they do not, validation fails and, with [ApiController], the request is rejected with a 400 Bad Request (Problem Details response). An attacker’s cross-site form or image request cannot read the request token, so it cannot forge a valid pair.

When CSRF Protection Is Mandatory

CSRF exploits the browser’s habit of sending cookies with every request to a domain, including requests initiated by a malicious third-party site. Protection is therefore required only for cookie-based authentication — for example ASP.NET Core Identity’s application cookie or a custom cookie authentication scheme consumed by a browser front end. Stateless APIs using JWT bearer tokens in the Authorization header are inherently protected, since the attacker cannot obtain or attach the victim’s token header.

Enabling and Applying Antiforgery

The antiforgery services are registered with AddAntiforgery in Program.cs. The attributes live in the Microsoft.AspNetCore.Mvc namespace: apply [ValidateAntiForgeryToken] to individual actions that modify state, or [AutoValidateAntiforgeryToken] at the controller level to validate all unsafe verbs (POST, PUT, PATCH, DELETE) while exempting GET, HEAD, OPTIONS and TRACE. The server typically exposes the request token through an endpoint that calls IAntiforgery.GetAndStoreTokens(HttpContext); browser clients then read it and include it in the header. Minimal APIs use the same AddAntiforgery registration and validate explicitly with IAntiforgery.IsRequestValidAsync(HttpContext).

Cookie Security Attributes

When cookie authentication is unavoidable, harden the cookie itself. Set SameSite to Strict or Lax so the browser withholds it in cross-site contexts, SecurePolicy = CookieSecurePolicy.Always so it travels only over HTTPS, and HttpOnly = true so JavaScript cannot read it (mitigating the impact of XSS). These are configured in Program.cs through AddCookie, with UseAuthentication registered before UseAuthorization in the pipeline.

CORS Interplay

CORS complements but does not replace antiforgery: CORS controls which origins a browser may call, while tokens prove the request’s intent. Never allow any origin (*) together with credentials — list trusted origins explicitly, or malicious sites could trigger requests that carry the user’s cookie.

Code Example

[ApiController]
[Route("api/[controller]")]
[AutoValidateAntiforgeryToken] // validates tokens on POST/PUT/PATCH/DELETE
public class OrdersController : ControllerBase
{
    [HttpPost]
    [ProducesResponseType(StatusCodes.Status201Created)]
    public IActionResult CreateOrder([FromBody] OrderDto order)
    {
        var created = _orders.Add(order); // business logic
        return CreatedAtAction(nameof(GetOrder), new { id = created.Id }, created);
    }

    [HttpGet("{id}")]
    [ProducesResponseType(StatusCodes.Status200OK)]
    public ActionResult<OrderDto> GetOrder(int id) => Ok(_orders.Get(id));
}

Server-side configuration in Program.cs:

builder.Services.AddControllers();
builder.Services.AddAntiforgery(options => options.HeaderName = "RequestVerificationToken");
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.Cookie.SameSite = SameSiteMode.Strict;
        options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
        options.Cookie.HttpOnly = true;
    });

var app = builder.Build();
app.UseAuthentication(); // before UseAuthorization
app.UseAuthorization();
app.MapControllers();
app.Run();

Best Practices

  • Prefer JWT for APIs: token authentication for SPA- or app-consumed APIs removes CSRF from the threat model entirely.

  • Protect state-changing endpoints only: tokens are needed for POST/PUT/PATCH/DELETE, not for read-only GETs.

  • Scope browser-facing protection: applying [AutoValidateAntiforgeryToken] globally can break non-browser API clients that cannot supply tokens; apply it to controllers reached by browser apps.

  • Combine defenses: when cookies are unavoidable, pair antiforgery tokens with a strict, explicit CORS policy and hardened cookie attributes.

71

Explain the mechanism of anti-forgery protection in ASP.NET Core Web API and specify the scenarios where enabling it is required.

Anti-forgery tokens protect against Cross-Site Request Forgery (CSRF) by requiring a secret, per-request value tied to the user’s authentication for state-changing requests. In ASP.NET Core Web API, this protection is required when the API relies on cookie-based authentication, whereas token-based schemes such as JWT bearer tokens are inherently resistant because browsers never attach them automatically.

Read full answer

Applies toASP.NET Core Web API on .NET 8

How It Works

The system follows the synchronizer token pattern: - Token pair: On a page load or via a dedicated endpoint, the server generates a cookie token (stored in an antiforgery cookie) and a matching request token, which it sends to the client. - Submission: For state-changing requests (POST, PUT, DELETE, PATCH), the client must send the request token back, typically in a custom header configured via HeaderName in AntiforgeryOptions or in a form field. - Validation: The server verifies that the submitted request token corresponds to the cookie token. A malicious cross-origin site cannot read the cookie due to the Same-Origin Policy and cannot guess the cryptographically random token, so it cannot forge a valid request. - Failure response: With [ValidateAntiForgeryToken], a missing or mismatched token results in a 403 Forbidden response.

When It Is Required

Authentication scheme Anti-forgery required? Reason
Cookie authentication (e.g., an SPA sharing the domain) Yes Browsers automatically attach cookies to cross-origin requests, which enables CSRF
JWT bearer in the Authorization header No Browsers never attach Authorization headers automatically, and attackers cannot read the token from storage due to the Same-Origin Policy
Mixed (cookies plus tokens) Yes, on cookie-authenticated endpoints only Only the requests that carry cookies are CSRF-vulnerable

Best Practices and Pitfalls

  • CORS configuration: Never combine AllowAnyOrigin with AllowCredentials. When cookies are sent, explicitly whitelist origins.

  • SameSite cookies: Set SameSite=Lax or Strict on authentication cookies as a baseline defense alongside tokens.

  • Never on GET: Do not validate anti-forgery tokens on GET requests; this breaks caching and link semantics, and CSRF only affects state changes.

  • Token rotation: Regenerate the token pair when the authentication session rotates, otherwise legitimate clients may receive 403 responses.

  • Minimal APIs: Attributes are unavailable; register AddAntiforgery() and validate manually via IAntiforgery.ValidateRequestAsync in the endpoint delegate.

  • JSON APIs: Set HeaderName in AntiforgeryOptions so the token can be sent in a header rather than a form field.

72

What caching options are available when building an ASP.NET Web API, and how do they differ?

ASP.NET Core Web API provides several caching mechanisms that operate at different layers: output caching for complete HTTP responses, in-memory caching for application data on a single server, and distributed caching for shared state across multiple instances. The right choice depends on whether you are caching full GET responses or individual data items, and whether your API scales to more than one server.

Read full answer

Applies toASP.NET Core Web API on .NET 8

Output Caching (Recommended for Responses)

Output caching, introduced in .NET 7, is the current recommended way to cache HTTP responses. It stores the full response (status code, headers, and body) and short-circuits the pipeline for subsequent matching requests.

  • Configuration: builder.Services.AddOutputCache() and app.UseOutputCache() in Program.cs.

  • Usage: apply [OutputCache(Duration = 60)] to a controller action or endpoint, or define named policies for finer control.

  • Features: supports varying by query string, headers, or custom values; supports tags for bulk eviction.

  • Response Caching ([ResponseCache] and the response caching middleware) still exists in .NET 8 but is largely superseded by output caching for server-side scenarios.

In-Memory Caching

IMemoryCache stores objects in the memory of a single application process.

  • Setup: builder.Services.AddMemoryCache(), then inject IMemoryCache.

  • API: GetOrCreate or TryGetValue with absolute or sliding expiration via ICacheEntry.

  • Best for: fast lookups of computed values, reference data, or results of expensive calls on a single-instance deployment.

  • Limitation: data is lost on restart and is not shared when the API runs on multiple servers or in a scale-out.

Distributed Caching

IDistributedCache abstracts an external cache store shared across servers.

  • Redis: the most common choice, enabled with AddStackExchangeRedisCache from the Microsoft.Extensions.Caching.StackExchangeRedis package.

  • Other providers: SQL Server, NCache, or the in-memory distributed implementation (development only, not for production).

  • API: async methods GetStringAsync, SetStringAsync, RefreshAsync, and byte-array equivalents.

  • Best for: multi-server deployments, cached data that must survive restarts, or shared session-like state.

Comparison

Mechanism Caches Scope Cross-server Typical use
Output Caching HTTP responses Per endpoint/policy In-memory per server Repeated identical GET requests
IMemoryCache Application objects Single process No Expensive computations, local lookups
IDistributedCache Application objects External store Yes Shared data, multi-instance APIs

Best Practices and Pitfalls

  • Cache only safe responses: output caching is appropriate for idempotent GET requests; never cache responses that contain user-specific or sensitive data without varying appropriately.

  • Set explicit expiration: always define absolute or sliding expiration so stale data does not serve indefinitely.

  • Invalidate deliberately: use output cache tags or remove keys from IMemoryCache/IDistributedCache when underlying data changes.

  • Watch memory growth: IMemoryCache can grow unbounded; set size limits or use a distributed cache for large datasets.

  • Do not cache error responses unless you intentionally want to suppress repeated failures; output caching by default does not cache 4xx/5xx responses.

  • Prefer output caching for full responses and in-memory/distributed caching for individual data items; mixing both on the same data can cause inconsistency.

73

Explain how server-side caching functions in an ASP.NET Web API and which caching mechanisms the framework provides to store and reuse responses or data.

Server-side caching in ASP.NET Core Web API keeps computed responses or frequently accessed data in fast storage on the server so later requests can be served without re-running expensive work such as database queries. The framework offers three main mechanisms: in-memory caching through IMemoryCache, distributed caching through IDistributedCache, and output caching for whole HTTP responses, which is the newer, recommended approach from .NET 7 onward.

Read full answer

Applies toASP.NET Core Web API on .NET 8

What Server-Side Caching Is

  • The server stores a copy of a result (an object, a list, or a full serialized HTTP response) in memory or a shared cache store.

  • On a repeat request, the cached copy is returned directly, cutting database round-trips, CPU work, and latency.

  • This differs from client-side caching, where Cache-Control headers instruct browsers or CDNs to store the response; server-side caching lives entirely on your API servers.

Caching Layers Available

Mechanism Scope Best for
IMemoryCache Single server process Fast, simple data caching; lost on restart or scale-out
IDistributedCache (Redis, SQL Server) Shared across servers Scaled-out deployments, server farms
Output caching ([OutputCache]) Whole HTTP response .NET 7+; replaces the older response caching middleware for output scenarios

The older [ResponseCache] attribute and response caching middleware still exist, but output caching is preferred for new .NET 7+ development because it is more flexible and policy-driven.

How It Works: The Cache-Aside Pattern

  1. A request arrives at the controller action.

  2. The code builds a cache key and checks the cache.

  3. On a hit, the cached value is returned immediately.

  4. On a miss, data is fetched from the database, stored in the cache with an expiration, then returned.

  5. On writes (POST, PUT, DELETE), the related cache entry is invalidated so stale data is never served.

Expiration can be absolute (expires at a fixed time) or sliding (resets on each access); absolute expiration is safer for data that changes. With IMemoryCache, set these via MemoryCacheEntryOptions; IDistributedCache uses DistributedCacheEntryOptions. Data caching is typically done in the service or repository layer, or directly in the controller for simple cases.

Output Caching in .NET 7+

  • Register with AddOutputCache() in Program.cs and enable the middleware with UseOutputCache() before MapControllers() or endpoint mapping.

  • Apply [OutputCache(Duration = 60)] to actions or endpoints; Duration is in seconds.

  • Use VaryByQueryKeys, VaryByHeader, or VaryByRouteValue to store separate variants (for example, one cache entry per ?include=details value).

  • By default it only caches safe, cacheable requests such as GET and HEAD, preserving correct HTTP semantics.

  • The default output cache store is in-memory per server; it does not automatically share entries across a server farm, so plan accordingly for scaled-out deployments.

Best Practices and Common Pitfalls

  • Invalidate on writes: always remove or update cache entries in POST, PUT, PATCH, and DELETE handlers.

  • Use distributed cache when scaling out: IMemoryCache is per-server, so instances diverge behind a load balancer.

  • Set absolute expiration to guarantee stale entries eventually expire even if invalidation is missed.

  • Vary cached responses by query string, header, or user claim when output differs per client; never cache user-specific data under a shared key.

  • Avoid caching sensitive data in shared stores like Redis without encryption.

  • Limit memory growth on IMemoryCache using size-based policies to prevent unbounded memory use.

74

Describe what middleware does in the ASP.NET Core Web API request pipeline and why the order of middleware components matters.

Middleware is a chain of components that every HTTP request passes through on its way into your API and that every response passes through on its way back out. Each component can inspect, modify, or short-circuit the request before handing it to the next component, which is how cross-cutting concerns such as exception handling, HTTPS redirection, CORS, authentication, authorization, rate limiting, routing, and endpoint execution are composed into one pipeline.

Read full answer

Applies toASP.NET Core Web API on .NET 8 (controller-based APIs and Minimal APIs share the same pipeline).

How the Pipeline Works

The pipeline is built at startup from a sequence of Use*, Map*, and Run extension methods on the WebApplication instance. Each middleware receives the current HttpContext and a RequestDelegate pointing to the next component.

  • Request direction: components execute in registration order, so the first registered middleware sees the raw request first.

  • Response direction: when a component calls await next(context) and the pipeline unwinds, the same component runs again in reverse order, so it can post-process the response.

  • Short-circuiting: if a middleware never calls next, no later component runs. This is how UseStaticFiles, the rate limiter, or an authorization failure can end the request without reaching endpoint execution.

  • Terminal middleware: Run registers a component that always terminates the chain, which is why it is typically placed last.

Extension Purpose Calls next?
Use Wraps the next component; can act before and after it Yes
Map / MapWhen Branches the pipeline on a path or a predicate Only within the branch
UseWhen Conditionally adds middleware while keeping a single main flow Yes, when the predicate matches
Run Ends the pipeline No

Built-in Middleware You Typically Configure

  • UseExceptionHandler: catches unhandled exceptions and, with AddProblemDetails(), converts them into RFC 9457 application/problem+json responses.

  • UseHsts / UseHttpsRedirection: enforce HTTPS; HSTS should only be enabled in production.

  • UseCors: applies the named CORS policy; never combine AllowAnyOrigin with AllowCredentials.

  • UseAuthentication / UseAuthorization: UseAuthentication must come before UseAuthorization so that the authenticated user exists before policies are evaluated.

  • UseRateLimiter: the built-in .NET 7+ rate limiter, configured through AddRateLimiter.

  • Routing and endpoints: UseRouting selects the route, and the endpoint middleware executes the matched controller action or Minimal API delegate through MapControllers, MapGet, and similar calls.

Why Ordering Matters

Order determines which component sees which state of the request. Exception handling must be early so that failures thrown by later components are caught. CORS must precede authentication when preflight requests need to succeed without credentials. Authentication must precede authorization, and both must precede endpoint execution. Rate limiting usually sits early so that rejected requests never reach routing or the data layer. A middleware registered too late simply will not run for requests that an earlier component short-circuits.

Custom Middleware

Custom middleware is a class with an InvokeAsync(HttpContext context, RequestDelegate next) method, registered through an extension method. It can take scoped services through constructor injection when it implements IMiddleware, which the built-in container resolves per request. Prefer IMiddleware when the middleware needs scoped dependencies such as a database context or a logging scope, and keep the class stateless otherwise.

Best Practices and Common Pitfalls

  • Register exception handling first so every later failure becomes a controlled problem-details response.

  • Keep the pipeline minimal in tests: WebApplicationFactory runs the full pipeline by default, so ordering bugs reproduce there too.

  • Do not create per-request work in middleware such as opening connections without disposal; middleware runs on the request thread and leaks show up under load.

  • Version your OpenAPI choices deliberately: on .NET 8 the template uses Swashbuckle (AddSwaggerGen / UseSwagger), while from .NET 9 the built-in Microsoft.AspNetCore.OpenApi (AddOpenApi / MapOpenApi) replaces it.

  • Verify order with logging: the order in Program.cs is the order at runtime, and a misplaced UseAuthorization is a common cause of unexpected 401 or 403 responses.

75

In ASP.NET Core, how does middleware facilitate the handling of cross-cutting concerns throughout the request processing pipeline?

Middleware in ASP.NET Core enables the implementation of cross-cutting concerns by letting developers insert reusable processing logic into the HTTP request pipeline before requests reach controllers or minimal API endpoints. Because every request passes through the same chain, concerns such as logging, authentication, CORS and error handling are applied consistently across all routes without duplicating code inside individual actions.

Read full answer

Applies toASP.NET Core 8 on .NET 8

Pipeline Architecture

The request pipeline is a sequence of middleware components arranged in the order they are registered in Program.cs. Each component receives an HttpContext and a RequestDelegate that represents the next component in the chain. - Invocation: A component runs logic before and after calling await next(context), so it can observe and modify both the incoming request and the outgoing response. - Short-circuiting: A component can stop the pipeline by returning without calling next, which is how authentication failures or rate-limit rejections prevent downstream work. - Termination: The last component produces the response, typically the endpoint middleware dispatching to a controller action or minimal API handler. - Ordering matters: components are executed in registration order on the way in and in reverse order on the way out, so exception handling must be registered first to catch failures from later components.

Typical Use Cases

Middleware is the standard location for logic that spans multiple endpoints. - Authentication: validates tokens and builds the ClaimsPrincipal before authorization runs. - Authorization: verifies that the authenticated user is allowed to reach the endpoint. - Logging and diagnostics: records request metadata, duration, correlation IDs and response status codes. - CORS: enforces cross-origin policies before resources are accessed. - Response compression and caching: reduces payload size and improves performance for supported clients. - Rate limiting: the built-in AddRateLimiter middleware (.NET 7+) throttles requests globally or per policy. - Global exception handling: catches unhandled exceptions and converts them into standardized error responses.

Error Handling Standards

Global exception handling middleware catches unhandled exceptions and returns standardized Problem Details. In .NET 8, calling AddProblemDetails() configures the framework to produce application/problem+json responses compliant with RFC 9457 (which replaces RFC 7807). Controllers and minimal API endpoints should use Problem() or ValidationProblem() for explicit business and validation errors, while middleware handles unexpected failures. This replaces manual JSON serialization of error payloads and ensures a consistent error shape across the whole API. With [ApiController], an invalid model still returns a 400 automatically.

How It Keeps Cross-Cutting Logic Separate

  • Single registration, global effect: a concern is configured once in Program.cs instead of being repeated in every action or endpoint.

  • Composability: components can be combined, reordered or replaced without touching application code, so an API can add rate limiting or OpenAPI without modifying controllers.

  • Consistency: policies such as authentication, CORS and error formatting are enforced uniformly, including for endpoints added later.

  • Testability: middleware can be unit tested with a constructed HttpContext and a stub RequestDelegate, independently of the business logic it wraps.

Best Practices

  • Keep logic focused: each middleware should handle a single concern so it stays small, understandable and testable.

  • Avoid blocking calls: use async methods throughout to prevent thread pool starvation.

  • Secure defaults: never combine AllowAnyOrigin with credentials when configuring CORS.

  • Dependency scope: resolve scoped services within InvokeAsync rather than injecting them into the constructor, to avoid captive dependency issues.

  • Ordering: place exception handling first, followed by authentication and authorization, then functional middleware such as CORS, compression, rate limiting and custom components.

76

What factors should you keep in mind when hosting an ASP.NET Core Web API on Azure?

Deploying an ASP.NET Core Web API to Azure is mostly about matching the Azure App Service (or container-based) hosting model to your app’s configuration, security, scaling and observability needs, rather than changing the application code itself.

Read full answer

Applies toASP.NET Core Web API on .NET 8, deployed to Azure App Service (Linux and Windows) and Azure Container Apps.

Hosting Model Options

Option When to use Notes
Azure App Service (Linux or Windows) Most line-of-business HTTP APIs; simplest PaaS Supports deployment slots, autoscale rules, custom domains, managed certificates, VNet integration
Azure Container Apps Containerized APIs, microservices with KEDA scaling Runs the published Docker image; good when you already containerize
Azure Kubernetes Service (AKS) Large multi-service estates needing full cluster control Most operational overhead of the three

For App Service, deploy the platform-independent build (dotnet publish) rather than a self-contained Windows build unless you have a specific reason; Linux plans are generally cheaper for .NET 8.

Configuration, Secrets and Security

  • App settings override appsettings.json: App Service application settings are injected as environment variables and take precedence over file configuration, so keep environment-specific values (connection strings, API endpoints, feature flags) in the portal or an Infrastructure-as-Code template, not in the repo.

  • Prefer app settings over App Service “connection strings” for ASP.NET Core: connection strings configured in the portal are injected as environment variables with prefixes such as CUSTOMCONNSTR_ or SQLAZURECONNSTR_, so they do not map directly onto ConnectionStrings:<name> without accounting for the prefix.

  • Never store secrets in source control or plain app settings: reference them from Azure Key Vault using the @Microsoft.KeyVault(...) reference syntax in app settings, or read them from code with DefaultAzureCredential and a managed identity.

  • Use a managed identity (system or user assigned) to access Key Vault, Azure SQL, Service Bus and Storage instead of connection strings with passwords.

  • Force HTTPS: bind a certificate (App Service Managed Certificate is free for custom domains), enable “HTTPS Only”, and let ASP.NET Core redirect HTTP to HTTPS. HSTS is enabled by default in the production configuration of the .NET 8 templates.

  • Forwarded headers: App Service terminates TLS in front of Kestrel, so add the forwarded headers middleware if your code relies on Request.Scheme, RemoteIpAddress or the original host.

  • CORS: configure an explicit allow-list of origins in your API (or via App Service CORS only when you cannot change the code). Never combine AllowAnyOrigin with AllowCredentials.

  • Authentication and authorization: validate JWT bearer tokens in the API (AddAuthentication().AddJwtBearer(...)) or delegate to Microsoft Entra ID Easy Auth; call UseAuthentication() before UseAuthorization().

  • Network isolation: use VNet integration for outbound calls to private resources, Private Endpoint for inbound, and restrict scm (Kudu) access to specific IP ranges.

Performance, Scaling and Rate Limiting

  • Plan choice: Free/F1 for experiments, Basic for low traffic, PremiumV3 (P1v3/P2v3) for production; Isolated for compliance-driven isolation.

  • Autoscale rules: scale out on CPU, memory, queue length or a custom metric; always set a maximum instance count to control cost.

  • Sticky sessions (ARR affinity): enable them only when your API keeps in-memory per-client state. The built-in rate limiter (AddRateLimiter, .NET 7+) also needs sticky sessions or a shared/distributed store such as Redis when the app runs on multiple instances.

  • Always On: required if the API hosts background services or WebJobs, otherwise the site is unloaded after idle time.

  • 64-bit: prefer 64-bit processes for memory-heavy workloads; the default depends on the plan and platform.

Observability

  • Enable Application Insights (connection string as an app setting) to get request traces, dependency calls, exceptions and metrics with no code changes, or add telemetry explicitly in Program.cs.

  • Use structured logging with ILogger<T> and correlate requests with a traceparent/Activity id.

  • Expose health checks (AddHealthChecks + MapHealthChecks) and wire them to the App Service health check feature so unhealthy instances are removed from rotation.

Deployment and CI/CD

  • Use deployment slots (Standard tier and above) for staging: deploy, warm up, test against production settings with “swap with preview”, then swap — the target slot’s settings apply, so configure slot-specific settings such as staging connection strings.

  • Automate with GitHub Actions, Azure DevOps or az webapp deploy; publish the build output or a container image from a pipeline, never from a developer machine.

  • Keep the deployment package small: exclude appsettings.Development.json, test assemblies and source files from the publish output.

Common Pitfalls

  • Storing secrets in appsettings.json and pushing them to a public repository.

  • Assuming Request.Host and the client IP are the original values without forwarded headers.

  • Enabling CORS with any origin plus credentials, or relying on App Service CORS as the only protection.

  • Forgetting that new instances read the same app settings, so anything cached in process memory (auth keys, limiter counters) must be distributed or pinned with sticky sessions.

  • Choosing a Free/Shared plan for a production API and hitting daily quotas with no SSL bindings.

77

What steps make an ASP.NET Core Web API hosted on Azure App Service serve traffic over HTTPS and reject plain HTTP, including binding a certificate for a custom domain?

Azure App Service already exposes your app’s default *.azurewebsites.net endpoint over HTTPS using a Microsoft-managed certificate, so “enabling HTTPS” in practice means two things: turning on the platform-level HTTPS Only setting so every HTTP request is redirected to HTTPS, and binding a certificate (ideally an App Service Managed Certificate) to any custom domain you map to the API.

Read full answer

Applies toASP.NET Core Web API on .NET 8, hosted on Azure App Service (Windows and Linux plans; App Service Managed Certificates require Basic tier or above).

How TLS Is Handled on Azure App Service

  • TLS termination at the front end: App Service terminates TLS on its load-balanced front end and forwards plain HTTP to your app instance over an isolated network segment. Your Kestrel process therefore sees http:// requests even when clients connect with https://.

  • Default endpoint certificate: the *.azurewebsites.net hostname gets a public, Microsoft-trusted certificate automatically, so https://<app>.azurewebsites.net works immediately with no configuration.

  • Custom domains need their own binding: after mapping a custom hostname (A or CNAME record), you must attach a certificate and create a TLS/SSL binding, otherwise the browser reports a hostname mismatch.

Forcing HTTPS-Only Traffic

  • Portal: App Service → Configuration → General settings → set HTTPS Only to On. Plain HTTP requests are then redirected to HTTPS at the platform edge before they reach your code.

  • Infrastructure as code: the same setting is the httpsOnly: true property on the Microsoft.Web/sites resource in ARM/Bicep/Terraform, which keeps the configuration versioned with your deployment.

  • In-app redirection: app.UseHttpsRedirection() is a second layer of defense, but because TLS is terminated upstream you may have to state the port explicitly, otherwise the middleware cannot infer it and fails to redirect.

Binding a Certificate to a Custom Domain

Option Cost Auto-renew Custom domain Notes
Default *.azurewebsites.net certificate Free Yes No Already enabled; covers the Azure hostname only
App Service Managed Certificate Free Yes Yes Basic tier and above; requires the domain to resolve to the app; not for Free/Shared tiers
Custom certificate in Azure Key Vault Your CA cost Depends on CA Yes Bring-your-own cert, ideal for wildcard, EV or cross-domain scenarios
Azure Front Door managed certificate Included in service Yes Yes TLS terminates at the edge; the App Service itself stays HTTP-only behind Front Door

For a custom domain, the recommended path is App Service Managed Certificate: in Custom domains, select the hostname, choose Add certificate → App Service Managed Certificate, then create an SNI-based TLS binding. Renewal is automatic before expiry. Store purchased certificates in Azure Key Vault and reference them from TLS/SSL settings so secrets stay out of the repository; the same Key Vault approach is required if you terminate TLS on Application Gateway, which has no managed certificates for custom listeners.

Code Example

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails();

// HSTS tells browsers to always use HTTPS for this host (ignored by non-browser API clients)
builder.Services.AddHsts(options =>
{
    options.Preload = true;
    options.IncludeSubDomains = true;
    options.MaxAge = TimeSpan.FromDays(365);
});

// App Service terminates TLS upstream, so state the port explicitly
builder.Services.Configure<HttpsRedirectionOptions>(options => options.HttpsPort = 443);

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseHsts(); // never in development
}

app.UseHttpsRedirection();   // complements the Azure "HTTPS Only" setting
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

app.Run();

Best Practices and Common Pitfalls

  • Raise the minimum TLS version: in Configuration → General settings → TLS setting, require TLS 1.2 as a minimum (TLS 1.3 where your plan supports it) so legacy clients cannot negotiate weak ciphers.

  • Prefer the platform HTTPS Only toggle over code-only redirection: it blocks insecure traffic at the edge and needs no deployment when certificates change.

  • Do not leave HTTP enabled “just for health checks”: Azure’s health probe paths work over HTTPS; use them instead of opening port 80.

  • Remember HSTS is browser-only: mobile apps and service-to-service clients ignore it, so it cannot be your only enforcement mechanism.

  • Watch for mixed-origin clients: after enabling HTTPS Only, update any client still calling http://, since the redirect adds latency and breaks clients that reject redirects.

  • Front Door / API Management users: terminate TLS at the edge and keep the origin restricted, otherwise the App Service hostname can still be reached directly over HTTP.

78

Explain the principle of HATEOAS and discuss its significance when designing RESTful services.

HATEOAS (Hypermedia as the Engine of Application State) is a REST constraint in which clients navigate an API through hypermedia links embedded in responses rather than by constructing URLs themselves. This decouples clients from the server’s URI structure, so an API can evolve without breaking existing clients.

Read full answer

Applies toASP.NET Core Web API on .NET 8 (the principle itself is version-independent)

Core Concept

HATEOAS represents Level 3 of the Richardson Maturity Model. Instead of knowing endpoints such as /api/products/1 in advance, a client receives a representation that contains links to related actions, such as self, update, or delete. Each response acts like a state machine: it tells the client which transitions are valid right now, based on the current resource state and the caller’s permissions.

Importance in RESTful Design

  • Loose coupling: Clients depend on relation types (for example rel="next") rather than hardcoded paths. If the server restructures its URIs, well-written clients keep working because they follow links instead of building them.

  • Discoverability: New capabilities can be introduced by adding links to existing responses, and clients can take advantage of them without a documentation update or redeployment.

  • Server-driven state management: The server decides which transitions are allowed. For example, an order resource exposes a cancel link only while the order status is Pending, and a ship link only once payment is confirmed.

  • Self-documenting interactions: A client that understands the media type and link relations needs little prior knowledge of the API, which is the main goal REST sets for machine-to-machine clients.

Hypermedia vs. Non-Hypermedia REST

Aspect Without HATEOAS (Level 2) With HATEOAS (Level 3)
Client knowledge Must know URI templates and available actions in advance Learns available actions from links in each response
URI changes Break clients that hardcode paths Clients keep working if they follow links
Workflow logic Client decides which action is valid next Server advertises valid next steps
Payload size Smaller, resource-only Larger, links add bytes and parsing work
Adoption effort Simple, familiar tooling Requires a link model, media-type design and client support

Implementation Guidance for ASP.NET Core

ASP.NET Core has no built-in HATEOAS middleware or formatter, so hypermedia is added explicitly. The usual approach is to define a small link type holding a relation (rel), a target URI (href) and an HTTP method, attach a list of such links to response DTOs, and populate them in the controller with the URL helper (Url.Action or Url.Link) before returning the resource with Ok. Alternatives include custom action filters that enrich results, or third-party packages that generate links from route metadata. Whatever approach is chosen, keep controller-based APIs ([ApiController] with ActionResult<T> or IActionResult) and Minimal APIs distinct; both can return hypermedia, but Minimal APIs construct links from IUrlHelper or LinkGenerator rather than a controller’s Url property.

Best Practices and Pitfalls

  • Prefer standard relation types: Use IANA-registered relations such as self, next, prev, collection and item; document custom relations clearly and use URIs to identify them.

  • Secure your links: Only emit links for actions the caller is actually authorized to perform, and avoid exposing internal route details; hiding a link is not a substitute for server-side authorization checks.

  • Mind the media type: Ideally describe links in the response’s media type (for example a profile or a JSON hypermedia format) rather than as an ad-hoc links array, so generic clients can interpret them.

  • Weigh the cost: HATEOAS increases payload size and client complexity. It pays off for rich, stateful workflows; for simple internal CRUD APIs, plain Level 2 services are often the pragmatic choice.

  • Keep error handling consistent: Return errors as Problem Details (application/problem+json, RFC 9457) using Problem() and ValidationProblem(), and remember that with [ApiController] an invalid model automatically returns a 400 response.

80

Which instrumentation and monitoring solutions are available for tracking the operational health and performance metrics of an ASP.NET Core Web API?

ASP.NET Core Web API on .NET 8 provides built-in health checks for liveness and readiness, alongside OpenTelemetry integration for distributed tracing and metrics collection to monitor performance. These tools allow developers to verify dependency availability and track request latency, error rates, and throughput across distributed systems.

Read full answer

Applies toASP.NET Core Web API on .NET 8

Built-in Health Checks

The Microsoft.Extensions.Diagnostics.HealthChecks middleware enables endpoints that report application status without requiring external libraries. You register checks via builder.Services.AddHealthChecks() and expose them using app.MapHealthChecks("/health"). The middleware returns HTTP 200 when healthy and HTTP 503 when unhealthy. Checks can verify database connections, external API availability, or disk space by implementing IHealthCheck. Separate endpoints are often used for liveness (process running) and readiness (dependencies ready) to orchestrate container restarts correctly in Kubernetes or Azure Container Apps.

Performance Monitoring with OpenTelemetry

OpenTelemetry (OTel) is the standard for observability in .NET 8, replacing older proprietary SDKs for many use cases. By adding OpenTelemetry.Extensions.Hosting, you configure tracing and metrics within Program.cs. Traces capture request spans, including SQL queries or HTTP calls made via IHttpClientFactory, providing a distributed view of transactions. Metrics track counters, histograms, and gauges such as request duration or active requests. Data is exported to backends like Azure Monitor, Prometheus, or Jaeger via OTLP exporters, enabling alerting and dashboard visualization. Additional diagnostics tools include .NET Event Counters, dotnet-trace, dotnet-monitor, and Azure Monitor Application Insights, which is increasingly aligned with OpenTelemetry in .NET 8.

Logging and Diagnostics

Structured logging via ILogger<T> complements metrics by providing context for failures and business events. Unhandled exceptions should return Problem Details (RFC 9457) using Problem() or ValidationProblem(), which monitoring tools parse to track error rates consistently. Log levels (Information, Warning, Error) should be configured based on environment, with verbose logging reserved for Development. Correlation IDs help trace a single request across logs and traces. Exceptions should be logged with logger.LogError(exception, ...) to capture stack traces. The Developer Exception Page should only be enabled in Development, whereas Production relies on centralized log aggregation.

Best Practices and Pitfalls

  • Secure Health Endpoints: Expose /health only internally or behind authentication to prevent information leakage about internal dependencies.

  • Lightweight Checks: Ensure health check logic does not consume significant resources or depend on the very system being tested, which could cause cascading failures.

  • Trace Sampling: In high-traffic systems, configure trace sampling to avoid overwhelming storage backends and incurring excessive costs.

  • Alerting: Configure alerts on 503 responses from health checks or elevated error rates in metrics rather than relying on manual dashboard inspection.

  • Consistency: Use consistent naming for metrics and tags to facilitate aggregation across different services in a microservices architecture.

  • Avoid Secrets: Never log connection strings, passwords, or PII; use configuration providers like Azure Key Vault for sensitive values.

81

What approaches make an ASP.NET Core Web API usable by the broadest possible variety of client applications?

An API is consumable by the widest range of clients when it speaks standard HTTP, negotiates content, documents itself, versions safely, and allows controlled cross-origin access. The umbrella term for this is interoperability, and in ASP.NET Core it is achieved through content negotiation, OpenAPI, API versioning, CORS, consistent status codes, and RFC 9457 Problem Details error bodies.

Read full answer

Applies toASP.NET Core Web API on .NET 8 (Swashbuckle for OpenAPI; the `Asp.Versioning.Mvc` package for versioning; built-in rate limiting from .NET 7).

Content Negotiation and Standard Media Types

  • JSON by default: AddControllers() returns JSON out of the box, and System.Text.Json serializes property names with camelCase, which JavaScript and mobile clients expect.

  • Content negotiation: the framework selects the formatter from the client’s Accept header. AddXmlSerializerFormatters() can support legacy XML/SOAP-era integrations, but JSON alone is the modern norm.

  • Prefer standard media types: application/json works with browsers, Postman, and code generators without special configuration; reserve custom media types for well-defined extensibility scenarios.

  • Stable payloads: ignore nulls or use consistent DTOs so generated clients and caches do not see shifting shapes.

Consistent HTTP Semantics and Errors

  • Correct verbs and codes: GET, PUT, and DELETE are idempotent; POST is not. Return 200 OK, 201 Created with a Location header, 204 No Content, 400, 401, 403, 404, 409, and 500 where each fits.

  • Problem Details: register application/problem+json support with AddProblemDetails() and return Problem() or ValidationProblem(). With [ApiController], an invalid model automatically returns 400 with a validation problem body.

  • Predictable error shape: one machine-readable error format lets every client, from curl to mobile SDKs, parse failures the same way.

API Versioning

  • Version from day one so released clients never break. Use the current Asp.Versioning.Mvc package; Microsoft.AspNetCore.Mvc.Versioning is deprecated.

  • Choose a strategy that matches your clients:

Strategy Example Best for
URI segment /api/v1/products Third-party and browser clients; most discoverable
Query string /api/products?api-version=1.0 Existing URLs that cannot change
Header api-version: 1.0 Clean URLs and internal clients
  • Report available versions (ReportApiVersions) so clients can detect upgrades.

Cross-Origin Requests (CORS)

  • Browsers block cross-origin calls unless the API opts in. Configure CORS explicitly with AddCors/UseCors and name exact origins.

  • Never combine AllowAnyOrigin() with AllowCredentials(); that combination is invalid and insecure. Use WithOrigins(...) when credentials are required.

Self-Documentation, Authentication, and Resilience

  • OpenAPI: in .NET 8 the template uses Swashbuckle (AddSwaggerGen/UseSwagger/UseSwaggerUI); from .NET 9 the built-in Microsoft.AspNetCore.OpenApi (AddOpenApi/MapOpenApi) replaces it. The document drives client generators, test UIs, and onboarding.

  • API style: controller-based APIs use [ApiController] and ActionResult<T>; Minimal APIs with TypedResults are a lightweight alternative and expose the same OpenAPI metadata. Keep the two styles distinct within one app.

  • Security: add JWT bearer authentication or ASP.NET Core Identity, and call UseAuthentication() before UseAuthorization().

  • Resilience: publish rate limits with AddRateLimiter so clients can implement back-off, and call other HTTP services through IHttpClientFactory rather than creating HttpClient per request.

Best Practices and Common Pitfalls

  • Do: version from the start, return Problem Details for every error, keep JSON casing stable, document with OpenAPI, and use IHttpClientFactory for outbound calls.

  • Avoid: raw strings or ad-hoc error shapes, AllowAnyOrigin with credentials, breaking released routes, and mixing classic Web API 2 patterns (IHttpActionResult, WebApiConfig) into ASP.NET Core.

  • Pipeline order: HTTPS redirection and CORS early, authentication before authorization, and endpoint mapping last.

82

What are the prevalent architectural patterns and design practices employed when building modern web APIs?

Modern API development relies on separation of concerns and standardized communication patterns to ensure maintainability and interoperability. Key approaches include layered architecture, CQRS, and standardized error handling via Problem Details.

Read full answer

Applies toASP.NET Core Web API on .NET 8

Core Architectural Patterns

  • Layered Architecture: Separates concerns into distinct layers (API, application/services, domain, infrastructure), keeping controllers thin and business logic testable.

  • Repository Pattern: Abstracts data access logic behind interfaces, allowing easy swapping of data sources and simplifying unit testing.

  • Unit of Work: Groups multiple database operations into a single transaction, ensuring consistency across aggregates.

  • CQRS (Command Query Responsibility Segregation): Separates read models from write models, optimizing performance for complex queries while keeping write logic focused.

  • Mediator Pattern: Decouples request handling from execution logic using a central mediator, often reducing controller bloat.

Standardized Response and Error Handling

APIs should return consistent shapes for success and failure. Use ActionResult<T> for typed responses and standard HTTP status codes: 200 for OK, 201 Created with a Location header, 204 No Content, and 404 for missing resources. For errors, adhere to RFC 9457 Problem Details (which replaces RFC 7807), returning application/problem+json. Register the service via AddProblemDetails(). With [ApiController], invalid models automatically return 400; explicit errors use Problem() or ValidationProblem().

API Versioning

As APIs evolve, breaking changes must be managed without disrupting existing clients. Use the Asp.Versioning.Mvc package (the modern replacement for the deprecated Microsoft.AspNetCore.Mvc.Versioning). Configure versioning via URL segment, query string, or header. Always document versions using OpenAPI/Swagger in .NET 8 (AddSwaggerGen); from .NET 9 the built-in Microsoft.AspNetCore.OpenApi replaces Swashbuckle.

Security and Resilience

  • Authentication: Use JWT bearer tokens or ASP.NET Core Identity. Ensure UseAuthentication() precedes UseAuthorization() in the middleware pipeline.

  • CORS: Configure explicit policies; never combine AllowAnyOrigin with AllowCredentials.

  • Rate Limiting: Enable built-in rate limiting via AddRateLimiter() (.NET 7+) to prevent abuse.

  • Outbound Calls: Use IHttpClientFactory (named or typed clients) for external HTTP dependencies to manage socket exhaustion and enable resilience policies like retries; use async all the way.

Best Practices

  • Keep controllers thin; move business logic to services or handlers.

  • Use async/await all the way through the call stack to avoid thread pool starvation.

  • Validate input early and return clear validation errors.

  • Document endpoints thoroughly using Swagger/OpenAPI attributes.

  • Avoid exposing internal exception details in production responses.

  • Consider Minimal APIs with TypedResults as a lighter alternative to controller-based APIs for simpler endpoints.

83

How do you coordinate API development at scale when several teams are delivering services at the same time?

Large-scale API work is managed by combining vertical, domain-owned slices with a thin layer of shared governance: each team owns its APIs end to end, while a central platform team supplies the standards, tooling and shared infrastructure that keep the portfolio consistent. The goal is to let teams move independently without fragmenting the API surface.

Read full answer

Applies toASP.NET Core Web API on .NET 8 (controller-based APIs with `[ApiController]`; Minimal APIs are the lighter alternative).

Domain-Owned Vertical Slices

Split the portfolio by bounded domain rather than by technical layer, and give each team ownership of its controllers, data stores, contracts and deployment pipeline.

  • Team per domain: orders, billing, identity and catalog are separate APIs, each with its own repository and release cadence.

  • Independent deployability: each API ships through its own CI/CD pipeline; a billing release must not require a catalog redeployment.

  • Shared data is a red flag: if two services need the same table, treat it as a sign the domain boundary is wrong, or expose the data through an API rather than a shared database.

  • Keep controller-based services and Minimal API services distinct in the portfolio; both are valid, but mixing them inside one service blurs ownership.

Shared Contracts and Standards

Central consistency comes from standards, not from centralising the code.

  • Problem Details for errors: register AddProblemDetails() and return Problem() / ValidationProblem() so every service emits application/problem+json (RFC 9457) with the same shape.

  • Uniform HTTP semantics: GET/PUT/DELETE idempotent, POST non-idempotent; 200, 201 Created with a Location header, 204 No Content, 400, 401, 403, 404, 409 and 500 used identically across services.

  • Common authentication: JWT bearer tokens or ASP.NET Core Identity, with UseAuthentication() before UseAuthorization() in every host.

  • Explicit CORS policy per service: never combine AllowAnyOrigin() with AllowCredentials().

API Versioning

Versioning is what lets teams evolve without coordinating every change.

  • Use the Asp.Versioning.Mvc (or Asp.Versioning.Http for Minimal APIs) NuGet packages; Microsoft.AspNetCore.Mvc.Versioning is the old, deprecated package and should not be used for new work.

  • Prefer URL segment versioning (/api/v1/orders) for public APIs, optionally combined with version headers for internal consumers.

  • Deprecate, don’t break: ship v2 alongside v1 and communicate a removal date.

Platform Infrastructure

A platform team provides reusable building blocks so product teams don’t re-solve the same problems.

Concern Shared approach
Documentation OpenAPI per service; .NET 8 templates use Swashbuckle (AddSwaggerGen/UseSwagger), while .NET 9 replaces it with the built-in Microsoft.AspNetCore.OpenApi (AddOpenApi/MapOpenApi)
Discovery API gateway or internal developer portal listing all services
Rate limiting Built-in AddRateLimiter middleware (.NET 7+), with policies tuned per service
Outbound HTTP IHttpClientFactory with named or typed clients — never a new HttpClient() per request
Observability Correlation IDs, structured logging and tracing propagated across services

Governance and Workflow

  • Design review before code: an API review checks naming, status codes, versioning and error format against the published guidelines.

  • Contract tests: consumer-driven tests catch breaking changes before they reach production.

  • Feature branches plus API previews: publish a preview version so consumers can prepare.

  • Deprecation policy: a documented timeline, Deprecation/Sunset headers, and migration guides.

Common Pitfalls

  • A single “API team” becoming a bottleneck for every change.

  • Versioning introduced after the first breaking change, when it is already too late.

  • Inconsistent error payloads because each service hand-rolls its own error model.

  • Security middleware ordered incorrectly, so authorisation runs before authentication.

84

What are some common anti-patterns you should steer clear of when building Web APIs?

An anti-pattern in Web API development is a seemingly convenient practice that reliably leads to brittle, insecure, or hard-to-maintain APIs — such as returning raw entity models, blocking on async calls, or inventing non-standard error formats. Recognising them matters more than memorising rules, because interviewers want to hear why a pattern hurts and what the correct alternative is.

Read full answer

Applies toASP.NET Core Web API on .NET 8 (controller-based APIs using `[ApiController]` and `ControllerBase`); the same principles apply to Minimal APIs.

REST and HTTP Semantics Anti-Patterns

  • Verbs in URLs: endpoints like POST /api/products/GetAll or /api/products/delete/5 ignore HTTP semantics. Use GET /api/products, DELETE /api/products/5. The HTTP method carries the verb; the URL names the resource.

  • Wrong status codes: returning 200 OK for a created resource, a validation failure, or a not-found case. Use 201 Created with a Location header for inserts, 204 No Content for successful deletes/updates that return nothing, 400 for validation errors, 404 when the resource does not exist, 409 for conflicts, and 500 for unhandled server faults.

  • Non-idempotent PUT or DELETE: a PUT that partially mutates state or a DELETE that creates side effects on each call breaks the contract that repeating the same request is safe. POST is the only inherently non-idempotent verb.

  • Chatty endpoints: forcing clients to issue one call per property or per list item instead of supporting filtering, paging, and projection.

Error-Handling Anti-Patterns

  • Custom, inconsistent error envelopes: hand-rolled { "success": false, "errorMessage": "..." } shapes force every client to parse a proprietary format. Return Problem Details (application/problem+json, RFC 9457) via AddProblemDetails() and Problem() / ValidationProblem(); with [ApiController], an invalid model automatically produces a 400 with a standard validation problem response.

  • Leaking stack traces or internal IDs to clients in production — log the detail server-side, return a generic message in the response body.

  • Silently swallowing exceptions with an empty catch or returning 200 OK with an error payload, which hides failures from monitoring and misleads clients.

  • Using Result<T>-style wrappers everywhere when the framework already models HTTP outcomes with ActionResult<T> and Problem Details.

Data and Contract Anti-Patterns

  • Returning EF Core entities directly: this couples clients to your database schema, invites over-posting, and causes lazy-loading/serialization cycles or ObjectDisposedExceptions. Map to a DTO or record.

  • One giant DTO used for every operation: create separate request/response models so validation and serialization stay explicit.

  • Accepting or returning more data than needed, which widens the attack surface and slows responses.

  • No validation: relying on client-side checks only. Validate on the server with data annotations or a validator, and let [ApiController] turn failures into 400 responses.

Code-Level Anti-Patterns

  • Blocking on async: .Result, .Wait(), or .GetAwaiter().GetResult() in an action burns thread-pool threads and can deadlock; go async end-to-end.

  • new HttpClient() per request: socket exhaustion and stale DNS. Use IHttpClientFactory with named or typed clients.

  • Business logic and data access inside controllers: controllers should orchestrate; move logic into services and repositories so it is testable.

  • Shared static state or mutable singletons to pass data between requests; use scoped services and HttpContext.Items.

  • Catching Exception broadly instead of specific exception types, and forgetting to log before rethrowing.

  • No API versioning from day one: retrofitting versioning is painful. Use the Asp.Versioning.Mvc package; Microsoft.AspNetCore.Mvc.Versioning is the deprecated predecessor.

Security Anti-Patterns

  • Access-Control-Allow-Origin: * combined with AllowCredentials — browsers reject it, and it signals a misconfigured CORS policy. Define explicit origins.

  • Authentication middleware ordered after authorization: UseAuthentication() must come before UseAuthorization().

  • Rolling your own token or crypto, storing secrets in code, or accepting unvalidated input in queries that reach EF Core without parameterization.

  • No rate limiting on public endpoints: use the built-in AddRateLimiter middleware (.NET 7+) to protect against abuse.

Best Practices to Mention Instead

  • Model resources, use correct verbs and status codes, and keep payloads minimal with DTOs.

  • Return Problem Details for every error, with a consistent logging correlation ID.

  • Keep controllers thin, services async, and HTTP calls through IHttpClientFactory.

  • Version, document with OpenAPI (AddSwaggerGen/UseSwagger in .NET 8), and secure with JWT bearer or ASP.NET Core Identity plus an explicit CORS policy and rate limiting.

Sign in to keep reading

You've opened 2 full answers. Sign in to read all 84 ASP.NET Web API answers, mark what you know and pick up where you left off.

All Web and Mobile Development subjects