Rants and Raves

Thanks for visiting my blog!

Program.cs in ASP.NET Core 2.0
Program.cs in ASP.NET Core 2.0
December 26, 2024

I am sure that many people like me are digging into ASP.NET Core 2.0 and curious about what has been changed. I’m going to start with the very start of your ASP.NET Core project, the program.cs.

Digging into the meat of ASP.NET Core 2.0 might lead you to identity, the better .NET Core support, and other changes. But I think the startup is where you can start to see the platform mature.

Program.cs

So in ASP.NET Core 1.x, the usual program.cs usually looked 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();
  }
}

What’s interesting about this is that you’re opting into each part of the web builder. I like that it’s explicit.

In ASP.NET Core 2.0, you can still do it explicitly, but they’ve added a simpler approach (that is in the templates):

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

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

This is smaller but what is it actually doing? The CreateDefaultBuilder does what the old one does leaving you only to have to specify the startup. But under the covers are some interesting tricks.

As part of CreateDefaultBuilder, it wires up configuration and logging so you don’t have to do it in the Startup class any longer. This is a smart move as you really want logging and configuration as early as possible. So with this setup, what the CreateDefaultBuilder is actually doing is this (from the MetaPackages github repo):

var builder = new WebHostBuilder()
  .UseKestrel()
  .UseContentRoot(Directory.GetCurrentDirectory())
  .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);
      }
  })
  .ConfigureLogging((hostingContext, logging) =>
  {
      logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
      logging.AddConsole();
      logging.AddDebug();
  })
  .UseIISIntegration()
  .UseDefaultServiceProvider((context, options) =>
  {
      options.ValidateScopes = context.HostingEnvironment.IsDevelopment();
  });

It’s just a shortcut for setting up the content root, kestrel, IIS integration, logging and configuration. But look at those two calls to ConfigureAppConfiguration and ConfigureLogging because they’re interesting.

ConfigureAppConfiguration is where it sets up and reads appsettings.json and appsettings.debug.json (or release) as well as environment variables. It is a lambda that is called to set that up.

Same for ConfigureLogging.

So that if you’re like me, you might want to setup your own logging or configuration, you just need to specify a lambda for your own needs like so:

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

public static IWebHost BuildWebHost(string[] args)
{
  return WebHost.CreateDefaultBuilder()
    .ConfigureAppConfiguration(ConfigConfiguration)
    .ConfigureLogging(ConfigureLogger)
    .UseStartup<Startup>()
    .Build();
}

static void ConfigConfiguration(WebHostBuilderContext ctx, IConfigurationBuilder config)
{
  config.SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("config.json", false, true)
    .AddXmlFile("settings.xml", true)
    .AddEnvironmentVariables();

}

static void ConfigureLogger(WebHostBuilderContext ctx, ILoggingBuilder logging)
{
  logging.AddConfiguration(ctx.Configuration.GetSection("Logging"));
  logging.AddConsole();
  logging.AddDebug();
}

So when I converted a project to 2.0, I didn’t want to switch over to appconfig.json, since I’ve been calling it config.json. So I just overrode the config stuff to read from the json file, an xml file and the environment variables. It does move this code to Program.cs instead of startup, but I like the approach.

In Comes Entity Framework Core Tooling

Looking at this code, my instinct was to remove the BuildWebHost call as it seemed unnecessary:

public static void Main(string[] args)
{
  WebHost.CreateDefaultBuilder()
    .ConfigureAppConfiguration(ConfigConfiguration)
    .ConfigureLogging(ConfigureLogger)
    .UseStartup<Startup>()
    .Build()
    .Run();
}

Normally, Entity Framework Core’s tooling wouldn’t care about any of this. But I was getting a strange exception when using the tooling:

No parameterless constructor was found on 'YourContext'. Either 
add a parameterless constructor to 'YourContext' or add 
an implementation of 'IDesignTimeDbContextFactory<YourContext>' 
in the same assembly as 'YourContext'.

Odd, but after hunting down someone in the EF team on github, I got my answer. In Entity Framework Core 2.0, the tooling will try to instantiate your context so it can do it works one of two ways:

First it will look for an implementation of this new IDesignTimeDbContextFactory interface (which just allows it to create a new context). If you don’t have this interface, it will attempt to startup your project, but it doesn’t want to start listening for HTTP calls, so it attempts to find an IWebHost but it never runs it. How does it look for it? It tries to call a public static method of the Program class called BuildWebHost. Yeah, so that BuildWebHost is a convention. If you want to get rid of it, just implement the IDesignTimeDbContextFactory, it’s really simple.

Why is this good? First of all, it allows you to decide what path to take when creating a DbContext class in design time. It also solves the awkward problem of Data assemblies needing a startup just to support the Entity Framework Core tools.

You could implement the IDesignTimeDbContextFactory, but that does require that you build up your own service factory to serve up your DbContext (or have a DbContext that has an empty constructor. My approach is a bit hacky, but it works:

public static void Main(string[] args)
{
  WebHost.CreateDefaultBuilder()
    .ConfigureAppConfiguration(ConfigConfiguration)
    .ConfigureLogging(ConfigureLogger)
    .UseStartup<Startup>()
    .Build()
    .Run();
}

// Only used by EF Tooling
public static IWebHost BuildWebHost(string[] args)
{
  return WebHost.CreateDefaultBuilder()
    .ConfigureAppConfiguration((ctx, cfg) =>
    {
      cfg.SetBasePath(Directory.GetCurrentDirectory())
        .AddJsonFile("config.json", true) // require the json file!
        .AddEnvironmentVariables();
    })
    .ConfigureLogging((ctx, logging) => { }) // No logging
    .UseStartup<Startup>()
    .UseSetting("DesignTime", "true")
    .Build();
}

I use the void Main to build up my runtime webhost, but then have a BuildWebHost (that I know EF Tooling is the only user of) that simplifies the configuration, removes the logging, and sets a setting for DesignTime so I can fork my Startup based on the fact that it’s being used at ‘design time’. For example, in my startup class, I don’t want to seed the database during EF Tooling:

if (_config["DesignTime"] != "true")
{
  using (var scope = app.ApplicationServices.CreateScope())
  {
    var initializer = scope.ServiceProvider.GetRequiredService<YourSampleDataInitializer>();
    initializer.RunAsync().Wait();
  }
}

The call to UseSetting simply adds an ad-hoc setting to the configuration system. Btw, the creation of the scope is a new ASP.NET Core 2.0 requirement that you’re always using Scoped services inside a scope (the Configure method is not a default scope).

HTH!