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:
- No support for startup assemblies (
IHostingStartup
) - No support for calling
UseStartup<Startup>
- Fewer logging providers
- No EventLog log provider for logging to the Windows event log
- No Debug provider for logging to the debugger console
- No EventSource provider for writing to ETW (Windows) or LTTng (Linux)
- Missing web hosting features
- No support for
UseStaticWebAssets()
for loading static assets from referenced projects and packages - No IIS integration
- No support for
- Missing features in the Kestrel configuration
- No support for
Regex
oralpha
constraints in routing
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 differentHostBuilder
instances in a previous post. If you want a better understanding ofWebApplicationBuilder
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 ofnew HostApplicationBuilder()
. - The slim builder calls
ConfigureSlimWebHost()
andConfigureWebDefaultsCore()
instead ofConfigureWebHostDefaults()
.
- 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
- The slim version calls
ApplyDefaultAppConfigurationSlim()
instead ofApplyDefaultAppConfiguration()
. As far as I can tell the slim version is essentially the same as the default version, but it usesConfigurationManager
. - The slim version calls
AddDefaultServicesSlim()
instead ofAddDefaultServices()
. The slim method adds the stripped down logging providers, whereas the original adds the more complete set.
- 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 callsAddRouting()
. - In the slim version,
null
is passed in, soConfigureWebDefaultsWorker()
callsAddRoutingCore()
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.