ASP.NET Core应用基本编程模式[1]:管道式的请求处理

IHostBuilder接口上定义了很多方法(其中很多是扩展方法),这些方法的目的主要包括以下两点:第一,为创建的IHost对象及承载的服务在依赖注入框架中注册相应的服务;第二,为服务承载和应用提供相应的配置。其实IWebHostBuilder接口同样定义了一系列方法,除了这里涉及的两点,支撑ASP.NET Core应用的中间件也是由IWebHostBuilder注册的。

即使采用基于IHostBuilder/IHost的承载系统,我们依然会使用IWebHostBuilder接口。虽然我们不再使用IWebHostBuilder的宿主构建功能,但是定义在IWebHostBuilder上的其他API都是可以使用的。具体来说,可以调用定义在IHostBuilder接口和IWebHostBuilder接口的方法(大部分为扩展方法)来注册依赖服务与初始化配置系统,两者最终会合并在一起。利用IWebHostBuilder接口注册的中间件会提供给GenericWebHostService,用于构建ASP.NET Core请求处理管道。

在基于IHostBuilder/IHost的承载系统中复用IWebHostBuilder的目的是通过如下所示的ConfigureWebHost扩展方法达成的,GenericWebHostService服务也是在这个方法中被注册的。ConfigureWebHostDefaults扩展方法则会在此基础上做一些默认设置(如KestrelServer),后续章节的实例演示基本上会使用这个方法。

publicstaticclass GenericHostWebHostBuilderExtensions

{

    publicstaticIHostBuilder ConfigureWebHost(thisIHostBuilder builder, Action configure);

}publicstaticclass GenericHostBuilderExtensions

{

    publicstaticIHostBuilder ConfigureWebHostDefaults(thisIHostBuilder builder, Action configure);

}

对IWebHostBuilder接口的复用导致很多功能都具有两种编程方式,虽然这样可以最大限度地复用和兼容定义在IWebHostBuilder接口上众多的应用编程接口,但笔者并不喜欢这样略显混乱的编程模式,这一点在下一个版本中也许会得到改变。

二、请求处理管道

下面创建一个最简单的Hello World程序。这个程序由如下所示的几行代码组成。运行这个程序之后,一个名为KestrelServer的服务器将会启动并绑定到本机上的5000端口进行请求监听。针对所有接收到的请求,我们都会采用“Hello World”字符串作为响应的主体内容。

class Program

{

    staticvoid Main()

    {

        Host.CreateDefaultBuilder()

            .ConfigureWebHost(builder => builder.Configure(app => app.Run(context => context.Response.WriteAsync("Hello World"))))

            .Build()

            .Run();

    }

}

从如上所示的代码片段可以看出,我们利用《服务承载系统》介绍的承载系统来承载一个ASP.NET Core应用。在调用Host类型的静态方法CreateDefaultBuilder创建了一个IHostBuilder对象之后,我们调用它的ConfigureWebHost方法对ASP.NET Core应用的请求处理管道进行定制。HTTP请求处理流程始于对请求的监听与接收,终于对请求的响应,这两项工作均由同一个对象来完成,我们称之为服务器(Server)。ASP.NET Core请求处理管道必须有一个服务器,它是整个管道的“龙头”。在演示程序中,我们调用IWebHostBuilder接口的UseKestrel扩展方法为后续构建的管道注册了一个名为KestrelServer的服务器。

当承载服务GenericWebHostService被启动之后,定制的请求处理管道会被构建出来,管道的服务器随后会绑定到一个预设的端口(如KestrelServer默认采用5000作为监听端口)开始监听请求。HTTP请求一旦抵达,服务器会将其标准化,并分发给管道后续的节点,我们将位于服务器之后的节点称为中间件(Middleware)。

每个中间件都具有各自独立的功能,如专门实现路由功能的中间件、专门实施用户认证和授权的中间件。所谓的管道定制主要体现在根据具体需求选择对应的中间件来构建最终的管道。在演示程序中,我们调用IWebHostBuilder接口的Configure方法注册了一个中间件,用于响应“Hello World”字符串。具体来说,这个用来注册中间件的Configure方法具有一个类型为Action<IApplicationBuilder>的参数,我们提供的中间件就注册到提供的IApplicationBuilder对象上。由服务器和中间件组成的请求处理管道如下图所示。

