Create your own
Lesson illustration

Tracing the ASP.NET Core Request Pipeline

Welcome back. In the previous lesson, you traced how ASP.NET Core’s DI container creates a controller’s dependency graph and how scoped services such as DbContext live for a request. Now widen the lens: follow that request from the moment it enters the application until ASP.NET Core sends an HTTP response.

Priority: Must know. Interviewers often ask some version of: “What happens when a request reaches an ASP.NET Core API?” A senior-level answer should distinguish the outer middleware pipeline from the inner MVC/controller execution, explain where routing and validation occur, and identify the points at which a request can end early.

By the end, you should be able to narrate a POST /api/orders request clearly, including why an invalid JSON payload may result in a 400 without the controller action running.


The two layers: pipeline first, endpoint execution second

A useful mental model is that ASP.NET Core receives an HTTP request as an HttpContext, then passes that context through an ordered pipeline of middleware. Middleware handles cross-cutting concerns: exception handling, HTTPS enforcement, CORS, authentication, logging, and authorization are typical examples.

If the request survives this pipeline and routing finds a matching endpoint, the endpoint executes. For a controller API, that endpoint hands work to MVC, which activates the controller, binds parameters, validates the model, invokes the action, and turns the action result into an HTTP response.

For an API such as:

POST /api/orders?dryRun=false
Content-Type: application/json
Authorization: Bearer <token>

{
  "customerId": 42,
  "items": [
    { "productId": 10, "quantity": 2 }
  ]
}

the trace usually has these stages:

  1. The web server creates an HttpContext for the request.
  2. Middleware runs in the order configured in Program.cs.
  3. Routing identifies the endpoint that matches the HTTP method and URL.
  4. Authentication establishes a caller identity when configured.
  5. Authorization checks whether that caller may invoke the selected endpoint.
  6. MVC creates the controller through DI.
  7. Model binding obtains action parameters from the route, query string, headers, form, or request body.
  8. Model validation records binding and validation problems in ModelState.
  9. If valid, the controller action executes and returns a result.
  10. ASP.NET Core serializes that result, writes the response, and control returns outward through earlier middleware.

The sequence is not guaranteed to reach step 9. A static-file middleware, failed authorization check, invalid model, unmatched route, or exception can end processing before the action runs.


Middleware behaves like nested request handling

Each Use middleware can perform work both before and after invoking the next component. This makes the request path and response path run in opposite orders.

app.Use(async (context, next) =>
{
    logger.LogInformation("Request started: {Path}", context.Request.Path);

    await next();

    logger.LogInformation(
        "Request completed with {StatusCode}",
        context.Response.StatusCode);
});

On the way in, this middleware logs “started,” then calls next(). On the way out, after later middleware and the endpoint have completed, it logs the final status code.

A Run delegate is terminal: it does not receive or call next, so later middleware and endpoints do not execute. Middleware may also deliberately short-circuit. For example, static-file middleware can serve /assets/logo.svg without involving routing or an API controller. Authorization middleware can produce 401 Unauthorized or 403 Forbidden without invoking the controller action.

This is why a precise interview answer should say:

Middleware is an ordered, nested pipeline. Each component may inspect or modify the request before calling the next component, inspect or modify the response afterward, or short-circuit the request entirely.

The position of a middleware component is therefore a correctness and security decision, not just formatting in Program.cs.

DEEP DIVE: ASP.NET CORE Middleware Pipeline in .NET 9 🚀 1.5 HOURS

Watch DEEP DIVE: ASP.NET CORE Middleware Pipeline in .NET 9 by Frank Liu for a visual explanation of nested middleware, short-circuiting, and the significance of middleware order.

Watch pipeline basics to understand the request and response paths through a series of delegates. Then watch built in ordering for the practical purpose and typical placement of HTTPS, routing, and CORS middleware. Focus on why a component that does not call the next delegate prevents later stages from running.


A representative controller API pipeline

For interview discussion, it is helpful to show an explicit pipeline, even though modern WebApplication hosting can automatically add some routing and endpoint middleware when it is omitted.

var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();

app.UseExceptionHandler();

app.UseHttpsRedirection();

app.UseRouting();

app.UseCors("angular-client");

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

This is a representative order for an API. A real application may add rate limiting, request logging, localization, output caching, health checks, or custom correlation middleware, but the core reasoning stays the same.

