What’s important in ASP.NET Core: The best abstraction ever
Let’s go through, what I think is most important to understand in the big picture about servers.
Servers, at their simplest level respond to requests
Servers can respond to requests by reading a file from the operating system file system, or by generating a response programmatically
For example, a index.html file
Or responding “Hey, I’m a teapot”
Servers can also incorporate temporal elements, such as state to perform the response
Let’s say, we want to include the users current order items - we would consider this state that has an effect on the response generation - it doesn’t change that it is still just response generation
Servers can call other servers in a loop where each server contributes to the request response chain in varying ways
A load balancer could intercept the request before it reaches your application and make a decision to send it to a specific place
For a general understanding, this is enough but also this incorporates the fundamentals you must understand. Fundamentally, it is always a request response chain.
As for ASP.NET core’s design, this is also in its philosophy: Here’s a minimal example of a server:
What do you see above?
I believe it would be fair to say, that ASP.NET core has no magic. The vast majority of its extended features from its base behaviour are implemented with an abstraction known as middlewares. Middlewares do everything, from serving static files, to authentication to exception handling. Most of your advanced features, such as UseHttpsRedirection or UseHealthChecks or UseOpenIdConnect are going to be simple shorthands that actually use middlewares under the hood.
The important thing then is to realize, that without middlewares there is nothing but request response mapping and even WITH middlewares, there is nothing but request response mapping.
What is a middleware?
We established that all of .NETs features are basically middlewares, so what is it?
It’s a pipeline. You register middlewares, and your request will flow through them and any middleware has the choice to contribute to the request’s processing or to stop the processing fully and deny any further processing.
The request goes in, the response comes out.
A single middleware’s code is split by the call to next(). The code before next is called on the way in - the code after next is called on the way out.
Middleware can perform operations on the incoming request, such as:
Reading or modifying the request: Logging, adding headers, parsing tokens, etc.
Passing the request down the pipeline: By calling
await next()
(wherenext
represents the next middleware in the pipeline).Processing the response: After
await next()
completes, the middleware can modify the response as it bubbles back through the pipeline.
An example of a middleware THAT LOGS THE REQUEST
Middlewares operate in a chain, and each component gets an opportunity to:
Act on the request or response.
Delegate to the next middleware in the chain.
The chain is linear, so middleware cannot "skip" arbitrary middlewares or "rewind" the pipeline. The explicit control flow (await next()
or no call to next()
) ensures predictability and avoids implicit, "magical" behaviors.
What’s the point?
So when you ask questions like:
Why isn’t my static file served?
Well, it’s probably a middleware
Why isn’t my routing working?
Well, it’s probably a middleware
Why isn’t my authentication working?
That’s amore, and probably a middleware
The point I’m trying to make, is that there is no magic and middlewares are roughly speaking all you have. It is an abstraction so simple and so coherent, that there is almost no problem that it cannot be used to solve within a request response chain. You can think of it as a function, with a request as an input and a response as an output.
The sooner you realize that ASP.NET Core is just a bunch of middlewares and preconfigured other middlewares, the less you will be confused. Middlewares also do nothing magical, their inputs are always requests and outputs are always responses - and within that abstraction, almost everything is possible.
Here’s an arbitrary list of 50 things, you could do with middleware. If you’re paying attention - you’ll notice that there is tech in the industry that specifically can be used to solve some of these problems, but are typically higher up in the request response chain. Some are managed Azure services, for example.
Log incoming requests.
Log outgoing responses.
Add custom headers to requests.
Add custom headers to responses.
Perform request authentication.
Enforce authorization policies.
Handle global exception handling.
Rewrite or modify URLs dynamically.
Redirect requests to another URL.
Validate incoming request payloads.
Compress response data using Gzip or Brotli.
Encrypt request or response data.
Decrypt incoming requests.
Cache responses for specific routes or queries.
Implement rate-limiting to control API traffic.
Throttle requests based on user or IP.
Implement custom load balancing strategies.
Filter or block requests by IP address.
Enforce and manage CORS policies.
Validate and normalize query string parameters.
Parse cookies and manage custom session data.
Inject dynamic values into the DI container.
Parse and validate JWT tokens.
Log and inspect request and response bodies.
Handle multipart file uploads.
Implement custom error pages for specific HTTP status codes.
Enforce HTTPS redirection for all requests.
Dynamically manage API versioning.
Handle localization and culture settings dynamically.
Measure and log performance metrics for requests.
Record and log request/response times.
Modify or normalize request paths, methods, or headers.
Transform incoming data formats (e.g., XML to JSON).
Normalize response formats (e.g., consistent JSON output).
Implement your own MVC framework from scratch. (Yes, UseMVC is just middlewares under the hood)
Build a lightweight API gateway or proxy.
Serve static files from a specific directory.
Inject custom templates into responses dynamically.
Add security headers (e.g.,
X-Frame-Options
,Strict-Transport-Security
).Handle sessions and manage session expiration.
Implement feature toggling for A/B testing.
Route requests dynamically based on custom logic.
Implement request retries for idempotent operations.
Validate or inspect user agents for compatibility or restrictions.
Detect and block malicious bots or web crawlers.
Capture telemetry or trace data for analytics systems.
Monitor and log resource usage (e.g., memory, CPU) per request.
Inject debug or diagnostic information for development environments.
Implement load testing hooks or stress simulation logic.
Serve a "maintenance mode" page or route dynamically when the application is down.
An abstraction so good, that it limits you very little and where understanding it is crucial.