ASP.NET Core 承载系统管道概述

Posted AI大胜

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ASP.NET Core 承载系统管道概述相关的知识,希望对你有一定的参考价值。

此文只是从中摘录整理下自己感兴趣的部分,以便备忘和方便查找回顾,详见:

请务必先通读以下官网概述(15min~30min):

ASP.NET Core 基础知识概述 | Microsoft Learn

承载系统

借助 .NET Core 提供的承载系统(Hosting),我们可以将任意一个或多个长时间运行的服务寄宿或者承载于托管进程中。ASP.NET Core 应用仅仅是该承载系统的一种典型的服务类型而已,任何需要在后台长时间运行的操作都可以定义成标准化的服务并利用该系统来承载。

承载系统通过 IHost 接口表示承载服务的宿主,宿主实例由对应的 IHostBuilder 对象来创建。HostBuilder 类型是对 IHostBuilder 接口的默认实现,通过调用该对象的 Build 方法来提供作为宿主的 IHost 对象。在调用 Build 方法之前,可以调用 IHostBuilder 接口的 ConfigureService 方法,注册针对某 IHostedService 接口的服务。针对 IHostedService 服务的注册还可以调用 IServiceCollection 接口的 AddHostedService<THostedService> 扩展方法来完成。

承载服务通过 IHostedService 接口表示,该接口定义的 StartAsync StopAsync 方法可以启动和关闭服务。

当作为宿主的 IHost 对象被启动时,它会利用依赖注入框架激活每个注册的 IHostedService 服务,并通过 StartAsync 方法来启动他们。当服务承载应用程序关闭时,作为服务宿主的 IHost 对象会被关闭,由它承载的每个 IHostedService 服务对象的 StopAsync 方法也随之被调用。

管道

请求处理管道 = 服务器 + 中间件

一个 ASP.NET Core 应用的核心就是由一个服务器和一组有序中间件组成的请求处理管道,服务器只负责监听、接收和分发请求、以及最终完成对请求的响应。所以,针对请求的处理能力和处理方式,由注册的中间件来决定,另外所有中间件针对请求的处理都在通过 HttpContext 对象表示的上下文中进行。

HTTP协议自身的特性决定了任何一个外部应用的工作模式都是监听、接收并处理HTTP请求,并且最终对请求予以响应。HTTP请求处理是管道式设计典型的应用场景:可以根据具体的需求构建一个管道,接收的HTTP请求像水一样流入这个管道,组成这个管道的各个环节依次对其做相应的处理。

HTTP请求处理流程始于对请求的监听与接收,终于对请求的响应。这两项工作均由同一个对象来完成,我们称之为服务器(Server)。ASP.NET Core 请求处理管道必须有一个服务器,他是整个管道的龙头。HTTP请求一旦抵达,服务器会将其标准化,并分发给管道后续的节点,我们将位于服务器之后的节点称为中间件(Middleware)。

服务器在管道中的职责非常明确,负责HTTP请求的监听,接收和最终的响应。具体来说,启动后的服务器会绑定到指定的端口进行请求监听。一旦有请求抵达,它会根据该请求创建代表请求上下文的 HttpContext 对象,并将该上下文分发给注册的中间件进行处理,当中间件管道完成了针对请求的处理之后,服务器会将最终生成的响应回复给客户端。

ASP.NET Core的请求处理管道由一个服务器和一组中间件组成,位于龙头的服务器负责请求的监听、接收、分发和最终的响应,而针对该请求的处理则由后续的中间件来完成。从概念上可以将请求处理管道理解为请求消息和响应消息流通的管道,服务器将接收的请求消息从一端流入管道并由相应的中间件进行处理,生成的响应消息反向流入管道,经过相应中间件处理后,由服务器分发给请求者。但从实现的程度来讲,管道中流通的并不是所谓的请求消息与响应消息。而是一个针对当前请求创建的上下文,这个上下文被抽象成如下这个HttpContext的类型。我们利用它不仅可以获取针对当前的请求的所有信息,还可以直接完成针对当前请求的所有响应工作。

中间件的注册可以通过调用 IWebHostBuilder 接口的Configure 扩展方法来完成,然后通过调用 IApplicationBuilder 接口的 Use 方法将表示中间件的 Func<RequestDelegate,RequestDelegate> 对象添加到当前中间件链条上。