Pipeline concernWhat it doesWhy placement matters
Exception handlingConverts unhandled downstream exceptions into a safe error response and logs themIt must be early enough to surround components that might throw.
HTTPS redirectionRedirects insecure HTTP requests to HTTPSIt should act before code depends on the request URL scheme.
RoutingMatches the request to endpoint candidates and selects oneLater middleware can inspect selected endpoint metadata.
CORSAdds or validates cross-origin policy behaviorIt must run before components that rely on CORS behavior; commonly before authentication and authorization.
AuthenticationAttempts to establish HttpContext.User from credentials such as a bearer tokenAuthorization needs an authenticated principal to evaluate.
AuthorizationApplies endpoint authorization metadata and policiesIt must occur after routing has selected the endpoint and after authentication.
Endpoint executionExecutes the controller endpoint selected by routingThis is where MVC binding, validation, and the action occur.

MapControllers() registers controller actions as endpoint candidates. In the modern hosting model, ASP.NET Core can add endpoint execution middleware automatically. For an interview, it is accurate to explain the conceptual distinction: routing selects an endpoint; endpoint execution invokes it.

One important nuance: authentication does not necessarily reject an anonymous request by itself. It tries to establish an identity. Authorization makes the access decision based on the selected endpoint’s [Authorize], policy, or anonymous-access metadata.


Routing selects an endpoint, not a controller instance

Consider this controller:

[ApiController]
[Route("api/orders")]
public sealed class OrdersController : ControllerBase
{
    [HttpGet("{id:int}")]
    public async Task<ActionResult<OrderDto>> GetById(int id)
    {
        // ...
    }

    [HttpPost]
    public async Task<ActionResult<OrderDto>> Create(
        CreateOrderRequest request,
        [FromQuery] bool dryRun = false)
    {
        // ...
    }
}

Routing uses endpoint metadata, including route templates and HTTP method constraints.

RequestSelected actionImportant result
GET /api/orders/42GetById(int id)42 satisfies the integer route constraint.
POST /api/orders?dryRun=trueCreate(CreateOrderRequest, bool)The path and HTTP method match HttpPost.
GET /api/orders/not-a-numberNo GetById matchThe {id:int} constraint does not match; the eventual result is normally 404.
DELETE /api/orders/42No action in this exampleThe URL may resemble a route, but no endpoint accepts DELETE; the result is normally 405 Method Not Allowed when another endpoint matches the route pattern.

Routing does not parse JSON and does not construct a controller merely to see whether an action can handle the request. It chooses an endpoint from route and method metadata. Once selected, that endpoint’s metadata becomes available to middleware in the routing zone. Authorization relies on this: it needs to know which endpoint and policy apply before making an access decision.

ASP.NET Core Series: Endpoint Routing

Watch ASP.NET Core Series: Endpoint Routing by the official dotnet channel to sharpen the distinction between route selection and endpoint execution.

Watch routing zone for the reason authentication and authorization are placed after routing but before endpoint execution. Then watch endpoint metadata to see how the selected endpoint carries metadata that later middleware can use.


Model binding: where action arguments come from

After MVC begins executing a controller endpoint, it must supply values for the action’s parameters. Model binding is the process that obtains request data, converts it to .NET types, and records problems when conversion or input formatting fails.

For the Create action above:

public async Task<ActionResult<OrderDto>> Create(
    CreateOrderRequest request,
    [FromQuery] bool dryRun = false)

the framework commonly obtains:

  • request from the JSON request body.
  • dryRun from ?dryRun=true in the query string.
  • A parameter named id, when it appears in {id} in the route template, from route data.

[ApiController] provides useful binding-source inference rules. A complex type that is not a registered service is inferred as [FromBody]; a parameter whose name matches a route placeholder is inferred as [FromRoute]; remaining simple parameters are normally inferred as [FromQuery].

For clarity at public API boundaries, many teams still write the source explicitly when it prevents ambiguity:

[HttpGet("{id:int}")]
public async Task<ActionResult<OrderDto>> GetById(
    [FromRoute] int id,
    [FromQuery] bool includeItems = false)
{
    // ...
}

[HttpPost]
public async Task<ActionResult<OrderDto>> Create(
    [FromBody] CreateOrderRequest request,
    [FromQuery] bool dryRun = false)
{
    // ...
}

The most common model-binding misconception is treating it as only JSON deserialization. It also covers route values, query strings, headers, form data, and conversion to action parameter types.

A body is generally read once. Therefore, a controller action should not have two unrelated parameters both expected to come from the request body. Instead, define one request DTO that represents the input contract.

