ASP.NET Core, Middleware and REST APIs

An ASP.NET Core application is a host (DI + configuration + logging from Module 45) running the Kestrel web server and a request pipeline:

Concept

ASP.NET Core Architecture An ASP.NET Core application is a host (DI + configuration + logging from Module 45) running the Kestrel web server and a request pipeline:

Middleware components form a chain; each can act before and after calling next. Order matters: error handling first, then HTTPS redirection, static files, routing, CORS, authentication, authorization, endpoints.

Routing maps URLs to endpoints: MapGet("/api/v1/ecus/{code}", ...), route constraints ({id:int}), route groups (MapGroup) for shared prefixes, filters and metadata.

Minimal APIs (lambdas) and controllers ([ApiController] classes with actions) are two styles over the same pipeline.

Model binding fills parameters from the route, query string, headers, body (JSON) and DI services.

Validation: data annotations on DTOs; [ApiController] validates automatically; for minimal APIs use an endpoint filter (as below) or the built-in validation (AddValidation() in .NET 10).

Filters: endpoint filters (minimal APIs) and action/exception/result filters (MVC) wrap endpoint execution.

ProblemDetails (RFC 9457) is the standard JSON error format.

Resources identified by URLs HTTP verbs express intent GET read, POST create, PUT replace, PATCH partial update, DELETE remove Status codes express results

200, 201 + Location, 204, 400 (validation), 401, 403, 404, 409 (conflict), 500

Every request carries what it needs (token, IDs) DTOs, not entities

EcuDto decouples the API contract from the database model Pagination, filtering, sorting

?page=2&pageSize=20, ?prefix=b, ?sort=-code URL (/api/v1), header or query string (Asp.Versioning package)

OpenAPI document (AddOpenApi()/MapOpenApi()), browsed with Swagger UI or Scalar

flowchart TB
    REQ["HTTP request"] --> M1["Error handling middleware"]
    M1 --> M2["Correlation ID middleware"]
    M2 --> M3["Timing / logging middleware"]
    M3 --> AUTH["Authentication / Authorization"]
    AUTH --> ROUTE["Routing"]
    ROUTE --> FILT["Endpoint filters / MVC filters"]
    FILT --> EP["Endpoint: minimal API or controller action"]
    EP --> RESP["HTTP response (flows back through the middleware)"]

Why It Matters

REST APIs connect test benches, dashboards, mobile apps and other services. ASP.NET Core is one of the fastest mainstream web frameworks, and its pipeline concepts (middleware, DI, configuration) are asked about in almost every .NET interview.

Syntax

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<EcuRepository>();
var app = builder.Build();
app.Use(async (ctx, next) => { /* before */ await next(ctx); /* after */ });
var v1 = app.MapGroup("/api/v1/ecus");
v1.MapGet("/{code}", (string code, EcuRepository repo) => repo.Find(code) is { } e ? Results.Ok(e) : Results.NotFound());
app.Run();

Example

51.1 A versioned REST API with custom middleware, validation, ProblemDetails, a controller and OpenAPI

The program starts the API on a free loopback port and then calls it with HttpClient, printing each status code and JSON body. It uses the Microsoft.AspNetCore.OpenApi package for the OpenAPI document.

#:sdk Microsoft.NET.Sdk.Web
#:package [email protected]
#:property PublishAot=false
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.WebHost.UseUrls("http://127.0.0.1:0");
builder.Services.AddSingleton<EcuRepository>();          // DI: the same container as Module 45
builder.Services.AddProblemDetails();                   // RFC 7807 error bodies
builder.Services.AddControllers();
builder.Services.AddOpenApi();                          // OpenAPI document at /openapi/v1.json

var app = builder.Build();
var pipelineLog = new List<string>();

// ---- Middleware pipeline: order matters ----
// 1. Error handling (outermost, so it catches everything below it)
app.Use(async (ctx, next) =>
{
    try { await next(ctx); }
    catch (Exception ex)
    {
        ctx.Response.StatusCode = 500;
        await ctx.Response.WriteAsJsonAsync(new ProblemDetails { Title = "Unexpected error", Detail = ex.Message, Status = 500 });
    }
});
// 2. Correlation ID: reuse the caller's ID or c
…

Expected Result

Note the different error shapes: 404 and 400 are standard ProblemDetails (with a traceId, which differs on every run), the validation response lists errors per field, and the custom error-handling middleware turned the unhandled exception from /boom into a controlled 500 response. CreatedAtRoute produced the Location header from the named route GetEcu.

