前言
当前文章用的 .NET Core SDK 3.1。
由于.NET Core 2.2以后,ASP.NET Core 的某些包不能从NuGet独立安装,所以如果是控制台项目需要修改.csproj文件的 <Project Sdk="Microsoft.NET.Sdk">
为 <Project Sdk="Microsoft.NET.Sdk.Web">
。
ASP.NET Core 应用本身就是一个长时间运行的托管服务,这个托管服务的名称是 GenericWebHostService
,ASP.NET Core 的很多特性都是由来实现的,路由,会话,缓存,认证授权,甚至可以通过管道定制创建属于自己的Web框架。管道是 ASP.NET Core 应用的核心,任何一个 ASP.NET Core 的框架都离不开 管道。
ASP.NET Core 和 ASP.NET 的管道一样都是责任链模式
,不同的是 ASP.NET Core 的管道由开发者自己组装,而 ASP.NET 的管道已经在框架中硬编码,开发者只能对责任链上的事件进行编码。
一、一个简单的 WebHost
在 ASP.NET Core 的默认模板中,HostBuilder 的初始化是通过 Host.CreateDefaultBuilder
方法初始化,然后通过 IHostBuilder
的 ConfigureWebHostDefaults
初始化 WebHost
的服务。
其中,Host.CreateDefaultBuilder
主要做得事情是加载环境变量配置,加载默认的配置文件配置,配置日志组件等等。
而 ConfigureWebHostDefaults
主要做得就是注册一个名为 GenericWebHostService
的托管服务,然后对这个托管服务进行相关配置。
IHostBuilder builder = new HostBuilder()
.ConfigureWebHost(builder => builder
.UseKestrel()
.Configure(app =>
app.Run(httpContext =>
httpContext.Response.WriteAsync("Hello world!")
)
)
);
IHost host = builder.Build();
host.Run();
在上面的代码中,UseKestrel
指定Kestrel作为 WebHost 要使用的服务器,要使用IIS需要 UseIIS
。
然后在管道中加了一个返回Hello world
的中间件。
当 WebHost 启动以后,构建的处理管道才会被真正构建出来,这个管道被绑定到 Kestrel 默认的端口并开始监听请求。
当 Http 请求一旦到达,这个服务器就会将其标准化为一个HttpContext对象,然后发送给管道
而实际处理请求的是配置在管道中的各个组件,这个组件我们称为管道中间件,每个组件都有各自的功能。
比如专门实现路由的中间件,还有实现用户认证和授权的中间件,
所谓的配置管道就是具体的需求,选择对应的中间件来构建最终的管道
而组成请求管道的中间件是一个委托
public delegate Task RequestDelegate(HttpContext context);
请求处理管道其实是由一个服务器加一组中间件所构成的,服务器负责请求的监听、接收、分发和最终的响应 。
二、中间件的注册与执行顺序
中间件的注册可以通过 Configure 中的 IApplicationBuilder 进行注册,通过 Use 方法直接将 MiddleWare 添加到当前的管道中,管道可以理解为委托组成的一条责任链,Use 方法接受的参数是 Func<RequestDelegate, RequestDelegate>,入参 next 表示管道中的下一个中间件,如果不在请求管道中调用 next 方法,即管道短路不再将请求继续往下传递,而是结束请求开始响应。管道短路可以避免不必要的工作。
static RequestDelegate Middleware(RequestDelegate next)
{
static async Task App(HttpContext context) =>
await context.Response.WriteAsync("Middleware 2 Begin.");
return App;
}
var hostBuilder = Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(builder =>
builder.Configure(app => app
.Use(next =>
{
async Task App(HttpContext context)
{
await context.Response.WriteAsync("Middleware 1 Begin.");
await next(context);
await context.Response.WriteAsync("Middleware 1 End.");
}
return App;
})
.Use(Middleware)
)
);
var host = hostBuilder.Build();
host.Run();
按照上面示例我们可以看到浏览器输出的内容就是我们中间件注册的顺序来依次处理响应。
三、中间件的定义
我们可以使用委托 Lamaba表达式 来定义中间件, 但是我们在大部分情况下,我们可以将自定义中间件定义为一个具体的类型,ASP.NET Core 提供了两种使用具体类型定义中间件的方法。一种是基于接口实现的中间件,一种是基于约定的中间件。
1. 基于接口实现
强类型中间件需要实现 IMiddleware 接口,该接口定义了 InvokeAsync 的方法,请求的处理代码就是写在该方法中,该方法第一个参数是 HttpContext,第二个参数代表着后续中间件组成的管道委托。
如果 HttpContext 还需要后续中间件进行处理的话,只需要调用 next 委托就可以了,如果不调用就会发生管道短路。
public class HelloMiddleware : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next) =>
await context.Response.WriteAsync("Middleware.");
}
static void Main(string[] args) =>
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(builder => builder
.ConfigureServices(collection => collection.AddSingleton<HelloMiddleware>())
.Configure(app => app
.UseMiddleware<HelloMiddleware>()
)
).Build()
.Run();
由于中间件是采用依赖注入的方式来提供的,所以我们还要预先进行服务注册,然后才能通过 IApplicationBuilder.UseMiddleware
方法注册中间件。
2. 基于约定定义
基于约定的中间件需要有一个有效的公共构造函数,这个函数必须包含 RequestDelegate 类型(建议第一个参数写 RequestDelegate 类型),通过依赖注入方式提供的和自定义的参数可以混编。
里面必须要 InvokeAsync 或者 Invoke 的方法,该方法接收类型为 HttpContext 的一个参数
自定义的参数通过 UseMiddleware
方法传入,由于依赖注入方式提供的参数和自定义的参数可以混编,自定义的参数在这里也可以随意摆放,但是相同的类型比如自定义了两个bool类型的参数,在这里将会是第一个传入的bool类型的值将会赋值给在构造函数中遇到的第一个bool类型参数。
public class Middleware
{
private readonly RequestDelegate _next;
private readonly ILogger<Middleware> _logger;
private readonly string _content;
private readonly IConfiguration _configuration;
private readonly bool _isToNext;
public Middleware(
bool isTest,
bool isToNext,
string content,
RequestDelegate next,
ILogger<Middleware> logger,
IConfiguration configuration
)
{
_next = next;
_content = content;
_logger = logger;
_configuration = configuration;
_isToNext = isToNext;
}
public async Task InvokeAsync(HttpContext httpContext)
{
await httpContext.Response.WriteAsync($"Hello {_content}!\r\n");
if (_isToNext) {
await _next(httpContext); }
}
}
static void Main(string[] args) =>
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(builder => builder
.Configure(app => app
.UseMiddleware<Middleware>(true, "World", false)
.UseMiddleware<Middleware>("Money", false, true)
)
).Build()
.Run();
基于约定的中间件的中间件不用手动注册,框架会自动以单例生命周期进行服务注册,还可以给中间件传参。
由于基于约定的中间件的生命周期被自动注册为单例模式,我们不应该在他的构造函数中去注入生命周期为作用域或者瞬时的服务,因为永远不会被释放掉。
四、注册服务和中间件
在上文第三节基于接口实现中,使用了 IWebHostBuilder.ConfigureServices
方法注册服务,用 IWebHostBuilder.Configure
方法注册中间件,还可以使用约定的形式,用 IWebHostBuilder.UseStartup
来注册服务和注册中间件。
public class Startup
{
// 无参数或只接受一个IServiceCollection类型的参数
public void ConfigureServices(IServiceCollection services)
{
foreach (var service in services)
{
Console.WriteLine($"{service.Lifetime,-10} {service.ServiceType.Name}");
}
}
public void Configure(IApplicationBuilder app) {
}
}
static void Main(string[] args) =>
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(builder => builder
.UseStartup<Startup>()
).Build()
.Run();
服务注册方法 Startup.ConfigureServices
并不是在任何情况下都需要,可能应用不依赖于任何服务,该方法为可选。
由于 Startup.ConfigureServices
方法调用是在整个服务注册的最后阶段,可以通过 IServiceCollection 去获取之前注册的所有服务。
在 IHostBuilder.ConfigureServices
和 IWebHostBuilder.ConfigureServices
中注册的服务,都无法在 Startup
的构造函数中注入,因为这时这些服务还没注册到 IServiceCollection(服务注册对象)中,只能在 Startup 中注入框架的公共服务。
而 Startup.Configure
方法调用的时间比较晚,所以任意方式注册的服务都可以注册到这个方法中。
中间件中也可以注册服务,由于ASP.NET Core 在创建中间件对象并利用它们构建整个请求处理管道的时候,所有的服务已经注册完成了,所以任何一个注册的服务都可以注册到中间件构造函数中。