public sealed class CreateOrderRequest
{
    [Required]
    public int? CustomerId { get; init; }

    [Required]
    [MinLength(1)]
    public List<CreateOrderItemRequest> Items { get; init; } = [];
}

public sealed class CreateOrderItemRequest
{
    [Range(1, int.MaxValue)]
    public int ProductId { get; init; }

    [Range(1, 100)]
    public int Quantity { get; init; }
}

Create web APIs with ASP.NET Core

Read Microsoft Learn’s Create web APIs with ASP.NET Core as the reference for API-controller conventions, automatic validation responses, and binding-source inference.

In “ApiController attribute” and “Automatic HTTP 400 responses,” read from automatic validation through the explanation of ValidationProblemDetails. Notice that the framework’s automatic 400 behavior is tied to [ApiController]. Then read “Binding source parameter inference.” Begin with the binding-source table. Continue through the inference rules, focusing on why sources are inferred and why two body-bound parameters are invalid.


Validation occurs after binding and can prevent action execution

Binding asks, “Can ASP.NET Core create a .NET value from the request?”
Validation asks, “Does that created value meet the API contract?”

Examples:

  • "quantity": "two" for an integer property is primarily a binding/conversion failure.
  • "quantity": 0 with [Range(1, 100)] is a validation failure.
  • A missing body, malformed JSON, or missing required value can also produce model-state errors.

MVC stores these errors in ModelState. With [ApiController], an invalid model automatically produces 400 Bad Request before the action body executes.

That means this explicit check is normally redundant in an API controller:

if (!ModelState.IsValid)
{
    return BadRequest(ModelState);
}

A clearer modern default is to allow the framework to generate the validation response. When custom validation logic needs to return an error in the same format, use ValidationProblem(...) rather than inventing a second error shape.

The response uses ValidationProblemDetails, a structured, machine-readable object. Clients can identify errors per field rather than parsing an English sentence.

A Postman response shows an ASP.NET Core API returning `400 Bad Request` with a `ValidationProblemDetails` body. The `errors` object groups required-field messages under `City`, `Name`, and `Gender`, illustrating the response returned when model validation fails before an action executes.

For the CreateOrderRequest example, this invalid request:

{
  "customerId": null,
  "items": []
}

could generate a response shaped like:

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "traceId": "00-...",
  "errors": {
    "CustomerId": [
      "The CustomerId field is required."
    ],
    "Items": [
      "The field Items must be a string or array type with a minimum length of '1'."
    ]
  }
}

The precise messages can vary with framework version and configuration. The interview-relevant point is that the response has a stable envelope, a 400 status, per-field errors, and a trace identifier that helps correlate client reports with server telemetry.

Do not confuse this with business-rule validation. “The item quantity must be between 1 and 100” is input validation. “The requested quantity exceeds available inventory” may require a database lookup and often belongs in application/domain logic. It might produce a different response, such as 409 Conflict for a state conflict, depending on the API’s contract.


Interview rehearsal: trace an invalid request

Answer aloud before reading the model answer.

The Angular client sends POST /api/orders?dryRun=false with a valid bearer token. The selected controller action takes CreateOrderRequest request, and the body is { "customerId": 42, "items": [] }. Items has [MinLength(1)], and the controller has [ApiController]. Explain the request path and whether the action body executes.

Model answer

The request first enters the middleware pipeline. Exception handling and HTTPS behavior run according to their configured position. Routing matches POST /api/orders to the Create action. Authentication validates the bearer token and establishes the caller identity; authorization evaluates the selected endpoint’s authorization metadata.

MVC then activates the controller through DI and model binding deserializes the JSON body into CreateOrderRequest. Validation evaluates [MinLength(1)] and records an error in ModelState because items is empty.

Because the controller uses [ApiController], MVC’s automatic model-state filter returns a 400 Bad Request with a ValidationProblemDetails response. The action method body does not execute, so no order service or database write should occur. The response then passes back outward through upstream middleware, allowing request logging or response headers to be added if the response has not already started.

A concise version for an interview is:

Routing selects the action, authentication and authorization run before MVC executes it, then MVC binds and validates the request DTO. With [ApiController], invalid model state short-circuits action execution and returns a structured 400 response automatically.


From action result to HTTP response

If validation succeeds, MVC invokes the action. Assume it calls an application service and returns:

