ASP.NET Core Architecture
By the end of this lesson
Explain how the framework is composed and where your code fits.
ASP.NET Core is not a container you deploy code into. It is a set of libraries you assemble into a program. Your application is a console application that happens to listen on a port.
That sounds like a technicality, and it changes how the framework reads. There is no hidden phase, and no configuration file the runtime consults behind your back. Everything that happens to a request is something your startup code asked for, in an order you can see.
Six names that appear in every explanation of this framework, including the rest of this course:
- Kestrel
- The web server built into .NET. It owns the network socket, speaks HTTP, and turns bytes on the wire into objects your code can read. It starts when your program starts and stops when it stops.
- The host
- The object that owns everything long-lived: configuration, logging, the service container, and the server itself. It is built once, before the first request arrives.
- HttpContext
- One object per request. It holds the request, the response, the authenticated user, and a per-request service scope. Created when the request arrives, discarded once the response is finished.
- Middleware
- A chain of components that every request passes through. Each one can act, pass the request along, and act again on the way back out.
- Endpoint
- The piece of your code that produces the answer: a controller action or a minimal API handler. It sits at the end of the chain.
- Service provider
- The dependency injection container. It knows how to build your classes, so asking it for an employee repository produces one, along with everything that one needs.
What happens between a client pressing send and your method running:
The host is already built
Configuration has been read, services registered and logging wired up. This happened once, at startup. No part of it repeats per request, which is why a value read here is fixed for the life of the process.
Kestrel accepts the connection
It parses the request line, the headers and the start of the body. A malformed request is rejected here, before any of your code is involved.
An HttpContext is created
One per request, with a fresh service scope attached to it. Anything registered as scoped will be created inside that scope and disposed with it.
Middleware runs in order
Each component you registered gets its turn, in the order you registered them. Any of them can answer the request and stop it going further.
Routing selects an endpoint
The routing middleware matches the path and method against your route templates and records the chosen endpoint on the context. Later middleware can read that choice and the metadata attached to it.
Your handler runs
The container builds the controller or resolves the handler's arguments from the request scope, model binding turns request text into typed values, and your method executes.
The response travels back out
Back through the same middleware in reverse order, then Kestrel writes the status line, headers and body to the socket.
var builder = WebApplication.CreateBuilder(args);
// Your code, part one: what this application is made of.
builder.Services.AddControllers();
builder.Services.AddScoped<IEmployeeRepository, SqlEmployeeRepository>();
var app = builder.Build();
// Your code, part two: what every request passes through.
app.UseExceptionHandler("/error");
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();- CreateBuilder reads configuration, sets up logging, works out which environment this is, and creates an empty service container. Nothing is listening yet.
- The Add calls describe what the application is able to build. They record a registration; they do not construct anything.
- Build produces the application and closes the registration phase. Kestrel is configured at this point and still not accepting connections.
- The Use and Map calls assemble the request pipeline. The order of these lines is the order a request travels, which is the subject of two later lessons.
- Run starts the server and blocks until the process is asked to shut down. From here on, the only code that executes is triggered by a request arriving.
Summary
- An ASP.NET Core application is an ordinary program that hosts a web server, not code deployed into one
- Kestrel owns the socket and the HTTP parsing at both ends of every request
- The host builds configuration, logging and the service container once, before any request arrives
- Each request gets an HttpContext and a service scope, travels through middleware to an endpoint, and returns along the same chain
- Your code appears in two places: registrations before Build, and the pipeline after it
Practice
Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.
Try it yourself
Once versus every time
Create a web API project. Log one line in the builder phase, before Build, and one line inside a single endpoint.
Start the application and call the endpoint three times. Count how many times each line appears.
Show solution
The startup line appears once and the endpoint line appears three times. That is the whole distinction between the host and a request, made visible.
It matters because the two phases have different rules. Work done at startup is paid for once and shared by every request, which is why it is the right place for expensive setup and the wrong place for anything that depends on the caller.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Logger.LogInformation("Startup: the host is being built");
app.MapGet("/api/employees/ping", (ILogger<Program> logger) =>
{
logger.LogInformation("Request: someone called ping");
return Results.Ok(new { status = "ok" });
});
app.Run();Think about it
Trace a rejected request
A request for GET /api/employees/482 comes back as 401 Unauthorized.
Which of the stages in this lesson ran, and which did not? What does that tell you about the cost of a rejected request, and about the absence of a log line from your action?
Show solution
Kestrel parsed the request, an HttpContext was created, and middleware ran as far as the component that rejected the call. Routing had run too, because the authorization step needs to know which endpoint was matched.
Your controller never executed. The repository was never constructed, and the database was never contacted. A rejected request is cheap, which is one reason authentication belongs early in the pipeline.
The practical lesson is about debugging. No log line from your action does not mean the request never arrived. It usually means something in front of your action answered it, and the request log is the place to look.
Saved in this browser only.