ASP.NET Core in Action 30 uilding ASP.NET Core apps with the generic host and Startup

Part 5 Going further with ASP.NET Core‌

Parts 1 through 4 of this book touched on all the aspects of ASP.NET Core you need to learn to build an HTTP application, whether that’s server-rendered applications using Razor Pages or JavaScript Object Notation (JSON) APIs using minimal APIs. In part 5 we look at four topics that build on what you’ve learned so far: customizing ASP.NET Core to your needs, interacting with third-party HTTP APIs, background services, and testing.

In chapter 30 we start by looking at an alternative way to bootstrap your ASP.NET Core applications, using the generic host instead of the WebApplication approach you’ve seen so far in the book. The generic host was the standard way to bootstrap apps before .NET 6 (and is the approach you’ll find in previous editions of this book), so it’s useful to recognize the pattern, but it also comes in handy for building non-HTTP applications, as you’ll see in chapter 34.

In part 1 you learned about the middleware pipeline, and you saw how it is fundamental to all ASP.NET Core applications. In chapter 31 you’ll learn how to take full advantage of the pipeline, creating branching middleware pipelines, custom middleware, and simple middleware- based endpoints. You’ll also learn how to handle some complex chicken-and-egg configuration issues that often arise in real-life applications. Finally, you’ll learn how to replace the built-in dependency injection container with a third-party alternative.

In chapter 32 you’ll learn how to create custom components for working with Razor Pages and API controllers. You’ll learn how to create custom Tag Helpers and validation attributes, and I’ll introduce a new component—view components—for encapsulating logic with Razor view rendering. You’ll also learn how to replace the attribute-based validation framework used by default in ASP.NET Core with an alternative.

Most apps you build aren’t designed to stand on their own. It’s common for your app to need to interact with APIs, whether those are APIs for sending emails, taking payments, or interacting with your own internal applications. In chapter 33 you’ll learn how to call these APIs using the IHttpClientFactory abstraction to simplify configuration, add transient fault handling, and avoid common pitfalls.

This book deals primarily with serving HTTP traffic, both server-rendered web pages using Razor Pages and web APIs commonly used by mobile and single-page applications.

However, many apps require long-running background tasks that execute jobs on a schedule or that process items from a queue. In chapter 34 I’ll show how you can create these long-running background tasks in your ASP.NET Core applications. I’ll also show how to create standalone services that have only background tasks, without any HTTP handling, and how to install them as a Windows Service or as a Linux systemd daemon.

Chapters 35 and 36, the final chapters, cover testing your application. The exact role of testing in application development can lead to philosophical arguments, but in these chapters I stick to the practicalities of testing your app with the xUnit test framework. You’ll see how to create unit tests for your apps, test code that’s dependent on EF Core using an in-memory database provider, and write integration tests that can test multiple aspects of your application at the same time.

In the fast-paced world of web development there’s always more to learn, but by the end of part 5 you should have everything you need to build applications with ASP.NET Core, whether they be server-rendered page-based applications, APIs, or background services.

In the appendices for this book, I provide some background and resources about .NET. Appendix A describes how to prepare your development environment by installing .NET 7 and an IDE or editor. In appendix B you’ll find a list of resources I use to learn more about ASP.NET Core and to stay up to date with the latest features.

30 Building ASP.NET Core apps with the generic host and Startup

This chapter covers

• Using the generic host and a Startup class to bootstrap your ASP.NET Core app

• Understanding how the generic host differs from WebApplication

• Building a custom generic IHostBuilder

• Choosing between the generic host and minimal hosting

Some of the biggest changes introduced in ASP.NET Core in .NET 6 were the minimal hosting APIs, namely the WebApplication and WebApplicationBuilder types you’ve seen throughout this book. These were introduced to dramatically reduce the amount of code needed to get started with ASP.NET Core and are now the default way to build ASP.NET Core apps.‌

Before .NET 6, ASP.NET Core used a different approach to bootstrap your app: the generic host, IHost, IHostBuilder, and a Startup class. Even though this approach is not the default in .NET 7, it’s still valid, so it’s important that you’re aware of it, even if you don’t need to use it yourself. In this chapter I introduce the generic host and show how it relates to the minimal hosting APIs you’re already familiar with. In chapter 34 you’ll learn how to use the generic host approach to build nonweb apps too.

I start by introducing the two main concepts: the generic host components (IHostBuilder and IHost) and the Startup class. These split your app bootstrapping code between two files, Program.cs and Startup.cs, handling different aspects of your app’s configuration. You’ll learn why this split was introduced, where each component is configured, and how it compares with minimal hosting using WebApplication.

