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()
, andRun
theIWebHost
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 theIConfigurationRoot
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 theStartup
configurationConfigureLogging
- sets up the logging that use to live inStartup.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 behaviourConfigureServices
- Adds additional services needed by components added to theIWebHost
. 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 theAssembly
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 inConfigureServices
is currentlyinternal
, 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!