自定义中间件

中间件的两种定义方式:

  • 继承 IMiddleware 接口。实现其 InvokeAsync 方法,完成针对请求的处理。
  • 按照约定定义具有指定方法模式的类。

区别:强类型方式定义的中间件,可以注册为任意生命周期模式的服务。但是按照约定定义的中间件则总是一个Singleton 服务。

承载环境

派生于 IHostEnvironment 接口的 IWebHostEnviroment接口定义了两个属性:WebRootPath WebRootFileProvider

WebRootPath 属性表示用于存放Web资源文件根目录的路径,WebRootFileProvider 属性则返回该路径对应的 IFileProvider 对象。如果我们希望外部可以采用http请求的方式直接访问某个静态文件(如JS、CSS、图片等),只需要将它们放于WebRootPath属性表示的目录之下即可。默认为wwwroot文件夹。

如果没有显示指定环境名称,当前应用就会采用默认的Production 环境名称。


宏观概述

遇到问题,解决方案分为两种,一种是“扬汤止沸”,一种是“釜底抽薪”。

ASP.NET Core是一个全新的Web 开发平台,为我们构建了一个可复用和可定制的请求处理管道,微软在它上面构建了MVC、SingnalR、GRPC、Orleans这样广泛使用的web框架。

ASP.NET Core 框架的请求处理管道由服务器中间件组成,管道利用服务器来监听和处理请求,并完成最终对请求的响应,应用针对请求的处理则体现在有序排列的中间件上。即当宿主程序(Host)启动后,管道被构建出来,作为管道龙头的服务器就开始监听来自客户端的HTTP请求。服务器解决针对HTTP请求的监听接收和最终的响应,具体针对请求的处理则由它递交给后续的中间件来完成。

.NET Core与传统.NET Framework最大的区别是跨平台,即开发出来的程序不再只是仅仅可以运行在Windows平台上,各种Linux和Mac上也都行了。除此之外,.NET Core针对容器也提供了原生的支持。

ASP.NET Core 框架依赖于一些基础框架,其中最重要的是依赖注入框架,它是构建请求处理管道的基石,也是应用的基本编程模式。整个ASP.NET Core框架建立在一个底层的依赖注入框架之上,它使用依赖注入容器提供所需的服务对象。

一个ASP.NET Core应用本质上就是一个用来监听、接收、处理、响应HTTP请求的后台服务。

.NET Framework的结构组成

从.NET Framework的结构组成的角度来讲,.NET Framework由如下图所示的两个层级构成,它们分别是提供运行环境的CLR(Common Language Runtime)和提供API的FCL(Framework Class Library)。

即:.NET Framework = Runtime + FCL

CLR为程序的执行提供一个托管(Managed)的执行环境,它是.NET Framework的执行引擎,为托管程序的执行提供内存分配、垃圾回收、安全控制、异常处理和多线程管理等方面的服务。CLR是.NET Framework的子集,但是两者却具有不同的版本策略。到目前为止,微软仅仅发布了4个版本的CLR,它们分别是1.0、1.1、2.0和4.0,.NET Framework 1.0和1.1分别采用CLR 1.0和1.1,CLR 2.0被.NET Framework 2.0和3.x共享,.NET Framework 4.x下的运行时均为CLR 4.0。

.NET Framework的各个应用模型是相互独立的。在开发某种类型的应用时,我们只需要引用应用模型对应的程序集就可以了,也就是说我们开发一个Windows Forms应用,是不需要去引用System.Web.dll程序集的。

跨平台的.NET Core

对于真正跨平台的.NET Core来说,微软不仅为它设计了针对不同平台被称为CoreCLR的运行时,同时还重新设计了一套被称为CoreFX的BCL。

NET Core目前支持的AppModel有4种(ASP.NET Core、Windows Forms、WPF和UWP),其中ASP.NET Core 提供了全平台的支持,而Windows Forms、WPF和UWP只能在Windows上运行。

除了.NET Framework、.NET Core,.NET还具有另一个重要分支,即Xamarin,它可以为iOS、OS和Android 编写统一的应用。

.NET Standard

.NET Standard为.NET Framework、.NET Core和Xamarin提供了统一的API,那么我们在这组标准API基础上编写的代码自然就能被所有类型的.NET应用复用。

