blog post image
Andrew Lock avatar

Andrew Lock

~7 min read

Exploring Program.cs, Startup.cs and CreateDefaultBuilder in ASP.NET Core 2 preview 1

One of the goals in ASP.NET Core 2.0 has been to clean up the basic templates, simplify the basic use-cases, and make it easier to get started with new projects.

This is evident in the new Program and Startup classes, which, on the face of it, are much simpler than their ASP.NET Core 1.0 counterparts. In this post, I'll take a look at the new WebHost.CreateDefaultBuilder() method, and see how it bootstraps your application.

Program and Startup responsibilities in ASP.NET Core 1.X

In ASP.NET Core 1.X, the Program class is used to setup the IWebHost. The default template for a web app looks something like this:

public class Program
{
    public static void Main(string[] args)
    {
        var host = new WebHostBuilder()
            .UseKestrel()
            .UseContentRoot(Directory.GetCurrentDirectory())
            .UseIISIntegration()
            .UseStartup<Startup>()
            .Build();

        host.Run();
    }
}

This relatively compact file does a number of things:

  • Configuring a web server (Kestrel)
  • Set the Content directory (the directory containing the appsettings.json file etc)
  • Setup IIS Integration
  • Define the Startup class to use
  • Build(), and Run the IWebHost

The Startup class varies considerably depending on the application you are building. The MVC template shown below is a fairly typical starter template:

public class Startup
{
    public Startup(IHostingEnvironment env)
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
            .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
            .AddEnvironmentVariables();
        Configuration = builder.Build();
    }

    public IConfigurationRoot Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        // Add framework services.
        services.AddMvc();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        loggerFactory.AddConsole(Configuration.GetSection("Logging"));
        loggerFactory.AddDebug();

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseBrowserLink();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
        }

        app.UseStaticFiles();

        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });
    }
}

This is a far more substantial file that has 4 main reponsibilities:

  • Setup configuration in the Startup constructor
  • Setup dependency injection in ConfigureServices
  • Setup Logging in Configure
  • Setup the middleware pipeline in Configure

This all works pretty well, but there are a number of points that the ASP.NET team considered to be less than ideal.

First, setting up configuration is relatively verbose, but also pretty standard; it generally doesn't need to vary much either between applications, or as the application evolves.

Secondly, logging is setup in the Configure method of Startup, after configuration and DI have been configured. This has two draw backs. On the one hand, it makes logging feel a little like a second class citizen - Configure is generally used to setup the middleware pipeline, so having the logging config in there doesn't make a huge amount of sense. Also it means you can't easily log the bootstrapping of the application itself. There are ways to do it, but it's not obvious.

In ASP.NET Core 2.0 preview 1, these two points have been addressed by modifying the IWebHost and by creating a helper method for setting up your apps.

Program and Startup responsibilities in ASP.NET Core 2.0 preview 1

In ASP.NET Core 2.0 preview 1, the responsibilities of the IWebHost have changed somewhat. As well as having the same responsibilities as before, the IWebHost has gained two more:

  • Setup configuration
  • Setup Logging

In addition, ASP.NET Core 2.0 introduces a helper method, CreateDefaultBuilder, that encapsulates most of the common code found in Program.cs, as well as taking care of configuration and logging!

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

    public static IWebHost BuildWebHost(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>()
            .Build();
}

As you can see, there's no mention of Kestrel, IIS integration, configuration etc - that's all handled by the CreateDefaultBuilder method as you'll see in a sec.

Moving the configuration and logging code into this method also simplifies the Startup file:

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

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
        }

        app.UseStaticFiles();

        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });
    }
}

This class is pretty much identical to the 1.0 class with the logging and most of the configuration code removed. Notice too that the IConfiguration object is injected into the class and stored in a property on the class, instead of creating the configuration in the constructor itself

This is new to ASP.NET Core 2.0 - the IConfiguration object is registered with DI by default. in 1.X you had to register the IConfigurationRoot yourself if you needed it to be available in DI.

My initial reaction to CreateDefaultBuilder was that it was just obfuscating the setup, and felt a bit like a step backwards, but in hindsight, that was more just a "who moved my cheese" reaction. There's nothing magical about the CreateDefaultBuilder it just hides a certain amount of standard, ceremonial code that would often go unchanged anyway.

The WebHost.CreateDefaultBuilder helper method

In order to properly understand the static CreateDefaultBuilder helper method, I decided to take a peek at the source code on GitHub! You'll be pleased to know, if you're used to ASP.NET Core 1.X, most of this will look remarkably familiar.

