ASP.NET Core中的中间件及其工作原理

中间件

中间件模式是经典的23中设计模式中的一种,也是.Net Core新引入的一种设计模式。我们在开发项目的过程中会大量的使用中间件。

在ASP.NET Core中,中间件是一个可以处理HTTP请求或响应的软件管道。在ASP.NET Core中,中间件具有非常特定的用途。比如,我们可能需要一个中间件验证用户,一个中间件处理错误,一个中间件来提供静态文件,如Javascript文件、CSS文件和图片等。

我们在ASP.NET Core中使用这些中间件设置请求处理管道,正是这管道决定了如何处理请求。而请求处理管道是由Startup.cs文件中的Configure()方法进行配置的,它也是应用程序启动的一个重要部分。

​ 以下是Congifure()方法中的代码

   // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGet("/", async context =>
                {
                    await context.Response.WriteAsync("Hello World!");
                });
            });
        }

如读者所见,由空项目模板生成的Configure()方法中,有一个非常简单的请求处理管道,其中有3个中间件。

  • UseDeveloperExceptionPage()
  • UseRouting()
  • UseEndpoints()

现在为了更好地了解中间件,我们对代码进行修改,以下是修改后Configure()方法中的代码,有两个中间件。

  public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.Run(async (context) =>
            {
                await context.Response.WriteAsync("Hello World");
            });
        }

通过这个简单的请求处理管道,所有的应用程序都可以将消息写入,然后再由浏览器显示出来。

中间件在ASP.NET Core中的工作原理

如图所示,显示中间以及它们如何适应请求处理管道。

在这里插入图片描述

​ 在ASP.NET Core中,中间件可以同时处理传入请求和传出响应。因此,中间件可以处理传入请求并将该请求传递给管道中的下一个中间件以进行下一步处理。

​ 如果我们有一个日志记录中间件,那么它可能只是记录请求的时间,处理完毕后将请求传递给下一个中间件以进行下一步处理。

终端中间件:该中间件可以处理请求,并决定不调用管道中的下一个中间件,从而使管道短路。微软将其命名为terminal middleware,翻译为终端中间件。短路通常是被允许的,因为它可以避免一些不必要的工作。

​ 如果请求的是一个图片文件或一个CSS纯静态文件,那么StaticFiles中间件可以在处理该请求后,使管道中的其余部分短路。就是说,在图中,如果请求是针对静态文件,则StaticFiles中间件不会调用MVC中间件,从而避免一些无用的操作。

中间件可以通过传入的HTTP请求来响应HTTP请求。比如,管道中的MVC中间件负责处理对URL/students的请求并返回学生列表信息。

中间件还可以处理传出响应。比如,日志记录中间件可以记录响应发送时间。此外,它还可以通过计算接收请求时间和响应时间之间的差异来计算处理请求所花费的时间。

中间件是按照添加到管道的顺序执行的。我们要以正确的顺序添加中间件,否则应用程序可能无法按预期运行。有时候程序虽然会编译成功,但是依然无法正常使用。

​ 在大型团队中,中间件应以NuGet包的形式提供。由NuGet处理更新,通过尽量将中间件拆的足够小,以提供每个中间件独立更新的能力。

​ 在微服务架构中往往设计很多种中间件,大型互联网公司的架构团队会开发各种不同类型的中间件。

​ 在我们的项目中,根据程序需求,可以向请求处理管道添加尽可能多的中间件。如果,我们要使用一些静态HTML页面和图像开发简单的Web应用程序,那么请求处理管道可能只包含StaticFiles中间件。这就是模块化设计的好处,让每个人都像玩积木一样。

​ 另一方面,如果,我们需要开发一个安全的数据驱动设计的Web应用程序,那么可能需要几个中间,如StaticFiles中间件、身份验证中间件、授权中间件和MVC中间件等。

配置ASP.NET Core请求处理管道

我们将使用中间件为ASP.NET Core应用程序配置请求处理管道。

作为应用程序启动的一部分,要在Configure()方法中设置请求处理管道

 public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.Run(async (context) =>
            {
                await context.Response.WriteAsync("Hello World");
            });
        }
    }

​ 目前的代码有两个中间件在管道中:UseDeveloperExceptionPage()方法和Run()方法

​ UseDeveloperExceptionPage()中间件:顾名思义,如果存在异常并且环境变量是Development,此中间件会被调用,显示开发异常界面。我们将在以后讨论这个UseDeveloperExceptionPage()中间件环境变量的配合使用

​ 第二个中间件是注册Run()方法到管道中,它只能处理信息传入的Response对象。目前,它是一个响应所有请求的中间件,返回“Hello World“。在这种情况下,无论请求路径是什么,所有请求都会被这个中间件处理,我们得到的返回值都是这个中间件调用Response对象返回的string类型的字符串。

​ 请注意,返回的值是纯文本而不是HTML。我们可以通过检测页面源代码来确认这一点。可以看到,源代码中没有任何HTML标签,只是纯文本。

​ 即使现在创建一个html文件,并且在请求中包含该文件的路径:http://localhost:5000/index.html,应用程序也无法返回该静态文件。这是因为目前我们的请求处理管道中没有可以提供静态文件的中间件,所以不支持HTML、图像、CSS和Javascript等文件的显示。在后面的内容中,我们将添加所需的中间件以便能够提供静态文件。

Configure()代码解析

 app.Run(async (context) =>
            {
                await context.Response.WriteAsync("Hello World");
            });

