浅讲.Net 6 之 WebApplicationBuilder

发布于 2022年 01月 24日 00:51

本文梯子

     

    正文

    介绍

    .Net 6为我们带来的一种全新的引导程序启动的方式。与之前的拆分成Program.cs和Startup不同,整个引导启动代码都在Program.cs中。

    WebApplicationBuilder builder = WebApplication.CreateBuilder(args);在上篇文章中,我简要描述了如何使用 WebApplication和WebApplicationBuilder配置 ASP.NET Core 应用程序。在这篇文章中,我们来深入看下代码.

    正文

    我们示例程序的第一步是执行WebApplicationBuilder builder = WebApplication.CreateBuilder(args);创建一个WebApplicationBuilder实例。

    从命令行中分配Args参数,并将选项对象传递给WebApplicationBuilder构造函数的WebApplicationOptions

      /// <summary>
            /// Initializes a new instance of the <see cref="WebApplicationBuilder"/> class with preconfigured defaults.
            /// </summary>
            /// <param name="args">Command line arguments</param>
            /// <returns>The <see cref="WebApplicationBuilder"/>.</returns>
            public static WebApplicationBuilder CreateBuilder(string[] args) =>
                new(new() { Args = args });
    

    WebApplicationOptions和WebApplicationBuilder后面在讲

        internal WebApplicationBuilder(WebApplicationOptions options, Action<IHostBuilder>? configureDefaults = null)
    
        /// <summary>
        /// Options for configuing the behavior for <see cref="WebApplication.CreateBuilder(WebApplicationOptions)"/>.
        /// </summary>
        public class WebApplicationOptions
        {
            /// <summary>
            /// The command line arguments.
            /// </summary>
            public string[]? Args { get; init; }
    
            /// <summary>
            /// The environment name.
            /// </summary>
            public string? EnvironmentName { get; init; }
    
            /// <summary>
            /// The application name.
            /// </summary>
            public string? ApplicationName { get; init; }
    
            /// <summary>
            /// The content root path.
            /// </summary>
            public string? ContentRootPath { get; init; }
    
            /// <summary>
            /// The web root path.
            /// </summary>
            public string? WebRootPath { get; init; }
        }
    
    
    
    

    WebApplicationBuilder由一堆只读属性和一个方法组成Build(),该方法创建了一个WebApplication. 我删除了部分讲解用不到的代码。

    如果您熟悉 ASP.NET Core,那么其中许多属性都使用以前版本中的常见类型

    • IWebHostEnvironment: 用于检索环境
    • IServiceCollection: 用于向 DI 容器注册服务。
    • ConfigurationManager: 用于添加新配置和检索配置值。我在之前的文章有讲
    • ILoggingBuilder: 用于注册额外的日志提供程序

    在WebHost和Host性质很有趣,因为它们暴露出新的类型,ConfigureWebHostBuilder和ConfigureHostBuilder。这些类型分别实现IWebHostBuilder和IHostBuilder。

    公开IWebHostBuilder和IHostBuilder接口对于允许从.NET 6 之前的应用程序迁移到新的最小托管,我们如何将的lambda风格配置IHostBuilder与命令式风格的WebApplicationBuilder协调起来,这就是ConfigureHostBuilder和ConfigureWebHostBuilder与一些内部沿来IHostBuilder实现。

    
        public sealed class WebApplicationBuilder
        {
            private const string EndpointRouteBuilderKey = "__EndpointRouteBuilder";
    
            private readonly HostBuilder _hostBuilder = new();
            private readonly BootstrapHostBuilder _bootstrapHostBuilder;
            private readonly WebApplicationServiceCollection _services = new();
            private readonly List<KeyValuePair<string, string>> _hostConfigurationValues;
    
            private WebApplication? _builtApplication;
    
            /// <summary>
            /// Provides information about the web hosting environment an application is running.
            /// </summary>
            public IWebHostEnvironment Environment { get; }
    
            /// <summary>
            /// A collection of services for the application to compose. This is useful for adding user provided or framework provided services.
            /// </summary>
            public IServiceCollection Services { get; }
    
            /// <summary>
            /// A collection of configuration providers for the application to compose. This is useful for adding new configuration sources and providers.
            /// </summary>
            public ConfigurationManager Configuration { get; }
    
            /// <summary>
            /// A collection of logging providers for the application to compose. This is useful for adding new logging providers.
            /// </summary>
            public ILoggingBuilder Logging { get; }
    
            /// <summary>
            /// An <see cref="IWebHostBuilder"/> for configuring server specific properties, but not building.
            /// To build after configuration, call <see cref="Build"/>.
            /// </summary>
            public ConfigureWebHostBuilder WebHost { get; }
    
            /// <summary>
            /// An <see cref="IHostBuilder"/> for configuring host specific properties, but not building.
            /// To build after configuration, call <see cref="Build"/>.
            /// </summary>
            public ConfigureHostBuilder Host { get; }
    
            /// <summary>
            /// Builds the <see cref="WebApplication"/>.
            /// </summary>
            /// <returns>A configured <see cref="WebApplication"/>.</returns>
            public WebApplication Build()
            {
                // Wire up the host configuration here. We don't try to preserve the configuration
                // source itself here since we don't support mutating the host values after creating the builder.
                _hostBuilder.ConfigureHostConfiguration(builder =>
                {
                    builder.AddInMemoryCollection(_hostConfigurationValues);
                });
    
                var chainedConfigSource = new TrackingChainedConfigurationSource(Configuration);
    
                // Wire up the application configuration by copying the already built configuration providers over to final configuration builder.
                // We wrap the existing provider in a configuration source to avoid re-bulding the already added configuration sources.
                _hostBuilder.ConfigureAppConfiguration(builder =>
                {
                    builder.Add(chainedConfigSource);
    
                    foreach (var (key, value) in ((IConfigurationBuilder)Configuration).Properties)
                    {
                        builder.Properties[key] = value;
                    }
                });
    
                // This needs to go here to avoid adding the IHostedService that boots the server twice (the GenericWebHostService).
                // Copy the services that were added via WebApplicationBuilder.Services into the final IServiceCollection
                _hostBuilder.ConfigureServices((context, services) =>
                {
                    // We've only added services configured by the GenericWebHostBuilder and WebHost.ConfigureWebDefaults
                    // at this point. HostBuilder news up a new ServiceCollection in HostBuilder.Build() we haven't seen
                    // until now, so we cannot clear these services even though some are redundant because
                    // we called ConfigureWebHostDefaults on both the _deferredHostBuilder and _hostBuilder.
                    foreach (var s in _services)
                    {
                        services.Add(s);
                    }
    
                    // Add the hosted services that were initially added last
                    // this makes sure any hosted services that are added run after the initial set
                    // of hosted services. This means hosted services run before the web host starts.
                    foreach (var s in _services.HostedServices)
                    {
                        services.Add(s);
                    }
    
                    // Clear the hosted services list out
                    _services.HostedServices.Clear();
    
                    // Add any services to the user visible service collection so that they are observable
                    // just in case users capture the Services property. Orchard does this to get a "blueprint"
                    // of the service collection
    
                    // Drop the reference to the existing collection and set the inner collection
                    // to the new one. This allows code that has references to the service collection to still function.
                    _services.InnerCollection = services;
    
                    var hostBuilderProviders = ((IConfigurationRoot)context.Configuration).Providers;
    
                    if (!hostBuilderProviders.Contains(chainedConfigSource.BuiltProvider))
                    {
                        // Something removed the _hostBuilder's TrackingChainedConfigurationSource pointing back to the ConfigurationManager.
                        // This is likely a test using WebApplicationFactory. Replicate the effect by clearing the ConfingurationManager sources.
                        ((IConfigurationBuilder)Configuration).Sources.Clear();
                    }
    
                    // Make builder.Configuration match the final configuration. To do that, we add the additional
                    // providers in the inner _hostBuilders's Configuration to the ConfigurationManager.
                    foreach (var provider in hostBuilderProviders)
                    {
                        if (!ReferenceEquals(provider, chainedConfigSource.BuiltProvider))
                        {
                            ((IConfigurationBuilder)Configuration).Add(new ConfigurationProviderSource(provider));
                        }
                    }
                });
    
                // Run the other callbacks on the final host builder
                Host.RunDeferredCallbacks(_hostBuilder);
    
                _builtApplication = new WebApplication(_hostBuilder.Build());
    
                // Mark the service collection as read-only to prevent future modifications
                _services.IsReadOnly = true;
    
                // Resolve both the _hostBuilder's Configuration and builder.Configuration to mark both as resolved within the
                // service provider ensuring both will be properly disposed with the provider.
                _ = _builtApplication.Services.GetService<IEnumerable<IConfiguration>>();
    
                return _builtApplication;
            }
    
       private void ConfigureApplication(WebHostBuilderContext context, IApplicationBuilder app)
            {
                Debug.Assert(_builtApplication is not null);
    
                // UseRouting called before WebApplication such as in a StartupFilter
                // lets remove the property and reset it at the end so we don't mess with the routes in the filter
                if (app.Properties.TryGetValue(EndpointRouteBuilderKey, out var priorRouteBuilder))
                {
                    app.Properties.Remove(EndpointRouteBuilderKey);
                }
    
                if (context.HostingEnvironment.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }
    
                // Wrap the entire destination pipeline in UseRouting() and UseEndpoints(), essentially:
                // destination.UseRouting()
                // destination.Run(source)
                // destination.UseEndpoints()
    
                // Set the route builder so that UseRouting will use the WebApplication as the IEndpointRouteBuilder for route matching
                app.Properties.Add(WebApplication.GlobalEndpointRouteBuilderKey, _builtApplication);
    
                // Only call UseRouting() if there are endpoints configured and UseRouting() wasn't called on the global route builder already
                if (_builtApplication.DataSources.Count > 0)
                {
                    // If this is set, someone called UseRouting() when a global route builder was already set
                    if (!_builtApplication.Properties.TryGetValue(EndpointRouteBuilderKey, out var localRouteBuilder))
                    {
                        app.UseRouting();
                    }
                    else
                    {
                        // UseEndpoints will be looking for the RouteBuilder so make sure it's set
                        app.Properties[EndpointRouteBuilderKey] = localRouteBuilder;
                    }
                }
    
                // Wire the source pipeline to run in the destination pipeline
                app.Use(next =>
                {
                    _builtApplication.Run(next);
                    return _builtApplication.BuildRequestDelegate();
                });
    
                if (_builtApplication.DataSources.Count > 0)
                {
                    // We don't know if user code called UseEndpoints(), so we will call it just in case, UseEndpoints() will ignore duplicate DataSources
                    app.UseEndpoints(_ => { });
                }
    
                // Copy the properties to the destination app builder
                foreach (var item in _builtApplication.Properties)
                {
                    app.Properties[item.Key] = item.Value;
                }
    
                // Remove the route builder to clean up the properties, we're done adding routes to the pipeline
                app.Properties.Remove(WebApplication.GlobalEndpointRouteBuilderKey);
    
                // reset route builder if it existed, this is needed for StartupFilters
                if (priorRouteBuilder is not null)
                {
                    app.Properties[EndpointRouteBuilderKey] = priorRouteBuilder;
                }
            }
    
            private sealed class LoggingBuilder : ILoggingBuilder
            {
                public LoggingBuilder(IServiceCollection services)
                {
                    Services = services;
                }
    
                public IServiceCollection Services { get; }
            }
        }
    
    
    ConfigureHostBuilder
     public sealed class ConfigureHostBuilder : IHostBuilder, ISupportsConfigureWebHost
    
    
        IHostBuilder ISupportsConfigureWebHost.ConfigureWebHost(Action<IWebHostBuilder> configure, Action<WebHostBuilderOptions> configureOptions)
            {
                throw new NotSupportedException("ConfigureWebHost() is not supported by WebApplicationBuilder.Host. Use the WebApplication returned by WebApplicationBuilder.Build() instead.");
            }
    

    ConfigureHostBuilder实现IHostBuilder和ISupportsConfigureWebHost,但 ISupportsConfigureWebHost 的实现是假的什么意思呢?

    这意味着虽然以下代码可以编译,但是会在运行时抛出异常。

    WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
    builder.Host.ConfigureWebHost(webBuilder =>
    {
        webBuilder.UseStartup<Startup>();
    });
    

    ConfigureServices(),该方法Action<>使用IServiceCollection从WebApplicationBuilder. 所以以下两个调用在功能上是相同的:

    后一种方法显然不值得在正常实践中使用,但仍然可以使用依赖于这种方法的现有代码,

     public IHostBuilder ConfigureAppConfiguration(Action<HostBuilderContext, IConfigurationBuilder> configureDelegate)
        {
            // Run these immediately so that they are observable by the imperative code
            configureDelegate(_context, _configuration);
            return this;
        }
    
        public IHostBuilder ConfigureServices(Action<HostBuilderContext, IServiceCollection> configureDelegate)
        {
            // Run these immediately so that they are observable by the imperative code
            configureDelegate(_context, _services);
            return this;
        }
    
    
    builder.Services.AddSingleton<MyImplementation>();
    
    
    builder.Host.ConfigureServices((ctx, services) => services.AddSingleton<MyImplementation>());
    

    并非所有委托ConfigureHostBuilder都立即传递给运行中的方法。例如UseServiceProviderFactory()保存在列表中,稍后在调用WebApplicationBuilder.Build()

          public IHostBuilder UseServiceProviderFactory<TContainerBuilder>(IServiceProviderFactory<TContainerBuilder> factory) where TContainerBuilder : notnull
            {
                if (factory is null)
                {
                    throw new ArgumentNullException(nameof(factory));
                }
    
                _operations.Add(b => b.UseServiceProviderFactory(factory));
                return this;
            }
    

    BootstrapHostBuilder

    回到ConfigureHostBuilder我们看内部的BootstrapHostBuilder,它记录了IHostBuilder收到的所有调用。例如ConfigureHostConfiguration()和ConfigureServices(),
    这与ConfigureHostBuilder立即执行提供的委托相比,BootstrapHostBuilder的保存将委托提供给稍后执行的列表。这类似于泛型的HostBuilder工作方式。但请注意,这BootstrapHostBuilder调用Build()会引发异常

     internal class BootstrapHostBuilder : IHostBuilder
        {
            private readonly IServiceCollection _services;
            private readonly List<Action<IConfigurationBuilder>> _configureHostActions = new();
            private readonly List<Action<HostBuilderContext, IConfigurationBuilder>> _configureAppActions = new();
            private readonly List<Action<HostBuilderContext, IServiceCollection>> _configureServicesActions = new();
    
            public IHost Build()
            {
                // HostingHostBuilderExtensions.ConfigureDefaults should never call this.
                throw new InvalidOperationException();
            }
            public IHostBuilder ConfigureHostConfiguration(Action<IConfigurationBuilder> configureDelegate)
            {
                _configureHostActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate)));
                return this;
            }
    
            public IHostBuilder ConfigureServices(Action<HostBuilderContext, IServiceCollection> configureDelegate)
            {
                // HostingHostBuilderExtensions.ConfigureDefaults calls this via ConfigureLogging
                _configureServicesActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate)));
                return this;
            }
            // .....
        }
    

    WebApplicationBuilder构造函数

    最后我们来看WebApplicationBuilder构造函数,注释都给大家翻译了

    internal WebApplicationBuilder(WebApplicationOptions options, Action<IHostBuilder>? configureDefaults = null)
            {
                Services = _services;
    
                var args = options.Args;
    
                //尽早运行方法配置通用和web主机默认值,以从appsettings.json填充配置
                //要预填充的环境变量(以DOTNET和ASPNETCORE为前缀)和其他可能的默认源
                //正确的默认值。
                _bootstrapHostBuilder = new BootstrapHostBuilder(Services, _hostBuilder.Properties);
    
                   //不要在这里指定参数,因为我们希望稍后应用它们,以便
                //可以覆盖ConfigureWebHostDefaults指定的默认值
                _bootstrapHostBuilder.ConfigureDefaults(args: null);
    
                // This is for testing purposes
                configureDefaults?.Invoke(_bootstrapHostBuilder);
    
                //我们上次在这里指定了命令行,因为我们跳过了对ConfigureDefaults的调用中的命令行。
                //args可以包含主机和应用程序设置,因此我们要确保
                //我们适当地订购这些配置提供程序,而不复制它们
                if (args is { Length: > 0 })
                {
                    _bootstrapHostBuilder.ConfigureAppConfiguration(config =>
                    {
                        config.AddCommandLine(args);
                    });
                }
    
                // ....
            }
    
    // 自ConfigureWebHostDefaults覆盖特定于主机的设置(应用程序名称)以来,上次将参数应用于主机配置。
                _bootstrapHostBuilder.ConfigureHostConfiguration(config =>
                {
                    if (args is { Length: > 0 })
                    {
                        config.AddCommandLine(args);
                    }
    
                    // Apply the options after the args
                    options.ApplyHostConfiguration(config);
                });
    
    

    到此你可能都非常疑惑这玩意到底在干嘛。只要能看明白下面代码就好了,调用BootstrapHostBuilder.RunDefaultCallbacks(),
    它以正确的顺序运行我们迄今为止积累的所有存储的回调,以构建HostBuilderContext. 该HostBuilderContext则是用来最终设定的剩余性能WebApplicationBuilder。

    完成特定于应用程序的配置后,在由我们手动调用Build()创建一个WebApplication实例。

                Configuration = new();
    
                // Collect the hosted services separately since we want those to run after the user's hosted services
                _services.TrackHostedServices = true;
    
                // This is the application configuration
                var (hostContext, hostConfiguration) = _bootstrapHostBuilder.RunDefaultCallbacks(Configuration, _hostBuilder);
    
                // Stop tracking here
                _services.TrackHostedServices = false;
    
                // Capture the host configuration values here. We capture the values so that
                // changes to the host configuration have no effect on the final application. The
                // host configuration is immutable at this point.
                _hostConfigurationValues = new(hostConfiguration.AsEnumerable());
    
                // Grab the WebHostBuilderContext from the property bag to use in the ConfigureWebHostBuilder
                var webHostContext = (WebHostBuilderContext)hostContext.Properties[typeof(WebHostBuilderContext)];
    
                // Grab the IWebHostEnvironment from the webHostContext. This also matches the instance in the IServiceCollection.
                Environment = webHostContext.HostingEnvironment;
                Logging = new LoggingBuilder(Services);
                Host = new ConfigureHostBuilder(hostContext, Configuration, Services);
                WebHost = new ConfigureWebHostBuilder(webHostContext, Configuration, Services);
    

    WebApplicationBuilder.Build()

    该Build()方法不是非常复杂,首先是将配置的配置源复制到_hostBuilder的ConfigurationBuilder实现中。调用此方法时,builder它最初为空,因此这将填充由默认构建器扩展方法添加的所有源,以及您随后配置的额外源。

     // source itself here since we don't support mutating the host values after creating the builder.
                _hostBuilder.ConfigureHostConfiguration(builder =>
                {
                    builder.AddInMemoryCollection(_hostConfigurationValues);
                });
    
                 _hostBuilder.ConfigureAppConfiguration(builder =>
                {
                    builder.Add(chainedConfigSource);
    
                    foreach (var (key, value) in ((IConfigurationBuilder)Configuration).Properties)
                    {
                        builder.Properties[key] = value;
                    }
                });
    
    
    

    接下来,是一样的事情IServiceCollection,将它们从_services实例复制到_hostBuilder的集合中。

    // This needs to go here to avoid adding the IHostedService that boots the server twice (the GenericWebHostService).
                // Copy the services that were added via WebApplicationBuilder.Services into the final IServiceCollection
                _hostBuilder.ConfigureServices((context, services) =>
                {
                    // We've only added services configured by the GenericWebHostBuilder and WebHost.ConfigureWebDefaults
                    // at this point. HostBuilder news up a new ServiceCollection in HostBuilder.Build() we haven't seen
                    // until now, so we cannot clear these services even though some are redundant because
                    // we called ConfigureWebHostDefaults on both the _deferredHostBuilder and _hostBuilder.
                    foreach (var s in _services)
                    {
                        services.Add(s);
                    }
    
                    // Add the hosted services that were initially added last
                    // this makes sure any hosted services that are added run after the initial set
                    // of hosted services. This means hosted services run before the web host starts.
                    foreach (var s in _services.HostedServices)
                    {
                        services.Add(s);
                    }
    
                    // Clear the hosted services list out
                    _services.HostedServices.Clear();
    
                    // Add any services to the user visible service collection so that they are observable
                    // just in case users capture the Services property. Orchard does this to get a "blueprint"
                    // of the service collection
    
                    // Drop the reference to the existing collection and set the inner collection
                    // to the new one. This allows code that has references to the service collection to still function.
                    _services.InnerCollection = services;
    
                    var hostBuilderProviders = ((IConfigurationRoot)context.Configuration).Providers;
    
                    if (!hostBuilderProviders.Contains(chainedConfigSource.BuiltProvider))
                    {
                        // Something removed the _hostBuilder's TrackingChainedConfigurationSource pointing back to the ConfigurationManager.
                        // This is likely a test using WebApplicationFactory. Replicate the effect by clearing the ConfingurationManager sources.
                        ((IConfigurationBuilder)Configuration).Sources.Clear();
                    }
    
                    // Make builder.Configuration match the final configuration. To do that, we add the additional
                    // providers in the inner _hostBuilders's Configuration to the ConfigurationManager.
                    foreach (var provider in hostBuilderProviders)
                    {
                        if (!ReferenceEquals(provider, chainedConfigSource.BuiltProvider))
                        {
                            ((IConfigurationBuilder)Configuration).Add(new ConfigurationProviderSource(provider));
                        }
                    }
                });
    

    接下来运行我们在ConfigureHostBuilder属性中收集的任何回调

    // Run the other callbacks on the final host builder
    Host.RunDeferredCallbacks(_hostBuilder);
    

    最后我们调用_hostBuilder.Build()构建Host实例,并将其传递给 的新实例WebApplication。调用_hostBuilder.Build()是调用所有注册回调的地方。

    _builtApplication = new WebApplication(_hostBuilder.Build());
    

    最后,为了保持一切一致ConfigurationManager实例被清除,并链接到存储在WebApplication. 此外IServiceCollectiononWebApplicationBuilder被标记为只读,因此在调用后尝试添加服务WebApplicationBuilder将抛出一个InvalidOperationException. 最后WebApplication返回。

     // Mark the service collection as read-only to prevent future modifications
                _services.IsReadOnly = true;
    
                // Resolve both the _hostBuilder's Configuration and builder.Configuration to mark both as resolved within the
                // service provider ensuring both will be properly disposed with the provider.
                _ = _builtApplication.Services.GetService<IEnumerable<IConfiguration>>();
    
                return _builtApplication;
    

    差不多就是这样.

    结语

    作者第一次解读源码,该讲解可能有些罗里吧嗦乱七八糟,直接看文章也可能看不懂,推荐大家打开源码对比阅读,我也在整理整理思绪争取下一篇好一些。

    最后欢迎各位读者关注我的博客, https://github.com/MrChuJiu/Dppt/tree/master/src 欢迎大家Star

    联系作者:加群:867095512 @MrChuJiu

    推荐文章