public static IWebHostBuilder CreateDefaultBuilder(string[] args)
{
    var builder = new WebHostBuilder()
        .UseKestrel()
        .UseContentRoot(Directory.GetCurrentDirectory())
        .ConfigureAppConfiguration((hostingContext, config) => { /* setup config */  })
        .ConfigureLogging((hostingContext, logging) =>  { /* setup logging */  })
        .UseIISIntegration()
        .UseDefaultServiceProvider((context, options) =>  { /* setup the DI container to use */  })
        .ConfigureServices(services => 
        {
            services.AddTransient<IConfigureOptions<KestrelServerOptions>, KestrelServerOptionsSetup>();
        });

    return builder;
}

There's a few new methods in there that I've elided for now, which I'll explore in follow up posts. You can see that this method is largely doing the same work that Program did in ASP.NET Core 1.0 - it sets up Kestrel, defines the ContentRoot, and sets up IIS integration, just like before. Additionally, it does a number of other things

  • ConfigureAppConfiguration - this contains the configuration code that use to live in the Startup configuration
  • ConfigureLogging - sets up the logging that use to live in Startup.Configure
  • UseDefaultServiceProvider - I'll go into this in a later post, but this sets up the built-in DI container, and lets you customise its behaviour
  • ConfigureServices - Adds additional services needed by components added to the IWebHost. In particular, it configures the Kestrel server options, which lets you easily define your web host setup as part of your normal config.

I'll look a closer look at configuration and logging in this post, and dive into the other methods in a later post.

Setting up app configuration in ConfigureAppConfiguration

The ConfigureAppConfiguration method takes a lambda with two parameters - a WebHostBuilderContext called hostingContext, and an IConfigurationBuilder instance, config:

ConfigureAppConfiguration((hostingContext, config) =>
{
	var env = hostingContext.HostingEnvironment;

	config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
			.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);

	if (env.IsDevelopment())
	{
		var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
		if (appAssembly != null)
		{
			config.AddUserSecrets(appAssembly, optional: true);
		}
	}

	config.AddEnvironmentVariables();

	if (args != null)
	{
		config.AddCommandLine(args);
	}
});

As you can see, the hostingContext parameter exposes the IHostingEnvironment (whether we're running in "Development" or "Production") as a property, HostingEnvironment. Apart form that the bulk of the code should be pretty familiar if you've used ASP.NET Core 2.0.

The one exception to this is setting up User Secrets, which is done a little different in ASP.NET Core 2.0. This uses an assembly reference to load the user secrets, though you can still use the generic config.AddUserSecrets<T> version in your own config.

In ASP.NET Core 2.0, the UserSecretsId is stored in an assembly attribute, hence the need for the Assembly code above. You can still define the id to use in your csproj file - it will be embedded in an assembly level attribute at compile time.

This is all pretty standard stuff. It loads configuration from the following providers, in the following order:

  • appsettings.json (optional)
  • appsettings.{env.EnvironmentName}.json (optional)
  • User Secrets
  • Environment Variables
  • Command line arguments

The main difference between this method and the approach in ASP.NET Core 1.X is the location - config is now part of the WebHost itself, instead of sliding in through the backdoor so-to-speak by using the Startup constructor. Also, the initial creation and final call to Build() on the IConfigurationBuilder instance happens in the web host itself, instead of being handled by you.

Setting up logging in ConfigureLogging

The ConfigureLogging method also takes a lambda with two parameters - a WebHostBuilderContext called hostingContext, just like the configuration method, and a LoggerFactory instance, logging:

ConfigureLogging((hostingContext, logging) =>
{
	logging.UseConfiguration(hostingContext.Configuration.GetSection("Logging"));
	logging.AddConsole();
	logging.AddDebug();
});

The logging infrastructure has changed a little in ASP.NET Core 2.0, but broadly speaking, this code echoes what you would find in the Configure method of an ASP.NET Core 1.0 app, setting up the Console and Debug log providers. You can use the UseConfiguration method to setup the log levels to use by accessing the already-defined IConfiguration, exposed on hostingContext.Configuration.

Customising your WebHostBuilder

Hopefully this dive into the WebHost.CreateDefaultBuilder helper helps show why the ASP.NET team decided to introduce it. There's a fair amount of ceremony in getting an app up and running, and this makes it far simpler.

But what if this isn't the setup you want? Well, then you don't have to use it! There's nothing special about the helper, you could copy-and paste its code into your own app, customise it, and you're good to go.

That's not quite true - the KestrelServerOptionsSetup class referenced in ConfigureServices is currently internal, so you would have to remove this. I'll dive into what this does in a later post.

Summary

This post looked at some of the differences between Program.cs and Startup.cs in moving from ASP.NET Core 1.X to 2.0 preview 1. In particular, I took a slightly deeper look into the new WebHost.CreateDefaultBuilder method which aims to simplify the initial bootstrapping of your app. If you're not keen on the choices it makes for you, or you need to customise them, you can still do this, exactly as you did before. The choice is yours!

Andrew Lock | .Net Escapades
Want an email when
there's new posts?