In section 30.4 you’ll learn how the helper function Host.CreateDefaultBuilder() works and use this knowledge to customize the IHostBuilder instance. This can give you greater control than minimal hosting, which may be useful in some situations.

In section 30.5 we take a step back and look at some of the drawbacks in the generic host bootstrapping code we’ve explored, particularly its apparent complexity compared to minimal hosting with WebApplication.

Finally, in section 30.6 I discuss some of the reasons you might nevertheless choose to use the generic host instead of minimal hosting in your .NET 7 app. In most cases I suggest using minimal hosting with WebApplication, but there are valid cases in which the generic host makes sense.

30.1 Separating concerns between two files‌

As you’ve seen throughout this book, the standard way to create an ASP.NET Core application in .NET 7 is with the WebApplicationBuilder and WebApplication classes inside Program.cs, using top-level statements. Before .NET 6, however, ASP.NET Core used a different approach, which you can still use in .NET 7 if you wish.‌‌

This approach typically uses a traditional static void Main() entry point (although top-level statements are supported) and splits its bootstrapping code across two files, as shown in figure 30.1:

• Program.cs—This contains the entry point for the application, which bootstraps a host object. This is where you configure the infrastructure of your application, such as Kestrel, integration with Internet Information Services (IIS), and configuration sources.

• Startup.cs—The Startup class is where you configure your dependency injection (DI) container, your middleware pipeline, and your application’s endpoints.

alt text

Figure 30.1 The different responsibilities of the Program and Startup classes in an ASP.NET Core app that uses the generic host instead of WebApplication

We’ll look at each of these files in turn in section 30.2 and 30.3 to see how they might look for a typical Razor Pages app. I discuss the generic host at the center of this setup and compare the approach with the newer WebApplication APIs you’ve used so far throughout the book.

30.2 The Program class: Building a Web Host‌

All ASP.NET Core apps are fundamentally console applications. With the Startup-based hosting model, the Main entry point builds and runs an IHost instance, as shown in the following listing, which shows a typical Program.cs file. The IHost is the core of your ASP.NET Core application: it contains the HTTP server (Kestrel) for handling requests, along with all the necessary services and configuration to generate responses.‌‌

Listing 30.1 The Program.cs file configures and runs an IHost

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>(); ❻
});
}

❶ Creates an IHostBuilder using the CreateHostBuilder method
❷ Builds and returns an instance of IHost from the IHostBuilder
❸ Runs the IHost and starts listening for requests and generating responses
❹ Creates an IHostBuilder using the default configuration
❺ Configures the application to use Kestrel and listen to HTTP requests
❻ The Startup class defines most of your application’s configuration.

The Main function contains all the basic initialization code required to create a web server and to start listening for requests. It uses an IHostBuilder, created by the call to CreateDefaultBuilder, to define how the generic IHost is configured, before instantiating the IHost with a call to Build().

TIP The IHost object represents your built application. The WebApplication type you’ve used throughout the book also implements IHost.

Much of your app’s configuration takes place in the IHostBuilder created by the call to CreateDefaultBuilder, but it delegates some responsibility to a separate class, Startup. The Startup class referenced in the generic UseStartup<> method is where you configure your app’s services and define your middleware pipeline.

NOTE The code to build the IHostBuilder is extracted to a helper method called CreateHostBuilder. The name of this method is historically important, as it was used implicitly by tooling such as the Entity Framework Core (EF Core) tools, as I discuss in section 30.5.‌

You may be wondering why you need two classes for configuration: Program and Startup. Why not include all your app’s configuration in one class or the other? The idea is to separate code that changes often from code that rarely changes.

The Program class for two different ASP.NET Core applications typically look similar, but the Startup classes often differ significantly (though they all follow the same basic pattern, as you’ll see in section 30.3). You’ll rarely find that you need to modify Program as your application grows, whereas you’ll normally update Startup whenever you add additional features. For example, if you add a new NuGet dependency to your project, you’ll normally need to update Startup to make use of it.

The Program class is where a lot of app configuration takes place, but this is mostly hidden inside the Host.CreateDefaultBuilder method.

CreateDefaultBuilder is a static helper method that simplifies the bootstrapping of your app by creating an IHostBuilder with some common configuration. This is similar to the way you’ve used WebApplication.CreateDefaultBuilder() throughout the book.

NOTE You can create custom HostBuilder instances if you want to customize the default setup and create a completely custom IHost instance, as you’ll see in section 30.4. This is different from WebApplicationBuilder, which always uses the same defaults.

The other helper method used by default is ConfigureWebHostDefaults. This uses a WebHostBuilder object to configure Kestrel to listen for HTTP requests.‌

Creating services with the generic host

