Skip to main content
ANVISoftware Solutions
Lesson 2 of 23Intermediate16 min

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.

The journey of a web requestA browser sends an HTTP request to a web server. The server passes it to the application, which may query a database. The application builds a response, which the server returns to the browser as an HTTP response.BrowserHTTP requestWeb serverApplicationyour codequeryDatabaserowsHTTP responseBrowser rendersEach arrow crosses a boundary, and every boundary can fail or be slow.Most performance work is about reducing or speeding up these crossings.
Your code is one stage in a longer journey. The web server owns both ends of it, and the application in the middle is the part you write.

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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

Program.cs — the two places your code appears
C#
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.

C#
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.