Application Startup in ASP.NET Core

2022-02-02 | dotnet · csharp ·

TLDR; In an ASP.NET web application, the Program.cs file contains code that configures, runs and manages the lifetime of your app. In this post we explore what happens when you run a new ASP.NET Core web application.

When working on an existing ASP.NET application it might be daunting to know where to begin with understanding the codebase. With a new ASP.NET Core 3.1 or .NET 5.0 web application you're met with two files with boilerplate code the Startup.cs and Program.cs. What do they do?

The entry point

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

In .NET applications, it is convention to have a Program.cs file with a public static method called Main inside the Program class1. Similar to command-line applications, the Program.Main method is the entry point of our application, where the execution chain begins when the application starts running.

When you run the application using dotnet run on the command line, the command looks in the current folder or project file, builds the project, launches a process and looks for the entry point to execute your code.

When the Main method is executed, the program creates a host builder, builds the host and then runs the application. The host is responsible for the application's startup and lifetime management.

The host is also responsible for configuring the necessary pieces your web application needs to communicate with incoming HTTP requests and to respond in the appropriate way. This includes: an HTTP server implementation, middleware components, logging, dependency injection (DI) services and configuration. The configuration and instantiation consists of the following steps:

  1. creating a builder
  2. configuring services
  3. creating the application host
  4. running the application

The next few sections expand on the above steps.

Creating a builder

The HostBuilder knows how to create your application and also configures the necessary default pieces your web application needs to determine how to operate. If we look at the definition of CreateDefaultBuilder method we see that it:

  • sets the base root of the project to the current directory. This is how the application is able to locate the folder containing static files, Razor pages, and configuration files such as appsettings.json.
  • loads configuration. The different configuration sources include appsettings.json, environment variables, and command-line arguments.
  • adds logging logging providers such as console and debug.

The next method to be called is the ConfigureWebHostDefaults which unsurprisingly, configures web specific defaults. This includes setting the HTTP server as Kestrel server and adds the host filtering middleware, which in combination with the AllowedHosts key in appsettings.json, specifies the allowable host names.

Configuring services

The call to UseStartup looks for the ConfigureServices and Configure methods in the Startup class. The ConfigureServices method allows us to register services with the dependency injection container. This gives us the ability to put our custom services into the ASP.NET Core, which can be injected into the application where needed. An example would be registering a database, whether that is an in-memory or a SQL database.

The Configure method allows us to customise the HTTP request pipeline or middleware pipeline.

public class Startup
{
    public IConfiguration Configuration { get; }

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        ...
        app.UseRouting();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
        });
    }
}

Creating the application

We call .Build() on the HostBuilder which initialises the host. This then:

  • starts the web host instance so it can listen, receive and process HTTP messages.
  • loads configuration from appsettings.json, environment variables and other configuration sources.
  • sends logging output to the console. This what we see in the console as we interact with the web application.

Run

We then Run the application. The Run method starts the application and blocks the calling thread until the host is shut down.

.NET 6.0

With .NET 6.0 we're given a Startup.cs file with the following:

WebApplicationBuilder? builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

WebApplication? app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthorization();

app.MapGet("/hi", () => "Hello!");

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();

There is a significant visual difference between the new boilerplate we are now given and the first example. However, the execution of steps remain the same:

  1. we create a builder which registers default services
  2. we register our services
  3. we build the web application
  4. we run the web application

.NET 6 simplifies the boiler plate code and introduces types WebApplicationBuilder and WebApplication that replaces the HostBuilder and Host respectively.

.NET WebApplication Host

This is the new and default way of creating a web application. There are a few major differences in how the WebApplication host is configured but the underlying steps to configure and manage the application is similar to the Host we saw in the previous section.

First of all, the Startup class has been removed. Instead of registering services with ConfigureServices, they are registered directly on the Services property. The same applies when configuring the HTTP pipeline. Instead of registering middleware with Configure we call the UseXyz methods directly on the app of type WebApplication (which implements IApplicationBuilder).

For new applications, this is the preferred way of getting started. However, this doesn't mean you should migrate every ASP.NET application to using the new template. The previous instantiation using the HostBuilder and Startup class still works in .NET 6.

With the simplified .NET 6.0 achieving the same result as the first example, is it necessary to know about HostBuilder and the separation of concerns with the Program.cs and Startup.cs? Well, yes. The next section covers why we still have the HostBuilder and briefly comments on the original way of creating a default web host (that was introduced in ASP.NET Core 2.1).

ASP.NET Core: Host and WebHost

ASP.NET Core WebHost

Before ASP.NET Core 3.x, this was the default way of configuring and instantiating your web application. The decision to move away from using the WebHost and WebHostBuilder was to decouple the web specific configuration (server and middleware) from application specific configuration (logging, configuration and dependency injection services).

Creating a WebHostBuilder involves:

  1. Configuring DI services, logging, configuration, default filtering middleware and the HTTP server
  2. UseStartup<Startup>() to specify the startup class to configure our dependency injection services and HTTP request pipeline.
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
           .UseStartup<Startup>();

For a web application using the generic HostBuilder, WebHost.CreateDefaultBuilder is split across two methods Host.CreateDefaultBuilder and ConfigureWebHostDefaults as we saw in the first code snippet. The WebHost is no longer recommended for creating new web applications. However, it is still used for backwards compatibility with ASP.NET Core 2.x.

.NET Generic Host

This is the default way of creating a web host in ASP.NET Core 3.x and .NET 5.0. The generic host separates the registering of configuration, dependency injection services and logging from the web specific resources such as the middleware components and the HTTP server implementation. This allows non-HTTP based applications to be able to use dependency injection services, logging and configuration.

The interfaces IHost and IHostBuilder are found in Microsoft.Extensions.Hosting, emphasising that we can now have hosted services that are not dependent on AspNetCore namespace.

Conclusion

In this post we looked at understanding what the boilerplate code does in a new ASP.NET Core application. We first looked at the ASP.NET Core 3.x and 5.0 example and then covered the new ASP.NET 6.0 web application template. We saw that despite the visual difference between template code, both do similar jobs and achieve the same outcome. We ended by talking about the differences between startup code across the different ASP.NET Core version, from 2.1 to 6.0.


  1. With .NET 6 this is no longer mandatory. Some of the new changes also include not requiring the namespace keyword and the removal of commonly used using statements.