It might seem strange that you must call ConfigureWebHostDefaults as well as CreateDefaultBuilder. Couldn’t we have one method? Isn’t handling HTTP requests the whole point of ASP.NET Core?

Well, yes and no! ASP.NET Core 3.0 introduced the concept of a generic host. This allows you to use much of the same framework as ASP.NET Core applications to write non-HTTP applications. These apps can run as console apps or can be installed as Windows services (or as systemd daemons in Linux) to run background tasks or read from message queues, for example.

Kestrel and the web framework of ASP.NET Core build on top of the generic host functionality introduced in ASP.NET Core 3.0. To configure a typical ASP.NET Core app, you configure the generic host features that are common across all apps—features such as configuration, logging, and dependency services. For web applications, you then also configure the services, such as Kestrel, that are necessary to handle web requests. In chapter 34 you’ll see how to build applications using the generic host to run scheduled tasks and build background services.

Even in .NET 7, WebApplication and WebApplicationBuilder use the generic host behind the scenes. You can read more about the evolution of ASP.NET Core’s bootstrapping code and the relationship between IHost and WebApplication on my blog at http://mng.bz/gBBv.

Once the configuration of the IHostBuilder is complete, the call to Build produces the IHost instance, but the application still isn’t handling HTTP requests yet. It’s the call to Run() that starts the HTTP server listening. At this point, your application is fully operational and can respond to its first request from a remote browser.

30.3 The Startup class: Configuring your application‌

As you’ve seen, Program is responsible for configuring a lot of the infrastructure for your app, but you configure some of your app’s behavior in Startup. The Startup class is responsible for configuring two main aspects of your application:

• DI container service registration

• Middleware configuration and mapping of endpoints

You configure each of these aspects in its own method in Startup: service registration in ConfigureServices and middleware/endpoint configuration in Configure. A typical outline of Startup is shown in the following listing.

Listing 30.2 An outline of Startup.cs showing how each aspect is configured

public class Startup
{
public void ConfigureServices(IServiceCollection services) ❶
{
// method details
}
public void Configure(IApplicationBuilder app) ❷
{
// method details
}
}

❶ Configures services by registering them with the IServiceCollection
❷ Configures the middleware pipeline for handling HTTP requests

The IHostBuilder created in Program automatically calls ConfigureServices and then Configure, as shown in figure 30.2. Each call configures a different part of your application, making it available for subsequent method calls. Any services registered in the ConfigureServices method are available to the Configure method. Once configuration is complete, you create an IHost by calling Build() on the IHostBuilder.

alt text

Figure 30.2 The IHostBuilder is created in Program.cs and calls methods on Startup to configure the application’s services and middleware pipeline. Once configuration is complete, the IHost is created by calling Build() on the IHostBuilder.

An interesting point about the Startup class is that it doesn’t implement an interface as such. Instead, the methods are invoked by using reflection to find methods with the predefined names of Configure and ConfigureServices. This makes the class more flexible and enables you to modify the signature of the Configure method to inject any services you registered in ConfigureServices using DI.

TIP If you’re not a fan of the flexible reflection approach, you can implement the IStartup interface or derive from the StartupBase class, which provide the method signatures shown previously in listing 30.2. If you take this approach, you won’t be able to use DI to inject services into the Configure() method.‌‌

ConfigureServices is where you add all your required and custom services to the DI container, exactly as you do with WebApplicationBuilder.Services in a typical .NET 7 ASP.NET Core app. The following listing shows how you might configure all the services for the Razor Pages recipe app you’ve seen throughout this book. This listing also shows how you can access the IConfiguration for your app: by injecting into the Startup constructor. You’ll see how to customize your app’s configuration in section 30.4.

Listing 30.3 Registering services with DI in ConfigureServices

public class Startup
{
public IConfiguration Configuration { get; } ❶
public Startup(IConfiguration configuration) ❶
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services) ❷
{
var conn = Configuration.GetConnectionString("DefaultConnection");
services.AddDbContext<AppDbContext>(options => ❸
options.UseSqlite(conn)); ❸
services.AddDefaultIdentity<ApplicationUser>(options => ❸
options.SignIn.RequireConfirmedAccount = true) ❸
.AddEntityFrameworkStores<AppDbContext>(); ❸
services.AddScoped<RecipeService>(); ❹
services.AddRazorPages(); ❺
services.AddScoped<IAuthorizationHandler, IsRecipeOwnerHandler>();
services.AddAuthorizationBuilder()
.AddPolicy("CanManageRecipe",
p => p.AddRequirements(new IsRecipeOwnerRequirement()));
}
public void Configure(IApplicationBuilder app) => { /* Not shown */ }
}