.NET 5

.NET Core 3.1是最后一个版本的.NET Core,今后不会再有.NET Core和.NET Framework之分,未来的.NET是一个统一平台,称为.NET 5/6/7……。所有类型的应用都是在统一的.NET Standard上开发的,所以不会再有移植性问题。


更新于:2023.5.20

一题多解,ASP.NET Core应用启动初始化的N种方案[上篇]

ASP.NET Core应用本质上就是一个由中间件构成的管道,承载系统将应用承载于一个托管进程中运行起来,其核心任务就是将这个管道构建起来。在ASP.NET Core的发展历史上先后出现了三种应用承载的编程方式,而且后一种编程模式都提供了针对之前编程模式的全部或者部分兼容,这就导致了一种现象:相同的功能具有N种实现方式。对这个发展历程不是特别了解的读者会有很多疑问?为什么这么多不同的编程模式都在做同一件事?它们之间的有什么差别之处?为什么有的API在最新的Minimal API又不能用了呢?[本文部分内容来源于《ASP.NET Core 6框架揭秘》第15章]

目录
一、应用承载过程中需要哪些初始化工作?
二、第一代应用承载模型
     基本编程模式
     利用环境变量和命令行参数
    承载环境设置方法
    使用Startup类型
三、第二代应用承载模型
     基本编程模式
     承载环境设置方法
     针对IWebHostBuilder的适配
    Startup构造函数注入的限制

一、应用承载过程中需要哪些初始化工作?

我们所谓的应用承载(Hosting)本就是将一个ASP.NET Core应用在一个具体的进程(Self-Host进程、IIS工作进程或者Windows Service进程等)中被启动的过程,在这个过程中需要利用提供的API完成一些必要的初始化工作。由于ASP.NET Core应用本质上就是一个由中间件构成的管道,所有整个初始化过程的目的就是为了构建这一中间件管道,毫不夸张地说,构建的中间件管道就是“应用”本身,所以“中间件注册”是最为核心的初始化工作。由于依赖注入的广泛应用,中间件的功能基本都依赖于注入的服务来完成,所以将依赖服务注册到依赖注入框架是另一项核心的初始化工作。

和任何类型的应用一样,ASP.NET Core同样需要通过配置来动态改变其运行时行为,所以针对配置的设置也是并不可少的。一个ASP.NET Core应用的配置分为两类,一种是用在中间件管道构建过程中,也就是应用承载过程中,我们将其称为“承载配置(Hosting Configuration)”。另一类配置则被用来控制中间件管道处理请求的行为,正如上面所说,中间件管道就是应用本身,所以这类配置被称为应用配置(App Configuration)。承载配置中有一个重要的组成部分,那就是描述当前的承载环境(Hosting Environment),比如应用的标识、部署环境的名称、存放内容文件和Web资源的目录等。承载配置最终会合并到应用配置中。

综上所示,ASP.NET Core应用承载的编程模型主要完成如下几种初始化工作,这些工作都具有N种实现方法。在接下来的内容中,我们将逐个介绍在三种不同的应用承载方式中,这些功能都有哪些实现方式。

  • 中间件注册

  • 服务注册

  • 承载配置的设置

  • 应用配置的设置

  • 承载环境的设置

二、第一代应用承载模型

ASP.NET Core 1.X/2.X采用的承载模型以如下图所示的IWebHostBuilder和IWebHost为核心。IWebHost对象代表承载Web应用的宿主(Host),管道随着IWebHost对象的启动被构建出来。IWebHostBuilder对象作为宿主对象的构建者,我们针对管道构建的设置都应用在它上面。

基本编程模式

现在我们将针对上述5种初始化设置放在一个简单的演示实例中。该演示实例会注册如下这个FoobarMiddleware中间件,后者利用注入的IHandler服务完成请求的处理工作。作为IHandler接口的默认实现类型,Handler利用构造函数注入的IOptions<FoobarbazOptions>对象得到配置选项FoobarbazOptions,并将其内容作为请求的响应。

public class FoobarMiddleware

    private readonly RequestDelegate _next;
    public FoobarMiddleware(RequestDelegate _)  
    public Task InvokeAsync(HttpContext httpContext, IHandler handler) 
    => handler.InvokeAsync(httpContext);