[HttpPost]
public async Task<ActionResult<OrderDto>> Create(
    [FromBody] CreateOrderRequest request,
    [FromQuery] bool dryRun = false)
{
    OrderDto created = await _orders.CreateAsync(request, dryRun);

    return CreatedAtAction(
        nameof(GetById),
        new { id = created.Id },
        created);
}

CreatedAtAction communicates an HTTP decision, not just a C# return value:

  • It sets the status code to 201 Created.
  • It serializes created, commonly as JSON.
  • It generates a Location header pointing to the resource’s retrieval endpoint.

MVC uses output formatters and content negotiation to produce the response representation. In a JSON API, the normal result is JSON, but the request’s Accept header and configured formatters can affect the representation.

At this stage, response headers and body are being written. Middleware that resumes after await next() can observe the result, log duration and status, or add headers if the response has not started. It must not try to change status code or headers after the response has already begun.

A complete successful trace is therefore:

  1. Middleware establishes cross-cutting handling.
  2. Routing selects the Create endpoint.
  3. Authentication identifies the caller.
  4. Authorization evaluates permission for this endpoint.
  5. MVC creates the controller using the request DI scope.
  6. Model binding supplies the request DTO and query parameter.
  7. Validation confirms the request contract.
  8. The action invokes application logic and returns CreatedAtAction.
  9. MVC serializes the DTO and writes 201 Created.
  10. Control returns through earlier middleware, which can log the final result.

Failure points and how to reason about them

When debugging, first ask which stage ended the request. This avoids putting breakpoints only in the controller when the request never reaches it.

SymptomLikely stageWhat to inspect
404 Not FoundRouting or no matching endpointURL, route template, route constraints, controller discovery, HTTP method
405 Method Not AllowedRouting found a path but not a compatible verbHttpGet, HttpPost, HttpPut, or other action constraints
400 Bad Request with field errorsModel binding or validationRequest body, Content-Type, DTO shape, conversion errors, validation attributes
401 UnauthorizedAuthentication or challengeMissing, expired, malformed, or invalid credentials
403 ForbiddenAuthorizationAuthenticated caller lacks the required role, claim, policy, or resource permission
415 Unsupported Media TypeInput formatter/content type negotiationContent-Type, expected request format, formatter configuration
500 Internal Server ErrorUnhandled exceptionException logs, trace ID, exception middleware, downstream dependencies

There are two senior-level cautions here:

  1. Do not expose raw exceptions or stack traces to clients in production. Exception-handling middleware should log the detailed exception internally and return a safe, consistent problem response externally.
  2. Do not treat every 400 as a controller bug. An automatic validation 400 proves the request probably reached MVC and was rejected before the action. Route mismatch and authorization failures occur earlier.

A 90-second interview answer

For “Walk me through an ASP.NET Core Web API request,” you can answer:

ASP.NET Core receives the request as an HttpContext and runs it through the middleware pipeline in the order configured in Program.cs. Middleware can perform work before and after the next component, or short-circuit, so exception handling should be early and static files or authorization can end requests before a controller runs.

Routing matches the request path and HTTP method to an endpoint and exposes its metadata. Authentication then establishes HttpContext.User, and authorization evaluates the policy for that selected endpoint. For a controller endpoint, MVC activates the controller through DI, model-binds action parameters from sources such as route values, query strings, and JSON body, and validates the resulting model. With [ApiController], invalid model state automatically returns a structured 400 ValidationProblemDetails response without executing the action.

If validation succeeds, the action calls application logic and returns an IActionResult or value. MVC uses an output formatter, usually JSON, to write the status code, headers, and response body. Finally, the response unwinds through earlier middleware, allowing logging and other post-processing.


Key takeaways

  • Middleware is an ordered, nested pipeline. A component can act before and after next(), or terminate the request early.
  • Routing selects an endpoint using URL, HTTP method, and route constraints; it does not deserialize the body or execute the controller.
  • Authentication establishes an identity; authorization uses the selected endpoint’s metadata to make an access decision.
  • MVC then creates the controller through DI, binds input from request sources, validates it, and invokes the action only if processing has not already been short-circuited.
  • With [ApiController], invalid model state automatically produces a structured 400 ValidationProblemDetails response before the action body runs.
  • A returned action result is converted to HTTP status, headers, and a serialized response; earlier middleware then resumes on the response path.

Next, you will begin the Angular fundamentals module by explaining how components, templates, services, and Angular DI work together, including the difference between standalone and NgModule-based application bootstrap.

Can't find a good explanation? Sign up and we'll make it for you

Sign up