ASP.NETCore学习记录(二) —— ASP.NET Core 中间件

原文:ASP.NETCore学习记录(二) —— ASP.NET Core 中间件

 ASP.NET Core 中间件


目录:

 


我们知道在 ASP.NET 中,有一个面向切面的请求管道,由22个主要的事件构成,能够让我们在往预定的执行顺序里面添加自己的处理逻辑。一般采取两种方式:一种是直接在 Global.asax 中对应的方法中直接添加代码。一种是是在 web.config 中通过注册 HttpModule 来实现对请求管道事件监听,并通过 HttpHandler 进入到我们的应用程序中。而在 ASP.NET Core 中,对请求管道进行了重新设计,通过使用一种称为中间件的方式来进行管道的注册,同时也变得更加简洁和强大。关于 HTTP请求流程和管道模型可以看这篇博客 https://www.cnblogs.com/xuhuale/p/10030878.html


什么是中间件 ?

中间件 (Middleware)组装到应用程序管道中以处理请求和响应的组件。 管道内的每个组件都可以选择是否将请求交给下一个组件,并在管道中调用下一个组件之前和之后执行某些操作。请求委托被用来建立请求管道,请求委托处理每一个HTTP请求。

请求委托通过使用 IApplicationBuilder 类型的 RunMap 以及 Use 扩展方法来配置,并在 Startup 类中传给 Configure方法。每个单独的清求委托都可以被指定为一个内嵌匿名方法,或其定义在一个可重用的类中。这些可重用的类被称作 “中间件”或“中间件组件”。每个位于请求管道内的中间件组件负责调用管道中的下一个组件,或适时短路调用链。

ASP.NET请求管道由一系列的请求委托所构成,它们一个接着一个被调用,如图所示(该执行线程按黑色箭头的顺序执行)。

 

ASP.NETCore学习记录(二) —— ASP.NET Core 中间件
微软官方的一个中间件管道请求图

 

每个委托:

  • 均可在下一个委托前后执行操作
  • 委托还可以决定是否将请求传递给下一个委托(任何委托都能选择停止传递到下一个委托,转而自己处理该请求,这就是请求管道的短路)。

短路是一种有意义的设计,因为它可以避免不必要的工作。比如说:

  • 静态文件中间件可以返回静态文件请求并使管道的其余部分短路。
  • 授权中间件只有通过身份验证之后才能调用下一个中间件,否则就会被他短路。

先在管道中调用异常处理委托,以便它们可以捕获在管道的后期阶段所发生的异常。

返回顶部


IApplicationBuilder

笔记一中,我们就简单介绍过 IApplicationBuilder,在 Startup 类的 Configure方法中,第一个参数便是 IApplicationBuilder

首先,IApplicationBuilder 是用来构建请求管道的,而所谓请求管道,本质上就是对 HttpContext 的一系列操作,即通过对 Request 的处理,来生成 Reponse。在 ASP.NET Core 中定义了一个 RequestDelegate 委托,来表示请求管道中的一个步骤,而对请求管道的注册是通过 Func<RequestDelegate, RequestDelegate> 类型的委托(也就是中间件)来实现的。

返回顶部


使用 IApplicationBuilder 创建中间件

可以先看一下 VisualStudio2017 中默认Web(MVC)站点模板关于请求管道设置的例子。

 

ASP.NETCore学习记录(二) —— ASP.NET Core 中间件
 

 

Startup.csConfigure 方法默认增加了下列这些中间件组件:

  • 异常/错误处理(同时针对开发环境和非开发环境)
  • Https重定向
  • 静态文件服务器
  • Cookie 策略实施
  • MVC
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        // 开发环境
        app.UseDeveloperExceptionPage(); 
    }
    else
    {	
        // 非开发环境
        app.UseExceptionHandler("/Home/Error"); 
        app.UseHsts();
    }

    app.UseHttpsRedirection();// Https重定向
    app.UseStaticFiles();   // 静态文件
    app.UseCookiePolicy();  // 使用Cookie策略

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    }); // MVC
}

上面的代码在非开发环境中,UseExceptionHandler 是第一个被加入到管道中的中间件,因此将会捕获之后调用组件中出现的任何异常,然后跳转到设置的异常页(/Home/Error)。

使用 UseHttpsRedirection 中间件,可以轻松实施HTTPS,将HTTP重定向到HTTPS(ASP.NET Core 2.1具有新功能)。

然后就是静态文件中间件 UseStaticFiles,静态文件中间件不提供授权检查,由它提供的任何文件,包括那些位于wwwroot下的文件都是公开可被访问的。UseCookiePolicy 是使用ASP.NET Core中的Cookie策略(详解Microsoft.AspNetCore.CookiePolicy),管道的最后执行的也就是MVC框架。