GET /api/v1/ecus?page=1 -> 200  {"items":[{"code":"BCM","firmware":"4.0.2"},{"code":"BMS","firmware":"1.9.0"}],"page":1,"pageSize":2,"total":4}
GET /api/v1/ecus?page=2 -> 200  {"items":[{"code":"GW","firmware":"3.2.0"},{"code":"VCU","firmware":"2.4.1"}],"page":2,"pageSize":2,"total":4}
GET /api/v1/ecus?prefix=b&sort=-code -> 200  {"items":[{"code":"BMS","firmware":"1.9.0"},{"code":"BCM","firmware":"4.0.2"}],"page":1,"pageSize":2,"total":2}
GET /api/v1/ecus/VCU -> 200  {"code":"VCU","firmware":"2.4.1"}
GET /api/v1/ecus/NOPE -> 404  {"type":"https://tools.ietf.org/html/rfc9110#section-15.5.5","title":"ECU 'NOPE' not found","status":404,"traceId":"0HNOUJVCVIN25:00000005"}
POST /api/v1/ecus (invalid) -> 400  {"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"One or more validation errors occurred.","status":400,"errors":{"Code":["The Code field is required."],"Firmware":["The field Firmware must match the regular expression '^\\d+\\.\\d+\\.\\d+$'."]},"traceId":"0HNOUJVCVIN25:00000006"}
POST /api/v1/ecus (valid) -> 201  {"code":"ABS","firmware":"1.0.0"}
  Location: /api/v1/ecus/ABS
DELETE /api/v1/ecus/ABS -> 204
GET /boom -> 500  {"title":"Unexpected error","status":5
…

Common Mistakes

Wrong middleware order (e.g. UseAuthorization before UseAuthentication, error handler registered last).

Returning EF Core entities directly (over-posting, cycles, leaking internal fields).

Returning 200 with an error message in the body instead of the proper status code.

Unbounded list endpoints without pagination.

Blocking calls (.Result) in request handlers — thread-pool starvation (Module 31).

Leaking exception details (stack traces) to clients in production.

Best Practices

DTOs for input and output, validated at the boundary.

ProblemDetails for all errors; the developer exception page only in Development.

Async all the way; pass CancellationToken (bound automatically from HttpContext.RequestAborted).

Version the API from the start; publish an OpenAPI document.

Keep endpoints thin: delegate to services (Module 56).

Interview Relevance

Very high: "what is middleware and why does order matter?", "minimal APIs vs controllers?", "how does model binding work?", "PUT vs PATCH?", "which status code for validation errors / creation / deletion?", "how do you version an API?", "what is ProblemDetails?".

Practice Questions

Add PUT /api/v1/ecus/{code} that returns 404 for unknown ECUs and 204 on success.

Write middleware that rejects requests without an X-Api-Key header with 401.

Add rate limiting with builder.Services.AddRateLimiter and a fixed window policy.

Convert the minimal API group to a controller and compare the code.

Quick Quiz

Q1. (Multiple choice) Which status code should a successful DELETE with no body return? A) 200 B) 201 C) 204 D) 202 Answer: C (verified: 204).

Q2. (Predict the output) Page 2 of 4 ECUs with pageSize=2, sorted by code (BCM, BMS, GW, VCU).

Answer: GW and VCU (verified).

Q3. (True/False) Middleware registered with app.Use can run code both before and after the rest of the pipeline.

Answer: True — code before await next(ctx) runs on the way in, code after it on the way out (the timing middleware does this).

Q4. (Find the bug) app.MapControllers(); app.UseMiddleware<ErrorHandlingMiddleware>(); and exceptions from controllers are not handled.

Answer: Middleware is applied in registration order, so the error handler must be registered before the endpoints.

Coding Challenge

Write a minimal API with GET /square/{n:int} returning { "n": n, "square": n*n } and a middleware that adds an X-Elapsed-Ms header; call it with HttpClient and print the body and header.

#:sdk Microsoft.NET.Sdk.Web
#:property PublishAot=false
using System.Diagnostics;

var builder = WebApplication.CreateSlimBuilder();
builder.Logging.ClearProviders();
builder.WebHost.UseUrls("http://127.0.0.1:0");
var app = builder.Build();

app.Use(async (ctx, next) =>
{
    var sw = Stopwatch.StartNew();
    ctx.Response.OnStarting(() =>
    {
        ctx.Response.Headers["X-Elapsed-Ms"] = sw.ElapsedMilliseconds.ToString();
        return Task.CompletedTask;
    });
    await next(ctx);
});
app.MapGet("/square/{n:int}", (int n) => Results.Ok(new { n, square = n * n }));
await app.StartAsync();

using var http = new HttpClient { BaseAddress = new Uri(app.Urls.First()) };
var response = await http.GetAsync("/square/12");
Console.WriteLine(await response.Content.ReadAsStringAsync());
Console.WriteLine($"has X-Elapsed-Ms header: {response.Headers.Contains("X-Elapsed-Ms")}");
Console.WriteLine($"GET /square/abc -> {(int)(await http.GetAsync("/square/abc")).StatusCode} (route constraint)");
await app.StopAsync();