How the ASP.NET Runtime Works – part 1

PART 1: Creating the ASP.NET Core Web Application

PART 2: The Request Pipeline

Every .NET web developer goes through the startup phase of an ASP.NET Core application, but few stop to really understand what happens behind the scenes. The sequence of operations—and the time spent at each stage—often goes unnoticed. Yet, this understanding is key if you want to properly measure how Minimal APIs stack up against controller-based endpoints in terms of performance. The startup process is divided into three main phases, each marked by a key method call.

var builder = WebApplication.CreateBuilder(args);
:
var app = builder.Build();
:
app.Run()

Before CreateBuilder is invoked, the application’s universe does not yet exist—there is no life, no activity. Once Run is called, the universe is fully alive and running in production, no longer receptive to configuration instructions or changes.

In this first article, we’ll focus on the creation of the running instance of an ASP.NET Core application. In the next, we’ll look at the request pipeline.

The Application Builder

The WebApplication.CreateBuilder method initializes the fundamental building blocks of any ASP.NET Core application, before you define the middleware pipeline and endpoints. By default, it sets up the web host, configuration, dependency injection (DI) container, and logging infrastructure. It then returns a preconfigured builder that you can further customize by adding or overriding services.

Web Host. The method creates an instance of the HostApplicationBuilder class—a general-purpose host—and sets the hosting environment based on the ASPNETCORE_ENVIRONMENT variable. By default, it recognizes three conventional environments: Development, Staging, and Production. It also configures Kestrel as the default web server and reads URL bindings from any valid source, such as the urls section in configuration files or the ASPNETCORE_URLS environment variable. In development mode, launchSettings.json can also specify URLs. These URLs instruct Kestrel which HTTP(s) ports and addresses to bind to.

Configuration. The method sets up default configuration sources, including application settings JSON files, custom environment variables, command-line arguments, and the user-secrets JSON file (only in development mode). At this stage, the settings file names are hard-coded to appsettings.json and appsettings.{Environment}.json. You can change these names later, after the method returns, as part of an override step.

Dependency Injection. The method CreateBuilder initializes the official DI container and registers essential framework services, including logging, routing, endpoints, HTTP features, and the Kestrel server. This container is what enables constructor injection or the use of the FromServices attribute usage later in the pipeline. Custom services can be added after the method returns.

Logging. The method also configures ILoggerFactory and ILogger<T> with default providers such as console, debug, and event source loggers, depending on the environment. Any logging configuration present in the application settings is applied automatically.

Once the method returns, the Kestrel server is ready to start listening for HTTP requests, but the application pipeline and endpoints are not yet defined—these are configured afterward.

Creating a Bare Minimum WebApplication

As you can see, the CreateBuilder method performs a number of tasks automatically. While you can override or customize them later by acting on the returned builder object, it is reasonable to ask whether a shorter path exists—one that avoids performing the same setup twice.

ASP.NET Core doesn’t expose a single empty builder method directly. So if you absolutely need to skip all the default conveniences it wires up, you can take two possible routes.

  • Use HostApplicationBuilder directly
  • Do it all by yourself

Let’s consider the following code:

var builder = new HostApplicationBuilder();

The returned builder is smaller than the one you get from CreateBuilder but still non empty. You still get dependency injection but don’t get default configuration, logging and, more importantly, you don’t get Kestrel set up. We’ll return in a moment on Kestrel and its key role in the application.

You can still avoid calling HostApplicationBuilder and do it by yourself. Here’s how.

var services = new ServiceCollection();
services.AddSingleton<MyService>();
var serviceProvider = services.BuildServiceProvider();

// Configure options
var options = new WebApplicationOptions
{
    Args = args,
    ApplicationName = "Basic App"
};

// Create the WebApplication manually
var app = WebApplication.Create(options);
builder.WebHost.UseKestrel();

At this point, you can add configuration, logging, authentication and other services as they suit you without having the clear anything that was conveniently created for you automatically.

Note that if without registering a web server (whether Kestrel or something else), you get a host that cannot serve HTTP requests.

Selecting the Web Server

ASP.NET Core is server-agnostic: the framework’s universe doesn’t care which star (web server) brings it to life. It still needs a server to handle HTTP requests—by default, Kestrel acts as this embedded star—but you can replace it with HTTP.sys or even a custom server.

The key point is that the application code—middleware, endpoints, and controllers—runs identically regardless of the server powering it. The server’s job is just to listen for incoming requests and pass them to ASP.NET Core, like a cosmic gateway connecting the outside world to your application universe. This separation allows any ASP.NET application to exist cross-platform, scale flexibly, and integrate easily with reverse proxies—all without rewriting your core logic.

Classic ASP.NET instead was heavily dependent on IIS for hosting while you could technically run it on other servers via adapters. In practice, classic ASP.NET was married to IIS relying on it for request handling, modules, and Windows-only hosting. This tight coupling meant that developers couldn’t easily run ASP.NET applications on Linux orlightweight servers, nor could they fully control the request pipeline outside IIS.

In ASP.NET Core, the main web server options are:

Kestrel. The default option. It can run standalone or behind a reverse proxy such as Nginx, Apache, or IIS. In recent versions, it has evolved into a fully-featured web server with support up to HTTP/3.

HTTP.sys. A Windows-native HTTP server that supports Windows authentication, port sharing, and HTTPS certificates managed by the OS. Typically used when hosting directly on Windows without IIS.

IIS / IIS Express. Not a web server in ASP.NET Core itself, but acts as a reverse proxy for Kestrel on Windows. Provides integration with Windows authentication, process management, and management tools.

Nginx / Apache.  Not native ASP.NET Core servers, but commonly used as reverse proxies on Linux. They handle TLS/SSL, load balancing, and logging.

Custom servers. Any component that implements IHttpServer can technically be used with ASP.NET Core. Rare in practice, but possible for specialized scenarios.

Reverse Proxying

A reverse proxy is a standard web server that sits in front of another web server and forwards incoming HTTP requests to it, often handling TLS/SSL, load balancing, and additional security. To configure IIS, Nginx, or Apache as a reverse proxy, you need to adjust settings on the respective server.

In particular, for IIS on Windows you install the .NET Core Module, which enables IIS to forward requests to Kestrel. For Nginx and Apache (Linux or cross-platform) you create a configuration file that specifies the destination of the forwarded requests—the port where Kestrel is listening.

In practice, the reverse proxy listens publicly on port 80/443, while Kestrel listens on a private port (typically 5000). The proxy forwards requests to Kestrel and adds forwarded headers so the application knows about the original request. The two main headers are: X-Forwarded-For and X-Forwarded-Proto.

The former contains the original client IP address and tells the ASP.NET Core application who the actual client is. The latter contains the original scheme (HTTP or HTTPS) used by the client allowing the app to generate correct URLs and redirects.

Summary

This article explored the WebApplication.CreateBuilder method, the starting point of any ASP.NET Core application. We saw how it sets up the web host, configuration, dependency injection, and logging infrastructure automatically, giving developers a ready-to-use foundation while still allowing customization. We also covered server options for ASP.NET Core—from the default cross-platform Kestrel to Windows-native HTTP.sys, and how reverse proxies like IIS, Nginx, and Apache fit into the picture.

Published by D. Esposito

Software person since 1992

Leave a comment