应用--WebApplication

应用--Program中的WebApplication

在6.0,微软团队对于NetCore做了很大的改变,其中有一个改变就是推出了新的托管模型--最小托管模型,使用该模型可以创建最小的web应用。(最小webapi请查看官网

需要掌握:

  • 什么是最小托管模型?以及他的作用
  • 什么是WebApplication和WebApplicationBuilder?他们和Host的区别是什么
  • WebApplication代码上以及对应逻辑上的构造流程,能简单描述就好了
  • WebApplication的六个属性以及其作用,为什么需要这6个属性?
  • WebApplicationBuilder的作用
  • WebApplication的6个属性以及继承4个接口有什么用

构造流程

得到WebApplicationBuilder构造器 -> 配置服务 -> build()方法得到WebApplication对象 -> 配置中间件 -> 运行主机

C# 复制代码
// 得到应用构造器:WebApplicationBuilder
var builder = WebApplication.CreateBuilder(args);
// 配置日志
builder.Logging.AddLog4Net("ConfigFile/log4net.config");
// 得到应用:WebApplication
var app = builder.Build();
// 配置中间件
app.UseStaticFiles();
// 运行主机
app.Run();

你可能在疑惑,在3.0至5.0的版本都是直接调用Host.CreateDefaultBuilder()方法得到HostBuilder构造器,然后调用ConfigureWebHostBuilder()配置WebHost,然后在上面配置一些服务,构建然后运行。而6.0使用WebApplication.CreateBuilder(args)方法得到的是一个WebApplicationBuilder构造器,然后构建运行,他们有什么区别吗?

答:没什么区别,流程都是一样的,WebApplication对主机和服务做了一个更进一步的封装,使得更加方便配置和学习,而且额外暴露2个Host属性WebHost属性用来配置(这2个属性也是方便之前的版本迁移到6.0的关键)。举个很简单的例子

区别 在3.0至5.0的版本中 6.0版本中
中间管道的配置 必须放在Startup.cs类中的Configure方法中,或者通过ConfigureWebHostDefaults中的webBuilder 来配置服务 通过调用app.UseXXXX来配置
路由中间件 使用app.UseRouting()之后才能app.UseEndpoints() 因为WebApplication继承了WebIEndpointRouteBuilder可以直接将路由,而无需显式调用 UseEndpointsUseRouting

按照上方Program.cs流程顺序介绍相关类和方法

1. CreateBuilder(args) 方法:

使用默认值来生成构造一个WebApplicationBuilder对象

该方法有3个重载:

  • WebApplication.CreateBuilder():使用预配置的默认值来构造;
  • WebApplication.CreateBuilder(String []):根据传入的命令行参数初始化;
  • WebApplication.CreateBuilder(WebApplicationOption):根据传入的预配置来构造;
C# 复制代码
public class WebApplicationOptions
{
    public WebApplicationOptions(); 
    
    // 命令行参数
    public string[]? Args { get; init; }

    // 环境名称。
    public string? EnvironmentName { get; init; }
    
    // 应用程序名称。
    public string? ApplicationName { get; init; }
    
    // 内容根路径。
    public string? ContentRootPath { get; init; }

    // Web 根路径。
    public string? WebRootPath { get; init; }
}

WebApplicationBuilder类:

要创建一个WebApplication对象,需要一个IHost对象IHost对象是通过IHostBuilder创建的,而WebApplication需要WebApplicationBuilder来构建,所以WebApplicationBuilder还需要一个IHostBuilder对象,我们针对WebApplication的一切配置,最终都会转移到这个对象上面才能生效,所以这就是为什么WebApplicationBuilder提供了这6个属性的原因。

构造函数

当通过调用WebApplication.CreateBuilder()方法的时候,根据命令行的参数传给WebApplicationBuilder的构造函数,而WebApplicationBuilder的构造函数内部会做:

  1. 创建HostBuilder _hostBuilder 类。
  2. 创建BootstrapHostBuilder对象,调用拓展方法ConfigureWebHostBuilder()ConfigureDefaults()方法,将初始化的设置和服务收集起来,然后把收集到的服务和配置注入到Services成员属性Configure成员属性中。
  3. 然后会创建承载托管环境的IWebHostEnvironment,对于Environment成员属性初始化。
  4. 调用Apply()方法得到HostBuilderContext上下文
  5. 使用HostBuilderContextWebHostBuilderContext,创建ConfigureWebHostBuilder和ConfigureHostBuilder并赋值给WebHost和Host属性。初始化Logging属性
  6. 得到一个Configure、Environment、WebHost、Host、Logging属性都被初始化的WebApplication对象

WebApplicationBuilder构造函数源码

c# 复制代码
public class WebApplicationBuilder
{
    private readonly HostBuilder _hostBuilder = new HostBuilder();
    private WebApplication _application;
    // 提供应用程序正在运行的Web托管环境的信息
    public IWebHostEnvironment Environment { get; }

    // 提供应用程序所需要的服务,即依赖注入容器
    public IServiceCollection Services { get; }

    // 提供应用程序所需要的配置
    public ConfigurationManager Configuration { get; }

    // 提供日志记录
    public ILoggingBuilder Logging { get; }

    // 配置WebHost服务器特定属性,实现IWebHostBuilder
    public ConfigureWebHostBuilder WebHost { get; }

    // 配置Host特定属性,实现IHostBuilder
    public ConfigureHostBuilder Host { get; }

    public WebApplicationBuilder(WebApplicationOptions options)
    {
        //创建BootstrapHostBuilder并利用它收集初始化过程中设置的配置、服务和针对依赖注入容器的设置
        var args = options.Args;
        var bootstrap = new BootstrapHostBuilder();
        bootstrap
            .ConfigureDefaults(null)
            // 此处用于中间件的注册
            .ConfigureWebHostDefaults(webHostBuilder => webHostBuilder.Configure(app              =>app.Run(_application.BuildRequestDelegate())))   
            .ConfigureHostConfiguration(config => {
                // 添加命令行配置源
                if (args?.Any() == true)
                {
                    config.AddCommandLine(args);
                }

                // 将WebApplicationOptions配置选项转移到配置中
                Dictionary<string, string>? settings = null;
                if (options.EnvironmentName is not null) {
                    (settings ??= new())[HostDefaults.EnvironmentKey] =  options.EnvironmentName;
                }
                if (options.ApplicationName is not null){
                    (settings ??= new())[HostDefaults.ApplicationKey] = options.ApplicationName;
                }
                if (options.ContentRootPath is not null){
                    (settings ??= new())[HostDefaults.ContentRootKey] = options.ContentRootPath;
                }
                if (options.WebRootPath is not null) {
                    (settings ??= new())[WebHostDefaults.WebRootKey] = options.EnvironmentName;
                }
                if (settings != null)
                {
                    config.AddInMemoryCollection(settings);
                }
            });

        // 将BootstrapHostBuilder收集到配置和服务转移到Configuration和Services上
        // 将应用到BootstrapHostBuilder上针对依赖注入容器的设置转移到_hostBuilder上
        // 得到BuilderContext上下文
        bootstrap.Apply(_hostBuilder, Configuration, Services, out var builderContext);

        // 如果提供了命令行参数,在Configuration上添加对应配置源
        if (options.Args?.Any() == true)
        {
            Configuration.AddCommandLine(options.Args);
        }
        // 构建WebHostBuilderContext上下文
        // 初始化Host、WebHost和Logging属性
        var webHostContext = (WebHostBuilderContext)builderContext.Properties[typeof(WebHostBuilderContext)];
        Environment = webHostContext.HostingEnvironment;
        Host = new ConfigureHostBuilder(builderContext, Configuration, Services);
        WebHost = new ConfigureWebHostBuilder(webHostContext, Configuration, Services);
        Logging = new LogginigBuilder(Services);
    }
   
}

简单来说WebApplicationBuilder的作用就是为了提供构建封装的一个HostBuilder对象

注意:记住这6个属性,NetCore的生态库基本上就是围绕这几个来构建的

接下来我们按照这几个属性来逐一分析其作用

IWebHostEnvironment Environment 属性

接口,继承IHostEnvironment,提供一些该应用程序的环境信息,比如根目录、环境变量、名称等,包含几个属性

  • WebRootPath,用于设置和获取Web的根目录,默认是wwwroot的子文件夹(在5.0的时候。是通过Host.CreateDefaultBuilder()方法去设置的);
  • ApplicationName:应用名称;
  • ContentRootFileProvider:;
  • ContentRootPath:
  • EnvironmentName:环境名称;

比如我们经常用的判断是否是开发环境就是用的该类

demo

C# 复制代码
var builder = WebApplication.CreateBuilder(args); 
bool isDevelopment = builder.Environment.IsDevelopment();
IServiceCollection Services 属性

依赖注入容器 ,可以注入服务,也可也获取服务实例,继承于ICollection<ServiceDescriptor>泛型接口,这个我们后续会在依赖注入章节详细描述。

通过往该属性添加系统服务支持,或者注入自己的服务

demo

C# 复制代码
var builder = WebApplication.CreateBuilder(args); 
// 注入Sql数据库支持
builder.Services.AddDbContext<SqlDbContext>();
// 依赖注入
builder.Services.AddSingleton<PersonService>();
ConfigurationManager Configuration 属性

配置管理, 是6.0版本新增的类,更见简单的用于获取和设置系统配置文件,替换掉了5.0版本中IConfigurationBuilder接口IConfigurationRoot接口(6.0是把这2个接口整合在一起了,看源码可以发现ConfigureManager继承于这2个接口)

ConfigurationManager密封类源码

C# 复制代码
public sealed class ConfigurationManager : IConfigurationBuilder, IConfigurationRoot, IConfiguration, IDisposable
{
    public ConfigurationManager();
    public string this[string key] { get; set; }
    public void Dispose();
    public IEnumerable<IConfigurationSection> GetChildren();
    public IConfigurationSection GetSection(string key);
}

public interface IConfigurationBuilder
{ 
  IDictionary<string, object> Properties { get; }  
  IList<IConfigurationSource> Sources { get; }
  IConfigurationBuilder Add(IConfigurationSource source);
  IConfigurationRoot Build();
}

增加系统配置文件

当你调用builder.Configuration.AddJsonFile("文件名称")拓展方法的来增加自定义配置文件的时候(实际上是往调用的IConfigurationBuilder.Add()方法往IList<IConfigurationSource> Sources { get; }属性增加了一条数据),会将立即加载提供程序并更新配置,这样可以不用等到Build()方法,可以避免在部分生成方法多次加载配置源数据。

demo

C# 复制代码
var builder = WebApplication.CreateBuilder(args); 
builder.Configuration.AddJsonFile("servicesetting.json");

获取系统配置数据

demo

C# 复制代码
var builder = WebApplication.CreateBuilder(args); 
ConfigurationManager config = builder.Configuration;
string value1 = config["DBContextModel:SqlConnection"];
IConfigurationSection value2 = config.GetSection("DBContextModel");
ILoggingBuilder Logging

提供日志记录,包括控制台、调试、事件日志、TraceSource等组件,你是不是在疑惑为什么创建项目的时候appsetting.json配置文件有一个这样的配置?他就和日志记录息息相关

appstting.json文件节点

json 复制代码
 "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },

具体在日志章节描述

ConfigureWebHostBuilder WebHost 属性

继承于IWebHostBuilder接口,目的是复用接口,继承于ISupportsStartUp接口,由于6.0使用的是最小托管模型,所以传统的使用Startup.cs文件来配置服务和注册中间件已经不支持了,继承此接口的是原因是:

微软原话"但是我们希望用户在采用这种编程方式时得到显式的提醒,所以依然让它实现该接口,并在实现的方法中抛出NotImplementedException类型的异常。"

WebApplication构造函数中,通过传进来的一些构造参数初始化一个实例赋值给该属性;

构造函数,通过WebHostContext,Services成员属性、Configure成员属性来初始化

ConfigureWebHostBuilder类源码

C# 复制代码
public class ConfigureWebHostBuilder : IWebHostBuilder, ISupportsStartup
{
    private readonly WebHostBuilderContext _builderContext;
    private readonly IServiceCollection _services;
    private readonly ConfigurationManager _configuration;

    public ConfigureWebHostBuilder(WebHostBuilderContext builderContext, ConfigurationManager configuration, IServiceCollection services)
    {
        _builderContext = builderContext;
        _services = services;
        _configuration = configuration;
    }
}
ConfigureHostBuilder Host 属性

继承于IHostBuilder接口,目的是复用接口,他更多的用来配置主机服务

WebApplication构造函数中,通过bootStrapBuilder收集到的服务,传进来的一些构造参数初始化一个实例赋值给他;

构造函数,通过HostBuilderContext ,Services成员属性、Configure成员属性来初始化

  • Services成员属性会直接赋值ConfigureHostBuilder的_services属性

  • Configure成员属性相关Host的配置会被存放在ConfigureHostBuilder内部类的一个_configureActions字段暂时存起来

ConfigureHostBuilder类源码

C# 复制代码
public class ConfigureHostBuilder : IHostBuilder
{
    private readonly ConfigurationManager _configuration;
    private readonly IServiceCollection _services;
    private readonly HostBuilderContext _context;
    private readonly List<Action<IHostBuilder>> _configureActions = new();

    internal ConfigureHostBuilder(HostBuilderContext context, ConfigurationManager configuration, IServiceCollection services)
    {
        _configuration = configuration;
        _services = services;
        _context = context;
    }
}

BootstrapHostBuilder类:

BootstrapHostBuilder继承于IHostBuilder,目的是为了构建和初始化IHostBuilder对象

这个它的作用是收集初始化IHostBuilder对象提供的设置并将它们分别应用到指定的IServiceCollection、ConfigurationManager和IHostBuilder对象上,在构造函数中会调用他的Apply()方法。

2. Build()方法:

简单来说就是将对于WebApplicationBuilder的一切配置转移到IHostBuilder对象上,然后得到一个WebApplication对象

注意!!!!!

WebApplication一旦创建,环境变量、配置都不允许再次改变(虽然我们也用不着,但是知道就好)

这个方法作用:

WebApplicationConfigure成员属性Services成员属性转移到HostBuilder上面

WebApplication.Build()方法源码

C# 复制代码
 // 获取WebApplication对象,用于配置 HTTP 管道和路由的 Web 应用程序
    public WebApplication Build()
    {
        // 在此处连接主机配置。我们不会尝试保留配置,在此处获取本身,因为我们不支持在创建构建器后更改主机值。
         _hostBuilder.ConfigureHostConfiguration(builder =>
            {
                builder.AddInMemoryCollection(_hostConfigurationValues);
          });
        
        // 将ConfigurationManager的配置转移到_hostBuilder
        _hostBuilder.ConfigureAppConfiguration(builder =>
        {
            builder.AddConfiguration(Configuration);
            foreach (var kv in ((IConfigurationBuilder)Configuration).Properties)
            {
                builder.Properties[kv.Key] = kv.Value;
            }
        });

         var chainedConfigSource = new TrackingChainedConfigurationSource(Configuration);
        
        _hostBuilder.ConfigureServices((context, services) =>
            {
                // 简单来说就是把WeApplicationBuilder中的IServiceCollection属性添加到泛型主机中
                foreach (var s in _services)
                {                
                    services.Add(s);
                }

                // 把服务列表只能关于主机的服务添加到主机中
                // 确保添加的任何托管服务在初始托管服务集之后运行。也就是托管服务在web主机启动前运行
                foreach (var s in _services.HostedServices)
                {
                    services.Add(s);
                }

                // 清除主机托管服务列表
                _services.HostedServices.Clear();

                // 将任何服务添加到用户可见的服务集合中,
                _services.InnerCollection = services;

               // 保留主机中的配置
                var beforeChainedConfig = true;
                var hostBuilderProviders = ((IConfigurationRoot)context.Configuration).Providers;

                if (!hostBuilderProviders.Contains(chainedConfigSource.BuiltProvider))
                {                   
                    ((IConfigurationBuilder)Configuration).Sources.Clear();
                    beforeChainedConfig = false;
                }
                // 使配置管理器与最终_hostBuilder的配置匹配。
                foreach (var provider in hostBuilderProviders)
                {
                    if (ReferenceEquals(provider, chainedConfigSource.BuiltProvider))
                    {
                        beforeChainedConfig = false;
                    }
                    else
                    {
                        IConfigurationBuilder configBuilder = beforeChainedConfig ? _hostConfigurationManager : Configuration;
                        configBuilder.Add(new ConfigurationProviderSource(provider));
                    }
                }
            });

        // 在最终主机构建器上运行其他回调
        Host.RunDeferredCallbacks(_hostBuilder);

        // 构建应用
        _builtApplication = new WebApplication(_hostBuilder.Build());
        
        // 将服务集合标记为只读以防止将来修改
         _services.IsReadOnly = true;

        // 解析_hostBuilder的配置和构建器。 
        _ = _builtApplication.Services.GetService<IEnumerable<IConfiguration>>();
        return _builtApplication;
    }

WebApplication类:

应用类

继承4个接口

  • IHost接口:所以这就是上文当中说到为什么WebApplicationBuilder需要一个IConfigureHostBuilder属性的原因;
  • IApplicationBuilder:提供配置应用程序请求管道机制的类,所以我们的中间件可以直接注册到WebApplication
  • IEndpointRouteBuilder:定义应用程序中路由生成器的协定。 路由生成器指定应用程序的路由。所以我们无需显示调用UseEndpoint、UseRouting这2个中间件,这个在6.0的更新中也提到了;
  • IAsyncDisposable:提供异步释放的接口;

6个重要属性

  • IServiceProvider:应用程序的已配置服务。提供在程序运行期间解析的服务类型,简称依赖注入容器;
  • IConfiguration:应用程序的已配置,可以获取已经配置好的配置源;
  • IWebHostEnvironment: 托管环境信息;
  • IHostApplicationLifetime:允许通知使用者应用程序生存期事件;
  • ILogger:日志服务;
  • ICollection<string> :HTTP 服务器绑定到的 URL 列表。(IServerAddressesFeature:启动地址);

WebApplication类源码

c# 复制代码
public sealed class WebApplication : IHost, IApplicationBuilder, IEndpointRouteBuilder, IAsyncDisposable
{
   public IServiceProvider Services => _host.Services;   
   public IConfiguration Configuration => _host.Services.GetRequiredService<IConfiguration>();
   public IWebHostEnvironment Environment => _host.Services.GetRequiredService<IWebHostEnvironment>();
   public IHostApplicationLifetime Lifetime => _host.Services.GetRequiredService<IHostApplicationLifetime>();
   public ILogger Logger { get; } 
   public ICollection<string> Urls => ServerFeatures.Get<IServerAddressesFeature>()?.Addresses ??
            throw new InvalidOperationException($"{nameof(IServerAddressesFeature)} could not be found.");

}

拓展方法

就是各种系统定义好的中间件服务。

3. Run()方法 :

WebApplication.BuildRequestDelegate()方法

前面调用ConfigureWebHostDefaults()扩展方法提供的委托会将使用BuildRequestDelegate()方法注册的中间件管道,作为请求处理器,至此一个WebApplication对象完成。

## 4. 总结 :
首先,最小托管模型是6.0微软推出来的一个新的应用模板,为的是方便配置和学习,他只有3句代码,利用他可以生成最小webapi。
第一句是var bulider = WebAppliaction.CreateBuilder();
这句代码的作用是通过调用WebAppliaction的工厂方法CreateBuilder()得到WebApplicationBuilder对象,因为创建一个WebApplication对象需要一个Host,Host则必须由HostBuilder创建,所以WebApplicationBuilder对象的作用是提供一个封装好的HostBuilder对象用来构建IHost,
它含有6个属性以及一个构造函数,属性包括IServiceCollection依赖注入容器Services、ConfigureManage配置管理Configure、IWebHostEnvironment托管环境environment、ILoggingBuilder日志记录logging、ConfigureWebHostBuilder类型webhost、ConfigureHostBuilder类型host,需要这6个属性的目的就是用来提供HostBuilder的创建
构造函数的作用是根据传进来的命令行参数来初始化这些属性,首先他会初始化一个_hostBuilder对象,然后创建一个bootstrapBuilder对象用来调用他的拓展方法收集服务和配置,赋值给services和configure属性,接下里根据bootstrapBuilder对象的一些属性,初始化剩余属性,初始化WebHost和Host。

第二句代码是var app = bulider.bulider();
在这句代码之前,我们可以注入自己的一些服务和系统服务,通过调用AddScope()等依赖注入方法或者使用系统提供的服务方法、如AddController(),
这句代码的作用是,根据得到WebApplicationBuilder对象来创建WebApplication,这句代码最重要的就是,把services和configure属性赋值给HostBuilder,然后我们可以看到这个对象继承了4个接口,一个是IHost接口,这就解释了为什么WebApplicationBuilder需要有一个ConfigurHostBuilder对象,还有一个IApplicationBuilder接口,这个接口是构建中间件管道服务的接口,所以我们的中间件可以直接注册在WebApplication的原因,IEndpointRouteBuilder则是默认构造了路由,还有一个异步释放的接口。

第三句代码是app.Run();
在这句代码之前可以注入中间件服务,比如UseAuthorization()之类的,
这句代码的作用是,通过调用WebApplication内部的BuildRequestDelegation()方法把注册的中间件管道作为请求处理器,至此一个WebApplication对象完成
所以根据这几行代码我们不难看出,WebApplication其实就是对Host的再次封装,只是为了我们更加简单的去配置一些我们需要的服务和中间件