建立在ASP.NET Core之上的应用基本上是根据某个框架开发的。一般来说,开发框架本身就是通过某一个或者多个中间件构建起来的。以ASP.NET Core MVC开发框架为例,它借助“路由”中间件实现了请求与Action之间的映射,并在此基础之上实现了激活(Controller)、执行(Action)及呈现(View)等一系列功能。应用程序可以视为某个中间件的一部分,如果一定要将它独立出来,由服务器、中间件和应用组成的管道如下图所示。

三、中间件

ASP.NET Core的请求处理管道由一个服务器和一组中间件组成,位于“龙头”的服务器负责请求的监听、接收、分发和最终的响应,而针对该请求的处理则由后续的中间件来完成。如果读者希望对请求处理管道具有深刻的认识,就需要对中间件有一定程度的了解。

RequestDelegate

从概念上可以将请求处理管道理解为“请求消息”和“响应消息”流通的管道,服务器将接收的请求消息从一端流入管道并由相应的中间件进行处理,生成的响应消息反向流入管道,经过相应中间件处理后由服务器分发给请求者。但从实现的角度来讲,管道中流通的并不是所谓的请求消息与响应消息,而是一个针对当前请求创建的上下文。这个上下文被抽象成如下这个HttpContext类型,我们利用HttpContext不仅可以获取针对当前请求的所有信息,还可以直接完成针对当前请求的所有响应工作。

publicabstractclass HttpContext

{

    publicabstractHttpRequest    Request {get;set; }

    publicabstractHttpResponse    Response {get; }

    ...

}

既然流入管道的只有一个共享的HttpContext上下文,那么一个Func<HttpContext,Task>对象就可以表示处理HttpContext的操作,或者用于处理HTTP请求的处理器。由于这个委托对象非常重要,所以ASP.NET Core专门定义了如下这个名为RequestDelegate的委托类型。既然有这样一个专门的委托对象来表示“针对请求的处理”,那么中间件是否能够通过该委托对象来表示?

publicdelegateTask RequestDelegate(HttpContext context);

Func<RequestDelegate, RequestDelegate>

实际上,组成请求处理管道的中间件可以表示为一个类型为Func<RequestDelegate, RequestDelegate>的委托对象,但初学者很难理解这一点,所以下面对此进行简单的解释。由于RequestDelegate可以表示一个HTTP请求处理器,所以由一个或者多个中间件组成的管道最终也体现为一个RequestDelegate对象。对于下图所示的中间件Foo来说,后续中间件(Bar和Baz)组成的管道体现为一个RequestDelegate对象,该对象会作为中间件Foo输入,中间件Foo借助这个委托对象将当前HttpContext分发给后续管道做进一步处理。

表示中间件的Func<RequestDelegate, RequestDelegate>对象的输出依然是一个RequestDelegate对象,该对象表示将当前中间件与后续管道进行“对接”之后构成的新管道。对于表示中间件Foo的委托对象来说,返回的RequestDelegate对象体现的就是由Foo、Bar和Baz组成的请求处理管道。

既然原始的中间件是通过一个Func<RequestDelegate, RequestDelegate>对象表示的,就可以直接注册这样一个对象作为中间件。中间件的注册可以通过调用IWebHostBuilder接口的Configure扩展方法来完成,该方法的参数是一个Action<IApplicationBuilder>类型的委托对象,可以通过调用IApplicationBuilder接口的Use方法将表示中间件的Func<RequestDelegate, RequestDelegate>对象添加到当前中间件链条上。

publicstaticclass WebHostBuilderExtensions

{

    publicstaticIWebHostBuilder Configure(thisIWebHostBuilder hostBuilder, ction configureApp);

}publicinterface IApplicationBuilder

{

    IApplicationBuilder Use(Func middleware);

}

在如下所示的代码片段中,我们创建了两个Func<RequestDelegate, RequestDelegate>对象,它们会在响应中写入两个字符串(“Hello”和“World!”)。在针对IWebHostBuilder接口的Configure方法的调用中,可以调用IApplicationBuilder接口的Use方法将这两个委托对象注册为中间件。

class Program

{

    staticvoid Main()

    {

        staticRequestDelegate Middleware1(RequestDelegate next) =>asynccontext =>        {

            awaitcontext.Response.WriteAsync("Hello");

            await next(context);

        };

        staticRequestDelegate Middleware2(RequestDelegate next) =>asynccontext =>        {

            awaitcontext.Response.WriteAsync(" World!");

        };

        Host.CreateDefaultBuilder().ConfigureWebHostDefaults(builder => builder.Configure(app => app

            .Use(Middleware1)

            .Use(Middleware2)))

        .Build()

        .Run();

    }

}