❶ The IConfiguration for the app is injected into the constructor.
❷ You must register your services against the provided IServiceCollection.
❸ Registers all the EF Core and ASP.NET Core Identity services
❹ Registers the custom service implementations
❺ Registers the framework services

After configuring all your services, you need to set up your middleware pipeline and map your endpoints. The process is similar to configuring your middleware pipeline using WebApplication:

• You add middleware to the pipeline by calling Use* extension methods on an IApplicationBuilder instance.

• The order in which you add the middleware to the pipeline is important and defines the final pipeline order.

• You can add middleware conditionally based on the environment.

However, there are some important differences between the WebApplication approach you’ve seen so far and the Startup approach:

• The IWebHostEnvironment for your app is exposed directly on WebApplication.Environment. To access this information inside Startup, you must inject it into the constructor or the Configure method using DI.

• As you saw in chapter 4, WebApplication automatically adds a lot of middleware to your pipeline, such as routing middleware, endpoint middleware, and the authentication middleware. You must add this middleware manually when using the Startup approach.

• WebApplication implements both IApplicationBuilder and IEndpointRouteBuilder, so you can add endpoints directly to WebApplication, by calling MapGet() or MapRazorPages(), for example.When using the Startup approach, you must call UseEndpoints() and map all your endpoints in a lambda method instead.

• The Configure method is not async, so it’s cumbersome to do async tasks. By contrast, when using WebApplication, you’re free to use async methods between any of your general bootstrapping code.

Despite these caveats, in many cases your Startup.Configure method will look almost identical to the way you configure the pipeline on WebApplication. The following listing shows how the Configure() method for the Razor Pages recipe app might look.‌

Listing 30.4 Startup.Configure() for a Razor Pages application

