blog post image
Andrew Lock avatar

Andrew Lock

~12 min read

Comparing WebApplication.CreateBuilder() to the new CreateSlimBuilder() method

Exploring the .NET 8 preview - Part 3

In this post I look at the new CreateSlimBuilder method. This was introduced in .NET 8 as an alternative to the existing WebApplication.CreateBuilder method, to support AOT scenarios. In this post I discuss the things missing from the slim builder at a high level, and then dig into the code to see how it's implemented.

As these posts are all using the preview builds, some of the features may change (or be removed) before .NET 8 finally ships in November 2023!

Why do we need CreateSlimBuilder?

In my previous post, I showed the Ahead-of-time (AOT) minimal API api template introduced in .NET 8. The very first line in that template is:

var builder = WebApplication.CreateSlimBuilder(args); 

compare that to the equivalent line in the web "empty" template (which is essentially unchanged from .NET 6-8)

var builder = WebApplication.CreateBuilder(args);

So why the change?

As I described in my previous post, an important part of the AOT compilation is trimming—removing all the parts of the framework and your app that aren't used. This significantly reduces the final binary size, and is necessary to achieve reasonable binary sizes.

In a "normal", JIT compiled, .NET app, you pay a little in binary size for including unused features or features you don't need, but it's relatively minimal. If you never invoke the unused methods, they're never compiled, and many assemblies may not even be loaded.

With AOT, you pay through the nose for every new feature. Removing features you don't need can have a huge impact on the size of the final AOT binary.

As I described in my previous post, the compiler also needs to be able to statically determine which types and methods in your app are actually used, which means reflection-based APIs are typically problematic.

CreateSlimBuilder removes a number of features which are either incompatible with AOT, or which are less useful for apps in which AOT shines, such as serverless and cloud-native apps. Even if you're not targeting these sorts of apps, you might want to consider CreateSlimBuilder if you don't need any of the features it removes. In the next section, we'll see what those changes are

What's missing from CreateSlimBuilder?

The CreateSlimBuilder method is similar to CreateBuilder. Both methods initialize a WebApplicationBuilder, but CreateSlimBuilder only initializes the minimum ASP.NET Core features necessary to run an app, as described in the docs. That means there's lots of things missing or changed:

You likely haven't directly used IHostingStartup in your apps, but if you've ever deployed to Azure App Service (AAS), you've probably used it without realising! IHostingStartup lets you load assemblies at runtime which change how your application is configured, by customising the services in your DI container for example. Last I checked, this is how AAS was adding some of their integrations. However, as mentioned previously, you can't load arbitrary dlls at runtime with AOT, because there's no JIT!

The support for UseStartup<> was removed as this is another design which is tricky for AOT, as it invokes methods in your app using reflection. This might actually be possible in AOT scenarios, but using Startup classes with WebApplicationBuilder isn't something people tend to do, so it mostly just adds bloat.

If you try to call builder.WebHost.UseStartup<Program>(); in your application you'll get a compile-time error from an analyzer, warning you that this will fail at runtime. I'm a big fan of the ASP.NET teams embracing of analyzers to catch runtime issues like this!

Most of the remaining features that were removed have been taken out because they're not typically used in the scenarios that AOT targets i.e. cloud/serverless, Linux, environments:

  • These environments tend to be Linux (if you're worried about startup times, you're not going to be hosting behind IIS on Windows) so removing the EventLog and IIS support makes sense.
  • It's common to handle HTTPS or HTTP/3 behind a TLS termination proxy, so that you don't have to manage certificates in your app.
  • You won't (hopefully!) have a debugger attached in a production environment.
  • UseStaticWebAssets() isn't necessary when publishing your app, and typically isn't used by API applications anyway

The lack of support for Regex route constraints is purely due to the fact that Regex support adds a lot of code, especially for the .NET 7 non-backtracking support! Removing support for inline Regex constraints by default removed about 1MB from the binary size. You can reenable the inline Regex support if required by using the following:

var builder = WebApplication.CreateSlimBuilder();

builder.Services.AddRoutingCore().Configure<RouteOptions>(options => {
    options.SetParameterPolicy<RegexInlineRouteConstraint>("regex");
});

If you want to add some of these features back in, such as the logging providers or HTTPS support, you can do so. For example, to enable HTTPS support call builder.WebHost.UseKestrelHttpsConfiguration().