由于我们注册了如上所示的两个中间件,所以它们会按照注册的顺序对分发给它们的请求进行处理。运行该程序后,如果利用浏览器对监听地址(“http://localhost:5000”)发送请求,那么两个中间件写入的字符串会以下图所示的形式呈现出来。

虽然可以直接采用原始的Func<RequestDelegate, RequestDelegate>对象来定义中间件,但是在大部分情况下,我们依然倾向于将自定义的中间件定义成一个具体的类型。至于中间件类型的定义,ASP.NET Core提供了如下两种不同的形式可供选择。

强类型定义:自定义的中间件类型显式实现预定义的IMiddleware接口,并在实现的方法中完成针对请求的处理。

基于约定的定义:不需要实现任何接口或者继承某个基类,只需要按照预定义的约定来定义中间件类型。

Run方法的本质

在演示的Hello World应用中,我们调用IApplicationBuilder接口的Run扩展方法注册了一个RequestDelegate对象来处理请求,实际上,该方法仅仅是按照如下方式注册了一个中间件。由于注册的中间件并不会将请求分发给后续的中间件,如果调用IApplicationBuilder接口的Run方法后又注册了其他的中间件,后续中间件的注册将毫无意义。

publicstaticclass RunExtensions

{

    publicstaticvoidRun(this IApplicationBuilder app, RequestDelegate handler)

    => app.Use(_ => handler);

}

四、定义强类型中间件

如果采用强类型的中间件类型定义方式,只需要实现如下这个IMiddleware接口,该接口定义了唯一的InvokeAsync方法,用于实现中间件针对请求的处理。这个InvokeAsync方法定义了两个参数:第一个参数是代表当前请求上下文的HttpContext对象,第二个参数是代表后续中间件组成的管道的RequestDelegate对象,如果当前中间件最终需要将请求分发给后续中间件进行处理,只需要调用这个委托对象即可,否则应用针对请求的处理就到此为止。

publicinterface IMiddleware

{

    Task InvokeAsync(HttpContext context, RequestDelegate next);

}

在如下所示的代码片段中,我们定义了一个实现了IMiddleware接口的StringContentMiddleware中间件类型,在实现的InvokeAsync方法中,它将构造函数中指定的字符串作为响应的内容。由于中间件最终是采用依赖注入的方式来提供的,所以需要预先对它们进行服务注册,针对StringContentMiddleware的服务注册是通过调用IHostBuilder接口的ConfigureServices方法完成的。

class Program

{

    staticvoid Main()

    {           

        Host.CreateDefaultBuilder()

            .ConfigureServices(svcs => svcs.AddSingleton(newStringContentMiddleware("Hello World!")))

            .ConfigureWebHost(builder => builder

            .Configure(app => app.UseMiddleware()))

        .Build()

        .Run();

    }

    privatesealedclass StringContentMiddleware : IMiddleware

    {

        privatereadonlystring _contents;

        publicStringContentMiddleware(stringcontents) => _contents = contents;

        publicTask InvokeAsync(HttpContext context, RequestDelegate next) => context.Response.WriteAsync(_contents);

    }

}

针对中间件自身的注册则体现在针对IWebHostBuilder接口的Configure方法的调用上,最终通过调用IApplicationBuilder接口的UseMiddleware<TMiddleware>方法来注册中间件类型。如下面的代码片段所示,在注册中间件类型时,可以以泛型参数的形式来指定中间件类型,也可以调用另一个非泛型的方法重载,直接通过Type类型的参数来指定中间件类型。值得注意的是,这两个方法均提供了一个参数params,它是为针对“基于约定的中间件”注册设计的,当我们注册一个实现了IMiddleware接口的强类型中间件的时候是不能指定该参数的。启动该程序后利用浏览器访问监听地址,依然可以得到上图所示的输出结果。

publicstaticclass UseMiddlewareExtensions

{

    publicstaticIApplicationBuilder UseMiddleware(thisIApplicationBuilder app,paramsobject[] args);

    publicstaticIApplicationBuilder UseMiddleware(thisIApplicationBuilder app, Type middleware,paramsobject[] args);   

}

五、按照约定定义中间件

可能我们已经习惯了通过实现某个接口或者继承某个抽象类的扩展方式,但是这种方式有时显得约束过重,不够灵活,所以可以采用另一种基于约定的中间件类型定义方式。这种定义方式比较自由,因为它并不需要实现某个预定义的接口或者继承某个基类,而只需要遵循一些约定即可。自定义中间件类型的约定主要体现在如下几个方面。

中间件类型需要有一个有效的公共实例构造函数,该构造函数要求必须包含一个RequestDelegate类型的参数,当前中间件利用这个委托对象实现针对后续中间件的请求分发。构造函数不仅可以包含任意其他参数,对于RequestDelegate参数出现的位置也不做任何约束。

针对请求的处理实现在返回类型为Task的InvokeAsync方法或者Invoke方法中,它们的第一个参数表示当前请求上下文的HttpContext对象。对于后续的参数,虽然约定并未对此做限制,但是由于这些参数最终由依赖注入框架提供,所以相应的服务注册必须存在。

采用这种方式定义的中间件类型同样是调用前面介绍的UseMiddleware方法和UseMiddleware<TMiddleware>方法进行注册的。由于这两个方法会利用依赖注入框架来提供指定类型的中间件对象,所以它会利用注册的服务来提供传入构造函数的参数。如果构造函数的参数没有对应的服务注册,就必须在调用这个方法的时候显式指定。

在如下所示的代码片段中,我们定义了一个名为StringContentMiddleware的中间件类型,在执行这个中间件时,它会将预先指定的字符串作为响应内容。StringContentMiddleware的构造函数具有两个额外的参数:contents表示响应内容,forewardToNext则表示是否需要将请求分发给后续中间件进行处理。在调用UseMiddleware<TMiddleware>扩展方法对这个中间件进行注册时,我们显式指定了响应的内容,至于参数forewardToNext,我们之所以没有每次都显式指定,是因为这是一个具有默认值的参数。

class Program

{

    staticvoid Main()

    {

        Host.CreateDefaultBuilder()

            .ConfigureWebHostDefaults(builder => builder.Configure(app => app

                .UseMiddleware("Hello")

                .UseMiddleware(" World!",false)))

        .Build()

        .Run();

    }

    privatesealedclass StringContentMiddleware

    {

        privatereadonly RequestDelegate    _next;

        privatereadonlystring _contents;

        privatereadonlybool _forewardToNext;

        publicStringContentMiddleware(RequestDelegate next,stringcontents,boolforewardToNext =true)

        {

            _next  = next;

            _forewardToNext = forewardToNext;

            _contents = contents;

        }

        publicasync Task Invoke(HttpContext context)

        {

            await context.Response.WriteAsync(_contents);

            if (_forewardToNext)

            {

                await _next(context);

            }

        }

    }

}

启动该程序后,利用浏览器访问监听地址依然可以得到下图所示的输出结果。对于前面介绍的两个中间件,它们的不同之处除了体现在定义和注册方式上,还体现在自身生命周期的差异上。具体来说,强类型方式定义的中间件可以注册为任意生命周期模式的服务,但是按照约定定义的中间件则总是一个Singleton服务。

龙华大道1号 http://www.kinghill.cn/Dynamics/2106.html

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,951评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,606评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,601评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,478评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,565评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,587评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,590评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,337评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,785评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,096评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,273评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,935评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,578评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,199评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,440评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,163评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,133评论 2 352

推荐阅读更多精彩内容

  • 中间件(Middleware)是ASP.NET Core中的一个重要特性。所谓中间件就是嵌入到应用管道中用于处理请...
    雪飞鸿阅读 2,020评论 0 0
  • 手写一个简版 asp.net core Intro 之前看到过蒋金楠老师的一篇 200 行代码带你了解 asp.n...
    天天向上卡索阅读 526评论 0 0
  • ASP.NET Core知多少系列:总体介绍及目录 1. 引言 对于ASP.NET Core应用程序来说,我们要记...
    圣杰阅读 4,557评论 4 18
  • 久违的晴天,家长会。 家长大会开好到教室时,离放学已经没多少时间了。班主任说已经安排了三个家长分享经验。 放学铃声...
    飘雪儿5阅读 7,519评论 16 22
  • 创业是很多人的梦想,多少人为了理想和不甘选择了创业来实现自我价值,我就是其中一个。 创业后,我由女人变成了超人,什...
    亦宝宝阅读 1,805评论 4 1