public class Startup
{
public void Configure(
IApplicationBuilder app, ❶
IWebHostEnvironment env) ❷
{
if (env.IsDevelopment()) ❸
{
app.UseDeveloperExceptionPage(); ❹
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting(); ❺
app.UseAuthentication();
app.UseAuthorization(); ❻
app.UseEndpoints(endpoints => ❼
{
endpoints.MapRazorPages(); ❽
});
}
}

❶ IApplicationBuilder is used to build the middleware pipeline.
❷ Other services can be accepted as parameters.
❸ Different behavior when in development or production
❹ WebApplication adds this automatically. You must explicitly add it when using
Startup.
❺ Similarly, you must explicitly call UseRouting.
❻ Must always be placed between the call to UseRouting and UseEndpoints
❼ Adds the endpoint middleware, which executes the endpoints
❽ Maps the Razor Pages endpoints

In this example, the IWebHostEnvironment object is injected into the Configure() method using DI so that you can configure the middleware pipeline differently in development and production. In this case, we add the DeveloperExceptionPageMiddleware to the pipeline when we’re running in development.‌

NOTE Remember that WebApplication adds this middleware automatically, but with Startup you must add it manually. The same goes for all the other automatically added middleware.

After adding all the middleware to the pipeline, you come to the UseEndpoints() call, which adds the EndpointMiddleware to the pipeline. When you use WebApplication, you rarely need to call this, as WebApplication automatically adds it at the end of the pipeline, but when you use Startup, you should add it at the end of your pipeline.

Note as well that the call to UseEndpoints() is where you define all the endpoints in your application. Whether they’re Razor Pages, Model-View-Controller (MVC) controllers, or minimal APIs, you must register them in the UseEndpoints() lambda.

NOTE Endpoints must be registered inside the call to UseEndpoints() using the IEndpointRouteBuilder instance from the lambda method.

Other than the noted differences, moving your service, middleware, and endpoint configuration between a Startup-based approach and WebApplication should be relatively simple, which may lead you to wonder whether there’s any good reason to choose the Startup approach over WebApplication. As always, the answer is “It depends,” but one possible reason is so that you can customize your IHostBuilder.

30.4 Creating a custom IHostBuilder‌

As you saw in section 30.2, the default way to work with a Startup class in ASP.NET Core is to use the Host.CreateDefaultBuilder() method. This opinionated helper method sets up many defaults for your app. It is analogous to the WebApplication‌.CreateBuilder() method in that way.

However, you don’t have to use the CreateDefaultBuilder method to create an IHostBuilder instance: you can directly create a HostBuilder instance and customize it from scratch if you prefer. Before you start doing that, though, it’s worth seeing some of the things the CreateDefaultBuilder method gives you and what they’re used for. You may then consider customizing the default HostBuilder instance instead of creating a completely bespoke instance.‌

NOTE You can use Host.CreateDefaultBuilder() in .NET 7 even if you’re not using ASP.NET Core by installing the Microsoft.Extensions.Hosting package. You’ll learn how to create non-HTTP applications using the generic host in chapter 34.

The defaults chosen by CreateDefaultBuilder are ideal when you’re initially setting up an app, but as your application grows, you may find you need to break it apart and tinker with some of the internals. The following listing shows a rough overview of the CreateDefaultBuilder method, so you can see how the HostBuilder is constructed. It’s not exhaustive or complete, but it should give you an idea of the amount of work the CreateDefaultBuilder method does for you!

Listing 30.5 The Host.CreateDefaultBuilder method

public static IHostBuilder CreateDefaultBuilder(string[] args)
{
var builder = new HostBuilder() ❶
.UseContentRoot(Directory.GetCurrentDirectory()) ❷
.ConfigureHostConfiguration(IConfigurationBuilder config => ❸
{ ❸
config.AddEnvironmentVariables("DOTNET_"); ❸
config.AddCommandLine(args); ❸
}) ❸
.ConfigureAppConfiguration((hostingContext, config) => ❹
{ ❹
IHostEnvironment env = hostingContext.HostingEnvironment; ❹
config ❹
.AddJsonFile("appsettings.json") ❹
.AddJsonFile($"appsettings.{env.EnvironmentName}.json"); ❹
if (env.IsDevelopment()) ❹
{ ❹
config.AddUserSecrets(); ❹
} ❹
config ❹
.AddEnvironmentVariables() ❹
.AddCommandLine(); ❹
}) ❹
.ConfigureLogging((hostingContext, logging) => ❺
{ ❺
logging.AddConfiguration( ❺
hostingContext.Configuration.GetSection("Logging")); ❺
logging.AddConsole(); ❺
logging.AddDebug(); ❺
logging.AddEventSourceLogger(); ❺
logging.AddEventLog(); ❺
}) ❺
.UseDefaultServiceProvider((context, options) => ❻
{ ❻
var isDevelopment = context.HostingEnvironment ❻
.IsDevelopment(); ❻
options.ValidateScopes = isDevelopment; ❻
options.ValidateOnBuild = isDevelopment; ❻
}); ❻
return builder; ❼
}

❶ Creates an instance of HostBuilder
❷ The content root defines the directory where configuration files can be found.
❸ Configures hosting settings such as determining the hosting environment
❹ Configures application settings
❺ Sets up the logging infrastructure
❻ Configures the DI container, optionally enabling verification settings
❼ Returns HostBuilder for further configuration by calling extra methods before calling Build()

The first method called on HostBuilder is UseContentRoot(). This tells the application in which directory it can find any configuration or Razor files it needs later. This is typically the folder in which the application is running, hence the call to GetCurrentDirectory.

TIP Remember that ContentRoot is not where you store static files that the browser can access directly. That’s the WebRoot, typically wwwroot.

The ConfigureHostingConfiguration() method is where your application determines which HostingEnvironment it’s currently running in. The framework looks for environment variables that start with "DOTNET_" (such as the DOTNET_ENVIRONMENT variable you learned about in chapter 10) and command-line arguments to determine whether it’s running in a development or production environment. This is used to populate the IWebHostEnvironment object that’s used throughout your app.‌

The ConfigureAppConfiguration() method is where you configure the main IConfiguration object for your app, populating it from appsettings.json files, environment variables, and User Secrets, for example. The default builder populates the configuration using all the sources shown in listing 30.5, which is similar to the configuration WebApplicationBuilder uses.‌

TIP There are some important differences in how the IConfiguration object is built using the default builder and the approach used by WebApplicationBuilder. You can read about these differences on my blog at http://mng.bz/e11V.

Next up after app configuration comes ConfigureLogging(). ConfigureLogging is where you specify the logging settings and providers for your application, which you learned about in chapter 26. In addition to setting up the default ILoggerProviders, this method sets up log filtering, using the IConfiguration prepared in ConfigureAppConfiguration().

The last method call shown in listing 30.5, UseDefaultServiceProvider, configures your app to use the built-in DI container. It also sets the ValidateScopes and ValidateOnBuild options based on the current HostingEnvironment. This ensures that when running the application in the development environment, the container automatically checks for captured dependencies, which you learned about in chapter 9.‌‌

As you can see, CreateDefaultBuilder does a lot for you. In many cases, these defaults are exactly what you need, but if they’re not, the default builder is optional. You could call new HostBuilder() and start customizing it from there, but you’d need to set up everything that CreateHostBuilder does: logging, hosting configuration, and service provider configuration, as well as your app configuration.

An alternative approach is to layer additional configuration on top of the existing defaults. In the following listing, I show how to add a Seq logging provider to the configured providers using ConfigureLogging(), as well as how to reconfigure the app configuration to load only from the appsettings.json provider by clearing the default providers.

Listing 30.6 Customizing the default HostBuilder

public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureLogging(logBuilder => logBuilder.AddSeq()) ❶
.ConfigureAppConfiguration((hostContext, config) => ❷
{
config.Sources.Clear(); ❸
config.AddJsonFile("appsettings.json"); ❹
}
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}

❶ Adds the Seq logging provider to the configuration
❷ HostBuilder provides a hosting context and an instance of ConfigurationBuilder.
❸ Clears the providers configured by default in CreateDefaultBuilder
❹ Adds a JSON configuration provider, providing the filename of the configuration file

A new HostBuilder is created in CreateDefaultBuilder() and executes all the configuration methods you saw in listing 30.5. Next, the HostBuilder invokes the extra ConfigureLogging() and ConfigureAppConfiguration() methods added in listing 30.6. You can call any of the other configuration methods on HostBuilder to further customize the instance before calling Build().‌

NOTE Each call to a Configure() method on HostBuilder adds an extra configuration function to the setup code; these calls don’t replace existing Configure () calls. The configuration methods are executed in the same order in which they’re added to the HostBuilder, so they execute after the CreateDefaultBuilder() configuration methods.

One of the criticisms of early ASP.NET Core apps was that they were quite complex to understand when you’re getting started, and after working your way through this chapter, you might well be able to see why! In the next section we compare the generic host and Startup approach with the newer minimal hosting WebApplication approach and discuss when you might want to use one over the other.‌

30.5 Understanding the complexity of the generic host‌

Before .NET 6, all ASP.NET Core apps used the generic host and Startup approach. Many people liked the consistent structure this added, but it also has some drawbacks and complexity:

• Configuration is split between two files.

• The separation between Program.cs and Startup is somewhat arbitrary.

• The generic IHostBuilder exposes newcomers to legacy decisions.

• The lambda-based configuration can be hard to follow and reason about.

• The pattern-based conventions of Startup may be hard to discover.

• Tooling historically relies on your defining a CreateHostBuilder method in Program.cs.

I’ll address each of these problems in turn and afterward discuss how WebApplication attempted to improve the situation.

Points 1 and 2 in the preceding list deal with the separation between Program.cs and Startup. As you saw in section 30.1, theoretically the intention is that Program.cs defines the host and rarely changes, whereas Startup defines the app features (services, middleware, and endpoints). This seems like a reasonable decision, but one inevitable downside is that you need to flick back and forth between at least two files to understand all your bootstrapping code.

On top of that, you don’t necessarily need to stick to these conventions. You can register services in Program.cs by calling HostBuilder.ConfigureServices(), for example, or register middleware using WebHostBuilder.Configure(). This is relatively rare but not entirely unheard-of, further blurring the lines between the files.

Point 3 relates to the fact that you must call ConfigureWebHostDefaults() (which uses an IWebHostBuilder) to set up Kestrel and register your Startup class. This level of indirection (and the introduction of another builder type) is a remnant of decisions harking back to ASP.NET Core 1.0. For people familiar with ASP.NET Core, this pattern is just one of those things, but it adds confusion when you’re new to it.

NOTE For a walk-through of the evolution of ASP.NET Core bootstrapping code, see my blog post at http://mng.bz/pPPK.

Similarly, the lambda-based configuration mentioned in point 4 can be hard for newcomers to ASP.NET Core to follow. If you’re new to .NET, lambdas are an extra concept you’ll need to understand before you can understand the basics of the code. On top of that, the execution of the lambdas doesn’t necessarily happen sequentially; the HostBuilder essentially queues the lambda methods so they’re executed at the right time. Consider the following snippet:

public static IhostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureLogging(logging => logging.AddSeq())
.ConfigureAppConfiguration(config => {})
.ConfigureServices(s => {})
.ConfigureHostConfiguration(config => {})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});

