WebApplicationBuilder and WebApplication

Configure an API in the builder phase, build it once, run it with Run or StartAsync/StopAsync, and choose between CreateBuilder, Slim and Empty.

Syntaxvar builder = WebApplication.CreateBuilder(args); builder.Services.AddSingleton<OrderStore>(); var app = builder.Build(); app.MapGet("/orders/{id:int}", (int id, OrderStore s) => s.Find(id)); app.Run();

A minimal-hosting Program.cs has two phases. The builder collects services, configuration and server settings. Build() turns them into a WebApplication, where you compose the middleware pipeline and map endpoints. The split is enforced.

The builder phase

  • builder.Services: the IServiceCollection. After Build() it is read-only; adding to it throws InvalidOperationException ("The service collection cannot be modified because it is read-only").
  • builder.Configuration: a ConfigurationManager. It is live, so a value is readable as soon as its source is added, which is how you read a connection string while registering a service.
  • builder.Logging, builder.Environment, builder.WebHost (Kestrel, URLs) and builder.Host (generic host settings such as the service provider options).

Build() can be called once. It creates the container (running DI validation in Development) and freezes the service collection.

The application phase

WebApplication exposes Services, Configuration, Environment, Logger, Lifetime and Urls. It also adds pipeline pieces you do not write: UseRouting at the start and UseEndpoints at the end when you map endpoints, the developer exception page in Development, and authentication and authorization middleware when those services are registered. Call them explicitly when their position matters, for example authentication after CORS.

Run versus StartAsync and StopAsync

app.Run() starts the host and blocks until shutdown; RunAsync() is the awaitable form. StartAsync() returns once hosted services have started and Kestrel is listening; you must then call StopAsync() (or WaitForShutdownAsync()) and dispose. Use Run in production and StartAsync/StopAsync in demos, tools and tests that drive the app themselves.

Three builders

CreateBuilderCreateSlimBuilderCreateEmptyBuilder
Config sourcesJSON files, user secrets, env vars, command lineSameNone
LoggingConsole, Debug, EventSourceConsoleNone
KestrelHTTP, HTTPS, HTTP/3HTTP only until UseKestrelHttpsConfiguration()Nothing until UseKestrelCore()
Regex route constraintsYesOpt inOpt in
Use forMost APIsNative AOT, trimmed, small servicesTooling, benchmarks, full control

Start with CreateBuilder. Choose Slim when you publish with Native AOT and want fewer dependencies kept by the trimmer; it is what the webapiaot template uses.

What goes wrong in production

  • A URL in code. app.Run("http://localhost:5000") overrides ASPNETCORE_HTTP_PORTS; in a container it binds loopback only and the pod never receives traffic.
  • Slim builder plus a regex route. {slug:regex(...)} builds and starts fine, then requests fail with 500 because the constraint is not registered.
  • Configuration read too early. A value read before a later source (a vault provider, say) is added is a stale copy; read options at resolve time instead.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Start the API on loopback in a smoke test, call GET /orders/1002, assert the JSON and stop it, all with StartAsync and StopAsync.
  • Switch an internal pricing service to CreateSlimBuilder before publishing it with Native AOT, and register the regex constraint its routes use.
  • Find why a container listens on localhost:5000 instead of port 8080: a URL passed to app.Run() overrides ASPNETCORE_HTTP_PORTS.

More examples

CreateEmptyBuilder: only what you add

Without <code>UseKestrelCore()</code> the build fails with "No service for type 'Microsoft.AspNetCore.Hosting.Server.IServer' has been registered", and without <code>AddRoutingCore()</code> the first <code>MapGet</code> throws, asking you to add the routing services. You also get no <code>appsettings.json</code> and no console logs unless you add them. It suits tiny sidecars and benchmarks where every dependency is a decision.

Example · csharp
var builder = WebApplication.CreateEmptyBuilder(new WebApplicationOptions { Args = args });

// No server, routing, configuration files or logging until you add them.
builder.WebHost.UseKestrelCore();
builder.Services.AddRoutingCore();
builder.Configuration.AddEnvironmentVariables(prefix: "INVENTORY_");

var app = builder.Build();

app.MapGet("/stock/{sku}", (string sku) => Results.Ok(new { sku, onHand = 37 }));

app.Run();

Slim builder: opting back in to HTTPS and regex constraints

On the slim builder a <code>regex</code> constraint compiles and the app starts, but requests to the app fail with 500 because no constraint is registered under that name. Registering <code>RegexInlineRouteConstraint</code> brings it back at the cost of the regex engine in the trimmed output. <code>UseKestrelHttpsConfiguration()</code> restores HTTPS endpoint configuration; behind a TLS-terminating proxy you usually do not need it.

Example · csharp
using Microsoft.AspNetCore.Routing.Constraints;

var builder = WebApplication.CreateSlimBuilder(args);

// Slim leaves these out to keep a trimmed or Native AOT app small.
builder.WebHost.UseKestrelHttpsConfiguration();
builder.Services.Configure<RouteOptions>(options =>
    options.SetParameterPolicy<RegexInlineRouteConstraint>("regex"));

var app = builder.Build();

app.MapGet("/products/{slug:regex(^[a-z0-9-]+$)}", (string slug) =>
    Results.Ok(new { slug, name = "Blue kettle" }));

app.Run();

Let configuration choose the port

The URL passed to <code>Run</code> overrides <code>ASPNETCORE_URLS</code>, <code>ASPNETCORE_HTTP_PORTS</code> and <code>--urls</code> (only an explicit <code>Kestrel:Endpoints</code> section beats it), so a value meant for local debugging ships to production. Inside a container, <code>localhost</code> is the container's own loopback: the process is healthy, the probe from the kubelet cannot connect, and the pod is restarted forever. Keep URLs in configuration, where each environment can set them.

Example · csharp
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/health/live", () => Results.Ok());

// Wrong in a container: binds 127.0.0.1 only and ignores ASPNETCORE_HTTP_PORTS.
// app.Run("http://localhost:5000");

// Right: Kestrel reads ASPNETCORE_HTTP_PORTS (8080 in the official images),
// ASPNETCORE_URLS or the Kestrel:Endpoints section.
app.Logger.LogInformation("Starting in {Environment}", app.Environment.EnvironmentName);
app.Run();

Discussion

  • Be the first to comment on this lesson.