public interface IHandler

    Task InvokeAsync(HttpContext httpContext);


public class Handler : IHandler

    private readonly FoobarbazOptions _options;
    private readonly IWebHostEnvironment _environment;

    public Handler(
        IOptions<FoobarbazOptions> optionsAccessor, 
        IWebHostEnvironment environment)
    
        _options = optionsAccessor.Value;
        _environment = environment;
    

    public Task InvokeAsync(HttpContext httpContext)
    
        var payload = @$"
Environment.ApplicationName: _environment.ApplicationName
Environment.EnvironmentName: _environment.EnvironmentName
Environment.ContentRootPath: _environment.ContentRootPath
Environment.WebRootPath: _environment.WebRootPath
Foo: _options.Foo
Bar: _options.Bar
Baz: _options.Baz
";
        return httpContext.Response.WriteAsync(payload);
    


public class FoobarbazOptions

    public string Foo  get; set;  = default!;
    public string Bar  get; set;  = default!;
    public string Baz  get; set;  = default!;

我们会利用与当前“承载环境”对应配置来绑定配置选项FoobarbazOptions,后者的三个属性分别来源于三个独立的配置文件。其中settings.json被所有环境共享,settings.dev.json针对名为“dev”的开发环境。我们为承载环境提供更高的要求,在环境基础上进步划分子环境,settings.dev.dev1.json针对的就是dev下的子环境dev1。针对子环境的设置需要利用上述的承载配置来提供。

如下所示的就是上述三个配置文件的内容。如果当前环境和子环境分别为dev和dev1,那么配置选项FoobarbazOptions的内容将来源于这三个配置文件。细心的朋友可能还注意到了:我们并没有放在默认的根目录下,而是放在创建的resources目录下,这是因为我们需要利用针对承载环境的设置改变ASP.NET Core应用存放内容文件和Web资源文件的根目录。

settings.json

  "Foo": "123"


settings.dev.json

  "Bar": "abc"


settings.dev.dev1.json

  "Baz": "xyz"

如下的应用承载程序涵盖了上述的5种初始化操作。中间件的注册通过调用IWebHostBuilder的Configure方法来完成,该方法的参数类型为Action<IApplicationBuilder>,中间件就是通过调用UseMiddleware<TMiddleware>方法注册到IApplicationBuilder对象上。IWebHostBuilder并未对承载配置定义专门的方法,但是我们可以利用UseSettings方法以键值对的形式对其进行设置,这里我们采用这种方式完成了针对“环境”、“内容文件根目录”、“Web资源文件根目录”和“子环境”的设置,前三个是“承载环境”的三个重要属性。承载配置最终会体现到表示承载上下文的WebHostBuilderContext对象上。

using App;
new WebHostBuilder()
    .UseKestrel()
    .UseSetting(WebHostDefaults.EnvironmentKey,"dev")
    .UseSetting(WebHostDefaults.ContentRootKey , 
        Path.Combine(Directory.GetCurrentDirectory(), "resources"))
    .UseSetting(WebHostDefaults.WebRootKey, 
        Path.Combine(Directory.GetCurrentDirectory(), "resources", "web"))
    .UseSetting("SubEnvironment", "dev1")
    .ConfigureAppConfiguration((context, configBuilder) => configBuilder
        .AddJsonFile(
            path: "settings.json", 
            optional: false)
        .AddJsonFile(
            path: $"settings.context.HostingEnvironment.EnvironmentName.json", 
            optional: true)
        .AddJsonFile(
            path: $"settings.context.HostingEnvironment.EnvironmentName.context.Configuration["SubEnvironment"].json", 
            optional: true))
    .ConfigureServices((context, services) => services
        .AddSingleton<IHandler, Handler>()
        .Configure<FoobarbazOptions>(context.Configuration))
    .Configure(app => app.UseMiddleware<FoobarMiddleware>())
    .Build()
    .Run();