The lambdas execute in the following order:

  1. ConfigureWebHostDefaults()
  2. ConfigureHostConfiguration()
  3. ConfigureAppConfiguration()
  4. ConfigureLogging()
  5. ConfigureServices()
  6. Startup.ConfigureServices()
  7. Startup.Configure()

For the most part, this ordering detail shouldn’t matter, but it still adds apparent complexity for those who are new to ASP.NET Core.

Point 5 in the list of challenges relates to the Startup class and the default convention/ pattern-based approach. Users coming to ASP.NET Core for the first time will likely be familiar with interfaces and base classes, but they may not have experienced the reflection-based approach.

Using conventions instead of an explicit interface adds flexibility but can make things harder for discoverability. There are also various caveats and edge cases to consider. For example, you can inject only IWebHostEnvironment and IConfiguration into the Startup constructor; you can’t inject anything into the ConfigureServices() method, but you can inject any registered service into Configure(). These are implied rules that you discover primarily by breaking them and then having your app shout at you!‌

TIP The pattern-based approach allows for a lot more than DI into Configure. You can also create environment-specific methods, such as Configure-DevelopmentServices or ConfigureProductionServices, and ASP.NET Core invokes the correct method based on the environment. You can even create a whole StartupProduction class if you wish! For more details on these Startup conventions, see the documentation at http://mng.bz/Oxxw.

