← ASP.NET Core: applications, contracts and operations
01 / 10 · 60 MIN

Hosting, middleware and request flow

Follow a request from host composition to its endpoint, identifying continuation, interception and response boundaries.

Compose before receiving

WebApplication.CreateBuilder(args) prepares configuration, services and the host. Build creates the application with that composition. MapGet registers an endpoint for a method and path; its handler participates only when a request arrives and the pipeline permits execution. app.Run() runs the host until shutdown.

This lesson uses ASP.NET Core 10.0 on .NET 10 LTS. You should already recognize C# lambdas, await and types. The new decision is where each function enters the HTTP path. Registering an endpoint neither invokes it immediately nor forces earlier middleware to continue.

Entry, continuation and return

Middleware can perform work before calling next, await continuation and perform work afterward. With A followed by B and terminal T, the normal path is A-before, B-before, T, B-after, A-after. Return order follows call composition; it is not a second pass in registration order.

This prediction assumes all components continue normally. An exception can prevent code after await next. Interception without calling next prevents downstream components but can return control to upstream ones. Mark each call and return instead of merely ordering source-file lines.

Terminal delegates and routes

app.Run(async context => ...) registers a terminal delegate without a next parameter. It differs from app.Run(), which runs the host. If the terminal delegate responds to every request before endpoint execution, an existing MapGet is insufficient to restore its handler. Giving it another name does not introduce continuation either.

WebApplication can automatically add routing and endpoint execution when endpoints are registered. Missing an explicit UseRouting alone does not establish failure. A process-status endpoint on its own path preserves other contracts. Such status should not be confused with dependency readiness.

Position protects a boundary

Exception handling must wrap components whose failures it needs to handle. If M throws before calling next and the handler is after M, the request has not entered that handler. Adding another instance at the end does not retroactively wrap the exception. The relevant boundary is the call stack actually traversed.

With explicit composition, routing selects an endpoint, authentication establishes identity and authorization applies conditions before execution. Configuration and services are still required. Order alone neither supplies an access policy nor demonstrates that every route requires one.

Observe a response already started

A fixed header known on entry can be set before next while the response has not started. Once HasStarted is true, headers and status are no longer freely changeable. Reading status for observation differs from attempting to replace the contract already sent.

Appending diagnostic text to JSON can invalidate the document and contradict Content-Length. The client does not turn those bytes into a header. In the example, logs receive only labels and status while the body retains its purpose. A real transformation requires explicit control of representation and emission timing.

Branches and effective paths

A middleware Map("/ops", ...) branch removes the matched prefix from Path and appends it to PathBase. For /ops/status with no previous PathBase, the branch observes Path=/status and PathBase=/ops. A path observation should identify its pipeline location to avoid confusing mounting prefixes with internal routes.

UseWhen can rejoin the main pipeline if the branch does not terminate the request and calls next. A terminating branch does not have that continuation. Do not confuse middleware Map with MapGet: one composes a path branch while the other registers an HTTP endpoint.

Guided reading and decision

In the example, fix GET /demo and trace A, routing, B and endpoint execution. Record expected text, the fixed header and the four log entries in order. Now imagine a terminal Run between A and B: B and the endpoint become unreachable even if that terminal responds with 200.

In a second scenario, move header assignment after next and assume HasStarted=true. The correction must restore the point when the value can still be written. Assessments ask you to select interpretations and decisions from these premises; they require no C# execution or server startup.

// Program.cs: ASP.NET Core 10.0, Microsoft.NET.Sdk.Web, net10.0.
// ImplicitUsings enabled. Teaching endpoint; no external operation.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Use(async (context, next) =>
{
    context.Response.Headers["X-Training-Contract"] = "pipeline-v1";
    app.Logger.LogInformation("A before");
    await next(context);
    app.Logger.LogInformation("A after: {Status}", context.Response.StatusCode);
});
app.UseRouting();
app.Use(async (context, next) =>
{
    app.Logger.LogInformation("B before");
    await next(context);
    app.Logger.LogInformation("B after");
});
app.MapGet("/demo", () => Results.Text("ready-for-reading"));
app.Run();
IN PRACTICE

For GET /demo, with no exceptions or other terminal middleware, A before precedes B before; the endpoint produces text; B after precedes A after. The header was set before emission. The text does not establish a business operation.

Common pitfalls

Confusing app.Run() with a Run delegate; treating MapGet as immediate execution; writing headers after HasStarted; placing exception handling after the component it must protect.

Related topics: Dependency lifetimes and configuration · Responses, errors and API contracts · Authentication and authorization

Take this idea with you

Draw calls to next and identify where they end. Selecting an endpoint and executing it are distinct stages; returning permits result observation but does not make an already sent response editable again.

Create account

Reference: WebApplication and WebApplicationBuilder · ASP.NET Core 10.0 / .NET 10 / C# 14