依赖服务利用IWebHostBuilder的ConfigureServices方法进行注册,该方法的参数类型为Action<WebHostBuilderContext, IServiceCollection>,意味着我们可以针对之前提供的承载配置(比如承载环境)进行针对性的服务注册。在这里我们不仅注册了依赖服务Handler,还利用当前配置对配置选项FoobarbazOptions实施了绑定。应用配置通过专门的方法ConfigureAppConfiguration进行设置,该方法的参数类型为Action<WebHostBuilderContext, IConfigurationBuilder>,意味着承载配置依然可以利用WebHostBuilderContext上下文获取到,这里我们这是利用它得到对当前环境匹配的三个配置文件。程序启动后,请求可以得到如下的响应内容。

利用环境变量和命令行参数

由于ASP.NET Core应用在启动时会使用前缀为“ASPNETCORE_”的环境变量作为承载配置,所以上述利用UseSettings方法针对承载配置的设置都可以按照如下的方式利用环境变量代替。

Environment.SetEnvironmentVariable(
    "ASPNETCORE_ENVIRONMENT", "dev");
Environment.SetEnvironmentVariable(
    "ASPNETCORE_SUBENVIRONMENT", "dev1");
Environment.SetEnvironmentVariable(
    "ASPNETCORE_CONTENTROOT", 
    Path.Combine(Directory.GetCurrentDirectory(), "resources"));
Environment.SetEnvironmentVariable(
    "ASPNETCORE_WEBROOT", 
    Path.Combine(Directory.GetCurrentDirectory(), "resources", "web"));

WebHost.CreateDefaultBuilder(args)
    .ConfigureAppConfiguration((context, configBuilder) => configBuilder
        .AddJsonFile(
            path: "settings.json", 
            optional: false)
        .AddJsonFile(
            path: $"settings.context.HostingEnvironment.EnvironmentName.json", 
            optional: true)
        .AddJsonFile(
            path: $"settings.context.HostingEnvironment.EnvironmentName.context.Configuration["SubEnvironment"].json", 
            optional: true))
    .ConfigureServices((context, services) => services
        .AddSingleton<IHandler, Handler>()
        .Configure<FoobarbazOptions>(context.Configuration))
    .Configure(app => app.UseMiddleware<FoobarMiddleware>())
    .Build()
    .Run();

上面的代码片段并没有直接创建WebHostBuilder对象,而是调用WebHost的静态方法CreateDefaultBuilder方法创建了一个具有默认配置的IWebHostBuilder对象。由于该方法传入了命令行参数args,它会将命令行参数作为承载配置源之一,所以程序中四个针对承载配置选项也可以利用命令行参数来完成。

承载环境设置方法

其实承载环境(环境名称、内容文件根目录和Web资源文件根目录)具有专门的方法,所以最方便的还是直接按照如下的方式调用这些方法对它们进行设置。对于我们演示的实例来说,针对环境名称、内容文件和Web资源文件根目录的设置可以直接调用IWebHostBuilder的UseEnvironment、UseContentRoot和UseWebRoot扩展方法来完成。

WebHost.CreateDefaultBuilder(args)
    .UseEnvironment("dev")
    .UseContentRoot(Path.Combine(Directory.GetCurrentDirectory(), "resources"))
    .UseWebRoot(Path.Combine(Directory.GetCurrentDirectory(), "resources", "web"))
    .UseSetting("SubEnvironment", "dev1")
    .ConfigureAppConfiguration((context, configBuilder) => configBuilder
        .AddJsonFile(path: "settings.json", optional: false)
        .AddJsonFile(path: $"settings.context.HostingEnvironment.EnvironmentName.json", optional: true)
        .AddJsonFile(path: $"settings.context.HostingEnvironment.EnvironmentName.context.Configuration["SubEnvironment"].json", optional: true))
    .ConfigureServices((context, services) => services
        .AddSingleton<IHandler, Handler>()
        .Configure<FoobarbazOptions>(context.Configuration))
    .Configure(app => app.UseMiddleware<FoobarMiddleware>())
    .Build()
    .Run();

使用Startup类型

为了不让应用承载程序代码显得过于臃肿,我们一般都会将服务注册和中间件注册移到按照约定定义的Startup类型中。如下面的代码片段所示,中间件和服务注册分别实现在Startup类型的ConfigureServices和Configure方法中,我们直接在构造函数中注入IConfiguration对象得到承载配置对象。值得一提,对于第一代应用承载方式,我们可以在Startup类型的构造函数中注入通过调用IWebHostBuilder的ConfigureServices方法注册的任何服务(包括ASP.NET Core内部通过调用这个方法注册的服务,比如本例的IConfiguration对象)。Startup类型只需要调用IWebHostBuilder的UseStartup<TStartup>扩展方法进行注册即可。