顺序
Startup.Configure 方法添加中间件组件的顺序定义了针对请求调用这些组件的顺序,以及响应的相反顺序。 此排序对于安全性、性能和功能至关重要。请求在每一步都可能被短路,所以我们要以正确的顺序添加中间件,如异常处理,我们需要添加在最开始,这样我们就能第一时间捕获异常,以及后续中间可能发生的异常,然后最终做处理返回。

最简单的ASP.NET应用程序(空白Web模板)是使用单个请求委托来处理所有请求。事实上,在这种情况下,并不存在所谓的“管道”,针对每个HTTP请求都调用单个匿名函数来处理。

public void Configure(IApplicationBuilder app)
{
    // 使用单个请求委托来处理所有请求
    app.Run(async (context) =>
    {
        await context.Response.WriteAsync("Hello World!");
    });
}

上方代码中 app.Run() 会中断管道,调用单个匿名函数来处理HTTP请求。在下面的例子中,Run了两个委托,只有第一个委托(“Hello,World!”)会被运行。

public void Configure(IApplicationBuilder app)
{
    app.Run(async (context) =>
    {
        await context.Response.WriteAsync("Hello World!");
    });

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

通过运行项目,发现确实只有在第一个委托执行了,app.Run 终止了管道。

 

ASP.NETCore学习记录(二) —— ASP.NET Core 中间件
 

 

你可以将多个请求委托 app.Use 连接在一起,next参数表示管道内的下一个请求委托。在管道中,可以通过不调用next参数来终止或短路管道。通常可以在执行下一个委托之前和之后执行一些操作,如下例所示:

public void Configure(IApplicationBuilder app)
{
    app.Use(async (context, next) =>
    {
        await context.Response.WriteAsync("Handing Request.\r\n");
        
        // 调用管道中的下一个委托
        await next.Invoke();
        await context.Response.WriteAsync("Finish Handing Request.\r\n");
    });

    app.Run(async (context) =>
    {
        await context.Response.WriteAsync("Hello from Middleware\r\n");
    });
}

运行项目,在浏览器中访问出现如下结果:

 

ASP.NETCore学习记录(二) —— ASP.NET Core 中间件
 

 

返回顶部


Run、Map 与 Use 方法

你可以使用 RunMapUse 方法配置 HTTP 管道。

Run 方法会短路管道,因为它不会调用 next 请求委托。因此 Run 方法一般只在管道底部被调用。Run 方法是一种惯例约定,有些中间件组件可能会暴露它们自己的 Run[Middleware]方法,而这些方法只能在管道末尾处运行。

Use 前面已经简单介绍通过 Use 构建请求管道的例子,Use 方法亦可使管道短路(即不调用 next 请求委托)。

Map 扩展方法用匹配基于请求路径的请求委托,Map只接受路径,并配置单独的中间件管道的功能。 Map* 方法可以基于给定请求路径的匹配项来创建请求管道分支。 如果请求路径以给定路径开头,则执行分支。如下面例子中:

public void Configure(IApplicationBuilder app)
{
    app.Map("/map1", HandleMapTest1);
    app.Map("/map2", HandleMapTest2);
    app.Run(async context =>
    {
        await context.Response.WriteAsync("<p> Hello from non-Map delegate. </p>");
    });
}

private static void HandleMapTest1(IApplicationBuilder app)
{
    app.Run(async context =>
    {
        await context.Response.WriteAsync("<p> Map Test 1 </p>");
    });
}

private static void HandleMapTest2(IApplicationBuilder app)
{
    app.Run(async context =>
    {
        await context.Response.WriteAsync("<p> Map Test 2 </p>");
    });
}

任何基于路径 /map1 的请求都会被管道中所配置 HandleMapTest1 方法处理。基于路径 /map2 的请求都会被管道中所配置 HandleMapTest2 方法处理。

 

ASP.NETCore学习记录(二) —— ASP.NET Core 中间件
 

 

下表显示上例来自 http://localhost:52831 的请求和响应。

请求 响应
http://localhost:52831 Hello from non-Map delegate.
http://localhost:52831/map1 Map Test 1
http://localhost:52831/map2 Map Test 2
http://localhost:52831/map3 Hello from non-Map delegate.

除了基于路径的映射外,MapWhen 方法还支持基于谓语的中间件分支,允许以非常灵活的方式构建单独的管道。任何 Func<HttpContext, bool> 类型的谓语都可以被用于将请求映射到新的管道分支。下例中使用了一个简单的谓语来检测查洵字符串中变量 branch 是否存在:

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.MapWhen(context => context.Request.Query.ContainsKey("branch"), HandleBranch);
        app.Run(async context =>
        {
            await context.Response.WriteAsync("<p> Hello from non-Map delegate. </p>");
        });
    }

    private static void HandleBranch(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            var branchVer = context.Request.Query["branch"];
            await context.Response.WriteAsync($"Branch used = {branchVer}");
        });
    }
}

