写点什么

关于 ASP.NET Core 中的管道和中间件

作者:雄鹿 @
  • 2024-03-30
    广东
  • 本文字数:4060 字

    阅读完需:约 13 分钟

关于 ASP.NET Core 中的管道和中间件

本文章介绍在 ASP.NET Core 中的管道和中间件

先决条件

.NET SDK

介绍

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 提供了两种使用具体类型定义中间件的方法。一种是基于接口实现的中间件,一种是基于约定的中间件

基于接口实现

强类型中间件需要实现 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 方法注册中间件

基于约定定义

基于约定的中间件需要有一个有效的公共构造函数,这个函数必须包含 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 在创建中间件对象并利用它们构建整个请求处理管道的时候,所有的服务已经注册完成了,所以任何一个注册的服务都可以注册到中间件构造函数中

发布于: 刚刚阅读数: 7
用户头像

雄鹿 @

关注

心像开满花的树。 2019-01-04 加入

一名全栈开发工程师,热爱编程、徒步、登山和摄影,对新技术充满好奇心,专注于使用ASP.NET Core和Angular进行Web应用的开发。

评论

发布
暂无评论
关于 ASP.NET Core 中的管道和中间件_ASP.NET Core_雄鹿 @_InfoQ写作社区