WebHost.CreateDefaultBuilder(args)
    .UseEnvironment("dev")
    .UseContentRoot(Path.Combine(Directory.GetCurrentDirectory(), "resources"))
    .UseWebRoot(Path.Combine(Directory.GetCurrentDirectory(), "resources", "web"))
    .UseSetting("SubEnvironment", "dev1")
    .ConfigureAppConfiguration((context, configBuilder) => configBuilder
        .AddJsonFile(path: "settings.json", optional: false)
        .AddJsonFile(path: $"settings.context.HostingEnvironment.EnvironmentName.json", optional: true)
        .AddJsonFile(path: $"settings.context.HostingEnvironment.EnvironmentName.context.Configuration["SubEnvironment"].json", optional: true))
    .UseStartup<Startup>()
    .Build()
    .Run();

public class Startup

    public IConfiguration Configuration  get; 
    public Startup(IConfiguration configuration) 
    => Configuration = configuration;
    public void ConfigureServices(IServiceCollection services) 
    => services
        .AddSingleton<IHandler, Handler>()
        .Configure<FoobarbazOptions>(Configuration);
    public void Configure(IApplicationBuilder app) 
    => app.UseMiddleware<FoobarMiddleware>();

三、第二代应用承载模型

除了承载Web应用,我们还有很多针对后台服务(比如很多批处理任务)的承载需求,为此微软推出了以IHostBuilder/IHost为核心的服务承载系统。Web应用本身实际上就是一个长时间运行的后台服务,我们完全可以将应用定义成一个IHostedService服务,该类型就是下图所示的GenericWebHostService。如果将上面介绍的称为第一代应用承载模式的话,这就是第二代承载模式。

基本编程模式

和所有的Builder模式一样,绝大部分API都落在作为构建者的IHostBuilder接口上,服务注册、承载配置、应用配置都具有对应的方法。由于中间件隶属于GenericWebHostService这一单一的承载服务,所以只能记住与IWebHostBuilder。如果采用第二代应用承载模型,上面演示的程序可以改写成如下的形式。

Host.CreateDefaultBuilder()
    .ConfigureHostConfiguration(config => config.AddInMemoryCollection(new Dictionary<string, string> 
        [WebHostDefaults.EnvironmentKey] = "dev",
        [WebHostDefaults.ContentRootKey] = Path.Combine(Directory.GetCurrentDirectory(), "resources"),
        [WebHostDefaults.WebRootKey] = Path.Combine(Directory.GetCurrentDirectory(), "resources","web"),
        ["SubEnvironment"] = "dev1"
    ))
    .ConfigureAppConfiguration((context, configBuilder) => configBuilder
        .AddJsonFile(path: "settings.json", optional: false)
        .AddJsonFile(path: $"settings.context.HostingEnvironment.EnvironmentName.json", optional: true)
        .AddJsonFile(path: $"settings.context.HostingEnvironment.EnvironmentName.context.Configuration["SubEnvironment"].json", optional: true))
    .ConfigureServices((context,services) => services
        .AddSingleton<IHandler, Handler>()
        .Configure<FoobarbazOptions>(context.Configuration))
    .ConfigureWebHost(webHostBuilder => webHostBuilder.Configure(app=>app
        .UseMiddleware<FoobarMiddleware>()))
    .Build()
    .Run();

如上面的代码片段所示,我们通过调用Host的静态方法CreateDefaultBuilder方法创建一个具有默认配置的IHostBuidler对象。IHostBuilder为承载配置的设置提供了独立的ConfigureHostConfiguration方法,该方法的参数类型为Action<IConfigurationBuilder>,我们演示的例子利用这个方法注册了一个基于内存字典的配置源,承载环境(环境名称、内容文件和Web资源文件根目录)和子环境名称在这里进行了设置。针对应用配置的设置通过ConfigureAppConfiguration方法来完成,该方法的参数类型为Action<HostBuilderContext, IConfigurationBuilder>,代表承载上下文的HostBuilderContext可以得到预先设定的承载环境和承载配置,我们的例子利用到定位与当前环境相匹配的配置文件。