If you're just interested in using CreateSlimBuilder() then that's about all you need to know. In the next section we dig deep into all the actual changes in the builder.

How is it implemented

In this section I look at how CreateSlimBuilder() is implemented, mostly by comparing it to the existing CreateBuilder() method.

A warning—this section isn't for newcomers and is pretty dry. It's much more depth than you would ever need to know, and is mostly just what I went through to figure out the first half of this post!

WebApplicationBuilder was added in .NET 6 but it built on all the existing generic IHostBuilder and IWebHostBuilder abstractions that were introduced in previous versions of .NET Core. Consequently, it's a bit of a Frankenstein's monster of components! For the most part, CreateSlimBuilder() creates a very similar WebApplicationBuilder to CreateBuilder().

I looked in depth at how WebApplicationBuilder works by combining various different HostBuilder instances in a previous post. If you want a better understanding of WebApplicationBuilder in general, I suggest reading that post!

We'll start with the method definition, in WebApplication, where both methods are defined as static helpers:

public sealed class WebApplication : IHost, IApplicationBuilder, IEndpointRouteBuilder, IAsyncDisposable
{
    public static WebApplicationBuilder CreateBuilder() =>
        new WebApplicationBuilder(new WebApplicationOptions());

    public static WebApplicationBuilder CreateSlimBuilder() =>
        new WebApplicationBuilder(new WebApplicationOptions(), slim: true);
                                                            // 👆 the only difference
}

As you can see, these methods call different WebApplicationBuilder constructors, with the slim builder passing the value slim: true.

If compare these two constructors (see diff below), we find that the "slim" version (somewhat surprisingly) includes a whole lot of additional code! The reason for this, as you'll see shortly, is that the default builder delegates a lot of this work to other helper methods, whereas the slim builder does most of the work inline here (and strips out what it doesn't need). In fact, there are only 2 "main" changes:

  • The slim builder calls Host.CreateEmptyApplicationBuilder() instead of new HostApplicationBuilder().
  • The slim builder calls ConfigureSlimWebHost() and ConfigureWebDefaultsCore() instead of ConfigureWebHostDefaults().