The Startup class isn’t the only place where ASP.NET Core relies on opaque conventions. You may remember in section

30.2 I mentioned that Program.cs deliberately extracts the building of the IHostBuilder to a method called CreateHostBuilder. The name of this method was historically important. Tooling such as the EF Core tools hooked into it so that they could load your application configuration and services when running migrations and other functionality. In earlier versions of ASP.NET Core, renaming this method would break all your tooling!

NOTE As of .NET 6, you don’t have to create a CreateHostBuilder method; you can create your whole app inside your Main function (or using top-level statements), and the EF Core tools will work without error. This was fixed partly to add support for WebApplication. If you’re interested in the mechanics of how it was fixed, see my blog at http://mng.bz/Y11z.

Once you’re experienced with ASP.NET Core, most of these gripes become relatively minor. You quickly get used to the standard patterns and avoid the pitfalls. But for new users of ASP.NET Core, Microsoft wanted a smoother experience, closer to the experience you get in many other languages.

The minimal hosting APIs provided by WebApplicationBuilder and WebApplication largely address these concerns. Configuration happens all in one file using an imperative style, with far fewer lambda-based configuration methods or implicit convention-based setup.

All the relevant objects like configuration and environment are exposed as properties on the WebApplicationBuilder or WebApplication types, so they’re easy to discover.‌

WebApplicationBuilder and WebApplication also try to hide much of the complexity and legacy decisions from you. Under the hood, WebApplication uses the generic host, but you don’t need to know that to use it or be productive. As you’ve seen throughout the book, WebApplication automatically adds various middleware to your pipeline, helping you avoid common pitfalls, such as incorrect middleware ordering.

NOTE If you’re interested in how WebApplicationBuilder abstracts over the generic host, see my post at http://mng.bz/GyyD.

In most cases, minimal hosting provides an easier bootstrapping experience to the generic host and Startup, and Microsoft considers it to be the modern way to create ASP.NET Core apps. But there are cases in which you might want to consider using the generic host instead.

30.6 Choosing between the generic host and minimal hosting‌

The introduction of WebApplication and WebApplicationBuilder in .NET 6, also known as minimal hosting, was intended to provide a dramatically simpler “getting started” experience for newcomers to .NET and ASP.NET Core. All the built-in ASP.NET Core templates use minimal hosting now, and in most cases there’s little reason to look back. In this section I discuss some of the cases in which you might still want to use the generic host approach.

In three main cases, you’ll likely want to stick with the generic host instead of using minimal hosting with WebApplication:

• When you already have an ASP.NET Core application that uses the generic host

• When you need (or want) fine control of building the IHost object

• When you’re creating a non-HTTP application

The first use case is relatively obvious: if you already have an ASP.NET Core app that uses the generic host and Startup, you don’t need to change it. You can still upgrade your app to .NET 7, and you shouldn’t need to change any of your startup code. The generic host and Startup are fully supported in .NET 7, but they’re not the default experience.

TIP In many cases, upgrading an existing project to .NET 7 simply requires updating the framework in the .csproj file and updating some NuGet packages. If you’re unlucky, you may find that some APIs have changed. Microsoft publishes upgrade guides for each major version release, so it’s worth reading these before upgrading your apps: http://mng.bz/zXX1.

If you’re creating a new app, but for some reason you don’t like the default options used by WebApplicationBuilder, using the generic host may be your best option. I generally wouldn’t advise this approach, as it will likely require more maintenance than using WebApplication, but it does give you complete control of your bootstrap code if you need or want it.

The final case applies when you’re building an ASP.NET Core application that primarily runs background processing services, handling messages from a queue for example, but doesn’t handle HTTP requests. The minimal hosting WebApplication and WebApplicationBuilder are, as their names imply, focused on building web applications, so they don’t make sense in this situation.

NOTE You’ll learn how to create background tasks and services using the generic host in chapter 34. .NET 8 introduces a non-HTTP version of the WebApplicationBuilder called HostApplicationBuilder which aims to simplify app bootstrapping for your background services.