IHostBuilder同样定义了ConfigureServices方法,该方法的参数类型为Action<HostBuilderContext, IServiceCollection>,意味着服务依然可以针对承载环境和承载配置进行注册。由于中间件的注册依然落在IWebHostBuilder上,所以IHostBuilder提供了ConfigureWebHost/ConfigureWebHostDefaults这两个扩展方法予以适配,它们具有一个类型为Action<IWebHostBuilder>的参数。

承载环境设置方法

和IWebHostBuilder一样,IHostBuidler同样提供了用来直接设置承载环境的方法。对于我们演示的实例来说,针对环境名称、内容文件和Web资源文件根目录的设置可以直接调用IHostBuidler的UseEnvironment、UseContentRoot和UseWebRoot扩展方法来完成。由于Web资源文件并未“服务承载”的范畴,所以针对Web资源文件根目录的设置还得采用直接设置承载配置的方式(或者调用IWebHostBuilder的UseWebRoot扩展方法)。

Host.CreateDefaultBuilder()
    .UseEnvironment("dev")
    .UseContentRoot(Path.Combine(Directory.GetCurrentDirectory(), "resources"))
    .ConfigureHostConfiguration(config => config.AddInMemoryCollection(new Dictionary<string, string> 
        [WebHostDefaults.WebRootKey] = Path.Combine(Directory.GetCurrentDirectory(), "resources","web"),
        ["SubEnvironment"] = "dev1"
    ))
    .ConfigureAppConfiguration((context, configBuilder) => configBuilder
        .AddJsonFile(path: "settings.json", optional: false)
        .AddJsonFile(path: $"settings.context.HostingEnvironment.EnvironmentName.json", optional: true)
        .AddJsonFile(path: $"settings.context.HostingEnvironment.EnvironmentName.context.Configuration["SubEnvironment"].json", optional: true))
    .ConfigureServices((context,services) => services
        .AddSingleton<IHandler, Handler>()
        .Configure<FoobarbazOptions>(context.Configuration))
    .ConfigureWebHost(webHostBuilder => webHostBuilder.Configure(app=>app
        .UseMiddleware<FoobarMiddleware>()))
    .Build()
    .Run();

针对IWebHostBuilder的适配

由于IHostBuilder利用扩展方法ConfigureWebHost/ConfigureWebHostDefaults提供了针对IWebHostBuilder的适配,意味着前面采用第一代应用承载方法编写的代码可以直接移植过来。如下面的代码片段所示,静态方法ConfigureWebHost完全依然利用IWebHostBuilder完成所有的初始化工作,我们只需要将指向该方法的Action<IWebHostBuilder>委托传入IHostBuilder的ConfigureWebHostDefaults扩展方法就可以了。

Host.CreateDefaultBuilder(args)
    .ConfigureWebHostDefaults(ConfigureWebHost)
    .Build()
    .Run();

static void ConfigureWebHost(IWebHostBuilder webHostBuilder)

    webHostBuilder.UseEnvironment("dev")
        .UseContentRoot(Path.Combine(Directory.GetCurrentDirectory(), "resources"))
        .UseWebRoot(Path.Combine(Directory.GetCurrentDirectory(), "resources", "web"))
        .UseSetting("SubEnvironment", "dev1")
        .ConfigureAppConfiguration((context, configBuilder) => configBuilder
            .AddJsonFile(path: "settings.json", optional: false)
            .AddJsonFile(path: $"settings.context.HostingEnvironment.EnvironmentName.json", optional: true)
            .AddJsonFile(path: $"settings.context.HostingEnvironment.EnvironmentName.context.Configuration["SubEnvironment"].json", optional: true))
        .UseStartup<Startup>();


public class Startup

    public IConfiguration Configuration  get; 
    public Startup(IConfiguration configuration) 
    => Configuration = configuration;
    public void ConfigureServices(IServiceCollection services) 
    => services
        .AddSingleton<IHandler, Handler>()
        .Configure<FoobarbazOptions>(Configuration);
    public void Configure(IApplicationBuilder app) 
    => app.UseMiddleware<FoobarMiddleware>();

Startup构造函数注入的限制