使用了上面的设置后,任何包含请求字符 branch 的请求将使用定义于 HandleBranch 方法内的管道,如果没有包含查询字符串 branch 的请求,将被 app.Run 所定义的委托处理。

 

ASP.NETCore学习记录(二) —— ASP.NET Core 中间件
 

 

请求 响应
http://localhost:52831 Hello from non-Map delegate.
http://localhost:52831/?branch=1 Branch used = 1
http://localhost:52831/?branch=2 Branch used = 2
http://localhost:52831/?branch Branch used =

另外还可以嵌套 Maps:

app.Map("/level1", level1App => {
   level1App.Map("/level2a", level2AApp => {
       // "/level1/level2a"
       //...
   });
   level1App.Map("/level2b", level2BApp => {
       // "/level1/level2b"
       //...
   });
});

Map 也可以一次匹配多个片段,例如:

app.Map("/level1/level2", HandleMultiSeg); // "/level1/level2"

以下 Startup.Configure 方法将为常见应用方案添加中间件组件:

中间件 描述
身份验证(Authentication) 提供身份验证支持
跨域资源共享(CORS) 配置跨域资源共享
响应支持(Response Caching) 提供缓存响应支持
响应压缩(Response Compression) 提供响应压缩支持
路由(Routing) 定义和约束请求路由
会话(Session) 提供对管理用户会话(session)的支持
静态文件(Static Files) 为静态文件和目录浏览提供服务提供支持
URL Rewriting Middleware 用于重写 Url,并将请求重定向的支持

更多中间件组件可以到 Microsoft 文档 上查看。

返回顶部


实战中间件

如何实现一个中间件呢,下面我们来实际操作。
中间件遵循 显式依赖原则 ,并在其构造函数中暴露所有的依赖项。中间件能够利用 UseMiddleware<T> 扩展方法的优势,直接通过它们的构造函数注入服务。依赖注入服务是自动完成填充的,扩展所用到的 params 参数数组被用于非注入参数。

下面来实现一个记录IP的中间件。

① 新建一个 ASP.NET Core WebApplication 项目,选择空的模板。

 

ASP.NETCore学习记录(二) —— ASP.NET Core 中间件
 

 

然后为项目添加—个 Microsoft.Extensions.Logging.Console。
NuGet命令行执行(请使用官方源):

Install-Package Microsoft.Extensions.Logging.Console

 

ASP.NETCore学习记录(二) —— ASP.NET Core 中间件
 

 

② 新建一个类 RequestIPMiddleware.es,将中间件委托移动到类:

using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace middlewareDemo
{
    public class RequestIPMiddleware
    {
        private readonly RequestDelegate _next;

        private readonly ILogger _logger;

        public RequestIPMiddleware(RequestDelegate next, ILoggerFactory loggerFactory)
        {
            _next = next;
            _logger = loggerFactory.CreateLogger<RequestIPMiddleware>();
        }

        public async Task Invoke(HttpContext context)
        {
            _logger.LogInformation("User IP:" + context.Connection.RemoteIpAddress.ToString());
            await _next.Invoke(context);
        }
    }
}

③ 再新建—个 RequestIPExtensions.cs,以下扩展方法通过 IApplicationBuilder 公开中间件:

using Microsoft.AspNetCore.Builder;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace middlewareDemo
{
    public static class RequestIPExtensions
    {
        public static IApplicationBuilder UseRequestIP(this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<RequestIPMiddleware>();
        }
    }
}

这样就编写好了一个中间件。
④ 使用中间件。在 Startup.cs 中添加 app.UseRequestIP() 来使用中间件:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole();
    app.UseRequestIP();
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

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

然后运行程序,这里选择使用 Kestrel
访问:http://localhost:5000/

 

ASP.NETCore学习记录(二) —— ASP.NET Core 中间件
 

 

成功运行,到这里我们还可以对这个中间件进行进一步改进,增加更多的功能,如限制访问等。


参考原文

Microsoft 文档 ASP.NET Core 中间件
ASP.NET Core 中间件(Middleware)详解
《ASP.NET Core 跨平台开发从入门到实战》

返回顶部


ASP.NETCore学习记录(二) —— ASP.NET Core 中间件

上一篇:HTTP 规范中的那些暗坑


下一篇:2021-09-18