关于这段代码的解析如下:

  • 我们调用Run()方法添加中间件到请求处理管道中
  • 如果将鼠标悬停在Run()方法上,则可以从智能提示中看到Run()方法是作为IApplicationBuilder接口的扩展方法实现的。这就是我们能够在IApplicationBuilder对象应用程序上调用此Run()方法的原因。

在这里插入图片描述

  • 传递给Run()方法的参数是一个RequestDelegate,我们可以从智能提示中看到它。
  • RequestDelegate是一个作为HttpContext对象的参数委托
  • 通过这个HttpContext对象,中间件可以访问传入的HTTP请求和传出的HTTP响应。
  • 目前,我们使用Lambda将请求进行传递,因为Lambda通过委托内联的方式作为匿名方法传递,所以Lambda表达式是一种特殊的委托。
  • 使用Run()扩展方法,只能将一个在终端中间件添加到请求管道。
  • 终端中间件会使管道短路,而不会去调用下一个中间件。

中间件掌握测试

  app.Run(async (context) =>
            {
                context.Request.ContentType = "text/plain;charset=utf-8";
                await context.Response.WriteAsync("从第一个中间件中打印Hello World");
            });

            app.Run(async (context) =>
            {
                await context.Response.WriteAsync("从第二个中间件中打印Hello World");
            });

以上中间件的输出结果(在此未放结果截图)说明如下:

  • 我们使用Run()方法注册了两个中间件。
  • 运行此项目时,我们只看到了第一个中间件的响应有返回值。
  • 第二个中间件没有响应。
  • 这是因为使用Run()方法注册的中间件无法调用管道中的下一个中间件。
  • 使用Run()方法注册的中间件是终端中间件。

中间件传递

如果希望中间件能够调用管道中的下一个中间件,则应注册Use()中间件,如下所示:

 app.Use(async (context, next) =>
            {
                await next();
            });

            app.Run(async (context) =>
            {
                await context.Response.WriteAsync("从第二个中间件中打印Hello World");
            });

注意,Use()方法有两个参数。第一个参数是HttpContext上下文对象;第二个参数是Func类型,即它是代表管道中下一个中间件的委托。

实践中间件的工作流程

接下来看一看实践中间件的工作流程

  public void Configure(IApplicationBuilder app, IWebHostEnvironment env,ILogger<Startup> logger)
        {
            app.Use(async (context, next) =>
            {
                logger.LogInformation("MW1:传入请求");
                await next();
                logger.LogInformation("MW1:传出响应");
            });

            app.Use(async (context, next) =>
            {
                logger.LogInformation("MW2:传入请求");
                await next();
                logger.LogInformation("MW2:传出响应");
            });

            app.Use(async (context, next) =>
            {

                await context.Response.WriteAsync("MW3:处理请求并生成响应");
                logger.LogInformation("MW3:处理请求并生成响应");
            });
        }

代码说明如下:

  • ILogger服务被注入Configure()方法中,用于日志的记录
  • 项目启动时,Main()方法调用CreateDefaultBuilder()配置日志记录

读者可以通过查看ASP.NET Core 3.1在Github的源代码来验证这一点。

  • 检查文件中ConfigureLogging()方法,读者会发现,ILogger配置了AddConfiguration、Console、Debug和EventSource。
.ConfigureLogging((hosting,logging)=>
{
 logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
 logging.AddConsole();
 logging.AddDebug();
 logging.AddEventSourceLogger();
}).

代码说明如下:

  • 我们使用依赖注入的方式将ILogger记录到系统中

  • 如果使用.NET Core CLI运行项目,则可以在控制台窗口中查看记录的信息。

  • 如果直接在Visual Studio上运行项目,则可以在输出窗口中查看记录的信息。

  • 我们将看到,信息按以下顺序记录着

    • MW1:传入请求
    • MW2:传入请求
    • MW3:处理请求并生成响应
    • MW2:传出响应
    • MW1:传出响应

在这里插入图片描述

将输出结果与下图所示的内容结和起来理解
在这里插入图片描述

  • 请记住,ASP.NET Core中的中间件可以处理传入请求和传出响应
  • 请求先到达Middleware1,它记录传入请求,因此我们首先看到此消息
  • Middleware1调用next()方法,next()方法会调用管道中的Middleware2
  • Middleware2记录传入请求
  • Middlew2会调用next()方法,next()方法再调用Middleware3
  • Middleware3处理请求并生成响应。
  • 此时请求处理流程在管道中开始反向传递
  • Middleware3将控制权交回到Middleware2,并将Middleware3生成的响应值(处理请求并生成响应)传递给它。Middleware2记录传出响应,这是我们接下来看到的。
  • Middleware2将控制权交给Middleware1
  • Middleware1记录传出响应,这是我们最后看到的。

总结

ASP.NET Core的中间件有以下特点:

  • 可同时被访问和请求
  • 可以处理请求,然后将请求传递给下一个中间件
  • 可以处理请求,并使管道短路
  • 可以处理传出响应
  • 中间件是按照添加的顺序执行的。

中间件的工作流程如下:

  • 所有的请求都会在每个中间件调用next()方法之前触发。请求按照一定方向依次穿过所有管道
  • 当中间件处理请求并产生响应时,请求处理流程在管道中开始反向传递
  • 所有的响应都会在每个中间件调用next()方法之前触发。响应按一定方向依次穿过所有管道。
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值