第二代应用承载模型利用ConfigureWebHost/ConfigureWebHostDefaults扩展方法对之前定义在IWebHostBuilder上的API(绝大部分是扩展方法)提供了100%的支持(除了Build方法),但是针对Startup构造函数中注入的服务则不再那么自由。如果采用基于IWebHostBuilder/IWebHost的应用承载方式,通过调用IWebHostBuilder的ConfigureServices方法注册的服务都可以注入Startup的构造函数中,如果采用基于IHostBuilder/IHost的应用承载方式,只有与“承载配置(承载环境属于承载配置的一部分)”相关的如下三个服务能够注入到Startup的构造函数中。

  • IHostingEnvironment

  • IWebHostEnvironment

  • IHostEnvironment

  • IConfiguration

对于如下这段代码,虽然注入Startup构造函数的Foobar同时通过调用IHostBuilder和IWebHostBuilder的ConfigureServices方法中进行了注册,但是在创建Startup实例的时候依然会抛出异常。

Host.CreateDefaultBuilder(args)
    .ConfigureServices(sevices=>sevices.AddSingleton<Foobar>())
    .ConfigureWebHostDefaults(ConfigureWebHost)
    .Build()
    .Run();

static void ConfigureWebHost(IWebHostBuilder webHostBuilder)

    webHostBuilder.UseEnvironment("dev")
        .ConfigureServices(sevices => sevices.AddSingleton<Foobar>())
        .UseContentRoot(Path.Combine(Directory.GetCurrentDirectory(), "resources"))
        .UseWebRoot(Path.Combine(Directory.GetCurrentDirectory(), "resources", "web"))
        .UseSetting("SubEnvironment", "dev1")
        .ConfigureAppConfiguration((context, configBuilder) => configBuilder
            .AddJsonFile(path: "settings.json", optional: false)
            .AddJsonFile(path: $"settings.context.HostingEnvironment.EnvironmentName.json", optional: true)
            .AddJsonFile(path: $"settings.context.HostingEnvironment.EnvironmentName.context.Configuration["SubEnvironment"].json", optional: true))
        .UseStartup<Startup>();


public class Startup

    public IConfiguration Configuration  get; 
    public Startup(IConfiguration configuration,Foobar foobar) 
    => Configuration = configuration;
    public void ConfigureServices(IServiceCollection services) 
    => services
        .AddSingleton<IHandler, Handler>()
        .Configure<FoobarbazOptions>(Configuration);
    public void Configure(IApplicationBuilder app) 
    => app.UseMiddleware<FoobarMiddleware>();


public class Foobar
 

综上所述,最初版本的ASP.NET Core由于只考虑到Web应用自身的承载,所以设计出了基于IWebHostBuilder/IWebHost模型。后来产生了基于后台服务承载的需求,所以推出了基于IHostBuilder/IHost的服务承载模型,原本的Web应用作为一个“后台服务(GenericWebHostService.)”进行承载。由于之前很多API都落在IWebHostBuilder(主要无数的扩展方法),出于兼容性的需求,一个名为GenericWebHostBuilder的实现类型被定义出来,它将针对IWebHostBuilder的方法调用转移到IHostBuilder/IHost的服务承载模型中。

.NET 6在IHostBuilder/IHost服务承载模型基础上推出了更加简洁的Minimal API,此时又面临相同的“抉择”。这次它不仅需要兼容IWebHostBuilder,还得兼容IHostBuilder,再加上Minimal API自身提供的API,所以“一题多解”的现象就更多了。如果你对ASP.NET Core的历史不甚了解,将会感到非常困惑。令你们更加感到困惑的时,此时定义在IWebHostBuilder和IHostBuilder的API并非全部可用,本文的下篇将为你一一解惑。

以上是关于ASP.NET Core 承载系统管道概述的主要内容,如果未能解决你的问题,请参考以下文章

ASP.NET Core 6框架揭秘实例演示[21]:如何承载你的后台服务

ASP.NET Core 6框架揭秘实例演示[22]:如何承载你的后台服务[补充]

《ASP.NET Core 6框架揭秘》实例演示[21]:如何承载你的后台服务

《ASP.NET Core 6框架揭秘》实例演示[22]:如何承载你的后台服务[补充]

ASP.NET Core 2.0 : 八.图说管道

asp.net core系列 68 Filter管道过滤器