If you’re not in any of these situations, strongly consider using the minimal hosting WebApplication approach and the imperative, scriptlike bootstrapping of top-level statements.

NOTE The fact that you’re using WebApplication doesn’t mean you have to dump all your service and middleware configuration into Program.cs. For alternative approaches, such as using a Startup class you invoke manually or local functions to separate your configuration, see my blog post at http://mng.bz/0KKJ.

In this chapter I provided a relatively quick overview of the generic host and Startup-based approach. If you’re thinking of moving from the generic host to minimal hosting, or if you’re familiar with minimal hosting but need to work with the generic host, you may find yourself looking around for an equivalent feature in the other hosting model. The documentation for migrating from .NET 5 to .NET 6 provides a good description of the differences between the two models, and how each individual feature has changed. You can find it at http://mng.bz/KeeX.

TIP Alternatively, David Fowler from the .NET team has a similar cheat sheet describing the migration. See http://mng.bz/9DDj.

Whether you choose to use the generic host or minimal hosting, all the same ASP.NET Core concepts are there: configuration, middleware, and DI. In the next chapter you’ll learn about some more advanced uses of each of these concepts, such as creating branching middleware pipelines and custom DI containers.

30.7 Summary

Before .NET 6, ASP.NET Core apps split configuration between two files: Program.cs and Startup.cs. Program.cs contains the entry point for the app and is used to configure and build a IHost object. Startup is where you configure the DI container, middleware pipeline, and endpoints for your app.

The Program class typically contains a method called CreateHostBuilder(), which creates an IHostBuilder instance. The Main entry point invokes CreateHostBuilder(), calls IHostBuilder.Build() to create an instance of IHost, and finally runs the app by calling IHost.Run().

You can create an IHostBuilder by calling Host.CreateDefaultBuilder(). This creates a HostBuilder instance using the default configuration, similar to the configuration used when calling WebApplication.CreateBuilder(). The default HostBuilder uses default logging and configuration providers, configures the hosting environment based on environment variables and command-line arguments, and configures the DI container settings.

ASP.NET Core apps using the generic host typically call ConfigureWebHostDefaults(), on the HostBuilder, providing a lambda that calls UseStartup() on an IWebHostBuilder instance. This tells the HostBuilder to configure the DI container and middleware pipeline based on the Startup class.

Use the Startup class to register services with DI, configure your middleware pipeline, and register your endpoints. It is a conventional class, in that it doesn’t have to implement an interface or base class. Instead, the IHostBuilder looks for specific named methods to invoke using reflection.

Register your DI services in the ConfigureServices(IServiceCollection) method of Startup. You register services using the same Add* methods you use to register services on WebApplicationBuilder.Services when using minimal hosting.

If you need to access your app’s IConfiguration or IWebHostEnvironment (exposed as Configuration and Environment, respectively, on WebApplicationBuilder), you can inject them into your Startup constructor.You can’t inject any other services into the Startup constructor.

Register your middleware pipeline in Startup.Configure(IApplicationBuilder). Use the same Use* methods you use with WebApplication to add middleware to the pipeline. As for WebApplication, the order in which you add the middleware defines their order in the pipeline.

WebApplication automatically adds middleware such as the routing middleware and endpoint middleware to the pipeline when you’re using minimal hosting. When using Startup, you must explicitly add this middleware yourself.

To register endpoints, call UseEndpoints(endpoints => {}) and call the appropriate Map functions on the provided IEndpointRouteBuilder in the lambda function. This differs significantly from minimal hosting, in which you can call Map directly on the WebApplication instance.

You can customize the IHostBuilder instance by adding configuration methods such as ConfigureLogging() or ConfigureAppConfiguration(). These methods run after any previous invocations, adding extra layers of configuration to the IHostBuilder instance.

The generic host is flexible but has greater inherent complexity due to its deferred execution style, extensive use of lambda methods, and heavy use of convention. Minimal hosting aimed to simplify the bootstrapping code to make it more imperative, reducing much of the indirection.

Minimal hosting enforces more defaults but is generally easier to work with for newcomers to ASP.NET Core.

If you already have an ASP.NET Core application using Startup and the generic host, there’s no need to switch to using WebApplication and minimal hosting; the generic host is fully supported in .NET 7. Additionally, if you’re creating a non- HTTP application, the generic host is currently the best option.

If you’re creating a new ASP.NET Core application, minimal hosting will likely provide a smoother experience. You should generally favor it over the generic host for new apps unless you need fine control of the IHostBuilder configuration.

Leave a Reply

Your email address will not be published. Required fields are marked *