- internal WebApplicationBuilder(WebApplicationOptions options, Action<IHostBuilder>? configureDefaults = null)
+ internal WebApplicationBuilder(WebApplicationOptions options, bool slim, Action<IHostBuilder>? configureDefaults = null)
{
    var configuration = new ConfigurationManager();

    configuration.AddEnvironmentVariables(prefix: "ASPNETCORE_");

+   // SetDefaultContentRoot needs to be added between 'ASPNETCORE_' and 'DOTNET_' in order to match behavior of the non-slim WebApplicationBuilder.
+   SetDefaultContentRoot(options, configuration);

+   // Add the default host environment variable configuration source.
+   // This won't be added by CreateEmptyApplicationBuilder.
+   configuration.AddEnvironmentVariables(prefix: "DOTNET_");

+   _hostApplicationBuilder = Microsoft.Extensions.Hosting.Host.CreateEmptyApplicationBuilder(new HostApplicationBuilderSettings
-   _hostApplicationBuilder = new HostApplicationBuilder(new HostApplicationBuilderSettings
    {
        Args = options.Args,
        ApplicationName = options.ApplicationName,
        EnvironmentName = options.EnvironmentName,
        ContentRootPath = options.ContentRootPath,
        Configuration = configuration,
    });

+   // Ensure the same behavior of the non-slim WebApplicationBuilder by adding the default "app" Configuration sources
+   ApplyDefaultAppConfigurationSlim(_hostApplicationBuilder.Environment, configuration, options.Args);
+   AddDefaultServicesSlim(configuration, _hostApplicationBuilder.Services);

+   // configure the ServiceProviderOptions here since CreateEmptyApplicationBuilder won't.
+   var serviceProviderFactory = GetServiceProviderFactory(_hostApplicationBuilder);
+   _hostApplicationBuilder.ConfigureContainer(serviceProviderFactory);

    // Set WebRootPath if necessary
    if (options.WebRootPath is not null)
    {
        Configuration.AddInMemoryCollection(new[]
        {
            new KeyValuePair<string, string?>(WebHostDefaults.WebRootKey, options.WebRootPath),
        });
    }

    // Run methods to configure web host defaults early to populate services
    var bootstrapHostBuilder = new BootstrapHostBuilder(_hostApplicationBuilder);

    // This is for testing purposes
    configureDefaults?.Invoke(bootstrapHostBuilder);

-   bootstrapHostBuilder.ConfigureWebHostDefaults(webHostBuilder =>
+   bootstrapHostBuilder.ConfigureSlimWebHost(webHostBuilder =>
    {
+       AspNetCore.WebHost.ConfigureWebDefaultsCore(webHostBuilder);

        // Runs inline.
        webHostBuilder.Configure(ConfigureApplication);

        webHostBuilder.UseSetting(WebHostDefaults.ApplicationKey, _hostApplicationBuilder.Environment.ApplicationName ?? "");
        webHostBuilder.UseSetting(WebHostDefaults.PreventHostingStartupKey, Configuration[WebHostDefaults.PreventHostingStartupKey]);
        webHostBuilder.UseSetting(WebHostDefaults.HostingStartupAssembliesKey, Configuration[WebHostDefaults.HostingStartupAssembliesKey]);
        webHostBuilder.UseSetting(WebHostDefaults.HostingStartupExcludeAssembliesKey, Configuration[WebHostDefaults.HostingStartupExcludeAssembliesKey]);
    },
    options =>
    {
        // We've already applied "ASPNETCORE_" environment variables to hosting config
        options.SuppressEnvironmentConfiguration = true;
    });

    // This applies the config from ConfigureWebHostDefaults
    // Grab the GenericWebHostService ServiceDescriptor so we can append it after any user-added IHostedServices during Build();
    _genericWebHostServiceDescriptor = bootstrapHostBuilder.RunDefaultCallbacks();

    // Grab the WebHostBuilderContext from the property bag to use in the ConfigureWebHostBuilder. Then
    // grab the IWebHostEnvironment from the webHostContext. This also matches the instance in the IServiceCollection.
    var webHostContext = (WebHostBuilderContext)bootstrapHostBuilder.Properties[typeof(WebHostBuilderContext)];
    Environment = webHostContext.HostingEnvironment;

    Host = new ConfigureHostBuilder(bootstrapHostBuilder.Context, Configuration, Services);
    WebHost = new ConfigureWebHostBuilder(webHostContext, Configuration, Services);
}

CreateEmptyApplicationBuilder() is a simple public helper method that creates a new HostApplicationBuilder().

public static class Host
{
    public static HostApplicationBuilder CreateEmptyApplicationBuilder(HostApplicationBuilderSettings? settings)
            => new HostApplicationBuilder(settings, empty: true);
}

So again, the empty/slim version calls a different constructor, this time of HostApplicationBuilder. If we compare the default constructor to the "empty" constructor (below), we can see a lot of code has been removed. Most of this was actually moved into the WebApplicationBuilder constructor, but there are some notable changes

- public HostApplicationBuilder(HostApplicationBuilderSettings? settings)
+ internal HostApplicationBuilder(HostApplicationBuilderSettings? settings, bool empty)
{
    settings ??= new HostApplicationBuilderSettings();
    Configuration = settings.Configuration ?? new ConfigurationManager();

-    if (!settings.DisableDefaults)
-    {
-        if (settings.ContentRootPath is null && Configuration[HostDefaults.ContentRootKey] is null)
-        {
-            HostingHostBuilderExtensions.SetDefaultContentRoot(Configuration);
-        }
-
-        Configuration.AddEnvironmentVariables(prefix: "DOTNET_");
-    }

    Initialize(settings, out _hostBuilderContext, out _environment, out _logging);

-    ServiceProviderOptions? serviceProviderOptions = null;
-    if (!settings.DisableDefaults)
-    {
-        HostingHostBuilderExtensions.ApplyDefaultAppConfiguration(_hostBuilderContext, Configuration, settings.Args);
-        HostingHostBuilderExtensions.AddDefaultServices(_hostBuilderContext, Services);
-        serviceProviderOptions = HostingHostBuilderExtensions.CreateDefaultServiceProviderOptions(_hostBuilderContext);
-    }

    _createServiceProvider = () =>
    {
        // Call _configureContainer in case anyone adds callbacks via HostBuilderAdapter.ConfigureContainer<IServiceCollection>() during build.
        // Otherwise, this no-ops.
        _configureContainer(Services);
-       return serviceProviderOptions is null ? Services.BuildServiceProvider() : Services.BuildServiceProvider(serviceProviderOptions);
+       return Services.BuildServiceProvider();
    };
}

Next we'll look at the difference between ConfigureWebHost and ConfigureSlimWebHost. As you can see below, these extension methods each create a different IWebHostBuilder implementation: GenericWebHostBuilder and SlimWebHostBuilder respectively.

- public static IHostBuilder ConfigureWebHost(this IHostBuilder builder, Action<IWebHostBuilder> configure, Action<WebHostBuilderOptions> configureWebHostBuilder)
+ public static IHostBuilder ConfigureSlimWebHost(this IHostBuilder builder, Action<IWebHostBuilder> configure, Action<WebHostBuilderOptions> configureWebHostBuilder)
{
    return ConfigureWebHost(
        builder,
-       static (hostBuilder, options) => new GenericWebHostBuilder(hostBuilder, options),
+       static (hostBuilder, options) => new SlimWebHostBuilder(hostBuilder, options),
        configure,
        configureWebHostBuilder);
}

Comparing the constructors of these two classes, the SlimWebHostBuilder really is a slimmed down version of the GenericWebHostBuilder. Two features have been excised, for the reasons discussed earlier:

  • Hosting assembly (IHostingStartup) support has been removed
  • UseStartup<T> support has been removed
+ public SlimWebHostBuilder(IHostBuilder builder, WebHostBuilderOptions options)
- public GenericWebHostBuilder(IHostBuilder builder, WebHostBuilderOptions options)
    : base(builder, options)
{
    _builder.ConfigureHostConfiguration(config =>
    {
        config.AddConfiguration(_config);

-       // We do this super early but still late enough that we can process the configuration
-       // wired up by calls to UseSetting
-       ExecuteHostingStartups();
    });

-   // IHostingStartup needs to be executed before any direct methods on the builder
-   // so register these callbacks first
-   _builder.ConfigureAppConfiguration((context, configurationBuilder) =>
-   {
-       if (_hostingStartupWebHostBuilder != null)
-       {
-           var webhostContext = GetWebHostBuilderContext(context);
-           _hostingStartupWebHostBuilder.ConfigureAppConfiguration(webhostContext, configurationBuilder);
-       }
-   });

    _builder.ConfigureServices((context, services) =>
    {
        var webhostContext = GetWebHostBuilderContext(context);
        var webHostOptions = (WebHostOptions)context.Properties[typeof(WebHostOptions)];

        // Add the IHostingEnvironment and IApplicationLifetime from Microsoft.AspNetCore.Hosting
        services.AddSingleton(webhostContext.HostingEnvironment);
#pragma warning disable CS0618 // Type or member is obsolete
        services.AddSingleton((AspNetCore.Hosting.IHostingEnvironment)webhostContext.HostingEnvironment);
        services.AddSingleton<IApplicationLifetime, GenericWebHostApplicationLifetime>();
#pragma warning restore CS0618 // Type or member is obsolete

        services.Configure<GenericWebHostServiceOptions>(options =>
        {
            // Set the options
            options.WebHostOptions = webHostOptions;
-           // Store and forward any startup errors
-           options.HostingStartupExceptions = _hostingStartupErrors;
        });

        // REVIEW: This is bad since we don't own this type. Anybody could add one of these and it would mess things up
        // We need to flow this differently
        services.TryAddSingleton(sp => new DiagnosticListener("Microsoft.AspNetCore"));
        services.TryAddSingleton<DiagnosticSource>(sp => sp.GetRequiredService<DiagnosticListener>());
        services.TryAddSingleton(sp => new ActivitySource("Microsoft.AspNetCore"));
        services.TryAddSingleton(DistributedContextPropagator.Current);

        services.TryAddSingleton<IHttpContextFactory, DefaultHttpContextFactory>();
        services.TryAddScoped<IMiddlewareFactory, MiddlewareFactory>();
        services.TryAddSingleton<IApplicationBuilderFactory, ApplicationBuilderFactory>();

        services.AddMetrics();
        services.TryAddSingleton<HostingMetrics>();

-       // IMPORTANT: This needs to run *before* direct calls on the builder (like UseStartup)
-       _hostingStartupWebHostBuilder?.ConfigureServices(webhostContext, services);

-       // Support UseStartup(assemblyName)
-       if (!string.IsNullOrEmpty(webHostOptions.StartupAssembly))
-       {
-           ScanAssemblyAndRegisterStartup(context, services, webhostContext, webHostOptions);
-       }
    });
}

Next we move onto the differences between ConfigureWebDefaults and ConfigureWebDefaultsCore. The former is called from the default ConfigureWebHostDefaults() method (called from the "normal" WebApplicationBuilder constructor). The latter is called directly inside the "slim" WebApplicationBuilder constructor.

The obvious differences here are that the slim version doesn't add IIS integration or the static web asset assemblies, as discussed previously. But there's also a difference in how the ConfigureWebDefaultsWorker() method—which configures Kestrel— is called.

- internal static void ConfigureWebDefaults(IWebHostBuilder builder)
+ internal static void ConfigureWebDefaultsCore(IWebHostBuilder builder)
{
-   builder.ConfigureAppConfiguration((ctx, cb) =>
-   {
-       if (ctx.HostingEnvironment.IsDevelopment())
-       {
-           StaticWebAssetsLoader.UseStaticWebAssets(ctx.HostingEnvironment, ctx.Configuration);
-       }
-   });

    ConfigureWebDefaultsWorker(
-        builder.UseKestrel(ConfigureKestrel),
+        builder.UseKestrelCore().ConfigureKestrel(ConfigureKestrel), 
-        configureRouting: services => services.AddRouting()
+        configureRouting: null
    );

-   builder
-       .UseIIS()
-       .UseIISIntegration();
}

To compare the difference in this configuration, it makes sense to inline the "original" UseKestrel(ConfigureKestrel) method, which gives us the following:

- builder.UseKestrel().ConfigureKestrel(ConfigureKestrel)
+ builder.UseKestrelCore().ConfigureKestrel(ConfigureKestrel), 

It's now more obvious that the only difference is UseKestrel() vs. UseKestrelCore(). And if we take a look at UseKestrel(), it's clear that the only differences in the Kestrel configuration between the default builder and the slim builder are the HTTPS and Quic support, as expected.

public static IWebHostBuilder UseKestrel(this IWebHostBuilder hostBuilder)
{
    return hostBuilder
        .UseKestrelCore()
        .UseKestrelHttpsConfiguration() // 👈 missing in the "slim" builder
        .UseQuic(options => // 👈 missing in the "slim" builder
        {
            // Configure server defaults to match client defaults.
            // https://github.com/dotnet/runtime/blob/a5f3676cc71e176084f0f7f1f6beeecd86fbeafc/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs#L118-L119
            options.DefaultStreamErrorCode = (long)Http3ErrorCode.RequestCancelled;
            options.DefaultCloseErrorCode = (long)Http3ErrorCode.NoError;
        });
}

There's one final difference in how ConfigureWebDefaultsWorker is called between the builders:

  • In the default version, ConfigureWebDefaultsWorker() is passed a lambda that calls AddRouting().
  • In the slim version, null is passed in, so ConfigureWebDefaultsWorker() calls AddRoutingCore() instead.

If we take a look at the code in AddRouting() we can see that this is where the Regex route constraint is added in the default builder (or removed from the slim builder, depending on how you look at it!)

public static IServiceCollection AddRouting(this IServiceCollection services)
{
    services.AddRoutingCore();
    services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<RouteOptions>, RegexInlineRouteConstraintSetup>());
    return services;
}

And that's it! It took a long time for me to navigate through all the code and diffs on GitHub to figure out the difference between the builders, so the second half of this post is mostly just documenting what I found. As long as you understand the high-level differences described in the first half of the post (or in the docs) then you should be good to go!

Summary

In this post, I looked at the WebApplication.CreateSlimBuilder() method introduced in the .NET 8 previews to support the AOT-compatible api template. I looked at why a new method was necessary, and what the differences were in using this method compared to the existing WebApplication.CreateBuilder() method. In the second half of the post, I dug into all the actual code diffs from GitHub to understand how these changes were made under the hood.

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