ASP.NET Core:中间件

一、什么是中间件

我们都知道,任何的一个web框架都是把http请求封装成一个管道,每一次的请求都是经过管道的一系列操作,最终才会到达我们写的代码中。而中间件就是用于组成应用程序管道来处理请求和响应的组件。管道内的每一个组件都可以选择是否将请求转交给下一个组件,并在管道中调用下一个组件之前和之后执行某些操作。请求委托被用来建立请求管道,请求委托处理每一个HTTP请求。

中间件可以认为有两个基本的职责:

  1. 选择是否将请求传递给管道中的下一个中间件。
  2. 可以在管道中的下一个中间件前后执行一些工作。

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

ASP.NET Core请求管道由一系列的请求委托所构成,它们一个接着一个的被调用,看下面一张微软官方的中间件请求管道图(图中执行线程按黑色箭头的顺序执行):

中间件短路:每一个委托在下一个委托之前和之后都有机会执行操作。任何委托都能选择停止传递到下一个委托,而是结束请求并开始响应,这就是请求管道的短路,这是一种有意义的设计,因为它可以避免一些不必要的工作。比如说,一个授权(authorization)中间件只有在通过身份验证之后才能调用下一个委托,否则它就会被短路,并返回“Not Authorized”的响应。异常处理委托需要在管道的早期被调用,这样它们就能够捕捉到发生在管道内更深层次出现的异常了。短路可以用下面这张图来表示:

在上图中,我们可以把中间件1认为是身份认证的中间件,HTTP请求发送过来,首先经过身份认证中间件,如果身份认证失败,那么就直接给出响应并返回,不会再把请求传递给下面的中间件2和中间件3.

中间件的执行跟调用的顺序有关,然后在响应时则以相反的顺序返回。

请求在每一步都可能被短路,所以我们要以正确的顺序添加中间件,如异常处理中间件,我们要添加在最开始的地方,这样就能第一时间捕获异常,以及后续中间可能发生的异常,然后最终做处理返回。

我们来看看Configure方法里面提供了哪些中间件:

复制代码
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        // 异常中间件
        app.UseDeveloperExceptionPage();
    }

    // 路由中间件
    app.UseRouting();
    // 授权中间件
    app.UseAuthorization();
    // 终结点中间件
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}
复制代码

1、中间件和过滤器的区别

中间件和过滤器都是一种AOP的思想,他们的功能类似,那么他们有什么区别呢?

  1. 过滤器更加贴合业务,它关注于应用程序本身,关注的是如何实现业务,比如对输出结果进行格式化,对请求的ViewModel进行数据校验,这时就肯定要使用过滤器了。过滤器是MVC的一部分,它可以拦截到你Action上下文的一些信息,而中间件是没有这个能力的。可以认为过滤器是附加性的一种功能,它只是中间件附带表现出来的特征。
  2. 中间件是管道模型里重要的组成部分,不可或缺,而过滤器可以没有。

二、中间件常用方法

中间件中定义了Run、Use、Map、MapWhen几种方法,我们下面一一讲解这几种方法。

1、Run方法

 我们先来看到Run()方法的定义:

中定义中可以看出:Run()方法中只有一个RequestDelegate委托类型的参数,没有Next参数,所以Run()方法也叫终端中间件,不会将请求传递给下一个中间件,也就是发生了“短路”。看下面的代码:

复制代码
// Run方法向应用程序的请求管道中添加一个RequestDelegate委托
// 放在管道最后面,终端中间件
app.Run(handler: async context => 
{
    await context.Response.WriteAsync(text: "Hello World1\r\n");
});
app.Run(handler: async context =>
{
    await context.Response.WriteAsync(text: "Hello World2\r\n");
});
复制代码

程序运行结果:

可以看到:只输出了中间件1的信息,没有输出中间件2的信息,说明发生了短路。

注意:Run()方法被称为终端中间件,要放在所有中间件的最后面,否则在Run()方法后面的中间件将不会被执行。

2、Use方法

我们先来看看Use()方法的定义:

可以看出:Use方法的参数是一个Func委托,输入参数是一个RequestDelegate类型的委托,返回参数也是一个RequestDelegate类型的委托,这里表示调用下一个中间件,我们在来看看RequestDelegate委托的定义:

可以看出:RequestDelegate是一个委托,有一个HttpContext类型的参数,HttPContext表示Http请求上下文,可以获取请求信息,返回值是Task类型,明白了Use()方法的参数以后,我们写一个自定义的Use()方法:

复制代码
// 向应用程序的请求管道中添加一个Func委托,这个委托其实就是所谓的中间件。
// context参数是HttpContext,表示HTTP请求的上下文对象
// next参数表示管道中的下一个中间件委托,如果不调用next,则会使管道短路
// 用Use可以将多个中间件链接在一起
app.Use(async (context, next) =>
{
    await context.Response.WriteAsync(text: "hello Use1\r\n");
    // 调用下一个委托
    await next();
});
app.Use(async (context, next) =>
{
    await context.Response.WriteAsync(text: "hello Use2\r\n");
    // 调用下一个委托
    await next();
});
复制代码

程序运行结果:

我们在上面说过,可以在调用中间件之前和之后做一些工作,看下面的代码:

复制代码
// 向应用程序的请求管道中添加一个Func委托,这个委托其实就是所谓的中间件。
// context参数是HttpContext,表示HTTP请求的上下文对象
// next参数表示管道中的下一个中间件委托,如果不调用next,则会使管道短路
// 用Use可以将多个中间件链接在一起
app.Use(async (context, next) =>
{
    // 解决中文乱码问题
    context.Response.ContentType = "text/plain; charset=utf-8";
    await context.Response.WriteAsync(text: "中间件1:传入请求\r\n");
    // 调用下一个委托
    await next();
    await context.Response.WriteAsync(text: "中间件1:传出响应\r\n");
});
app.Use(async (context, next) =>
{
    await context.Response.WriteAsync(text: "中间件2:传入请求\r\n");
    // 调用下一个委托
    await next();
    await context.Response.WriteAsync(text: "中间件2:传出响应\r\n");
});
app.Run(handler:async context =>
{
    await context.Response.WriteAsync(text: "中间件3:处理请求并生成响应\r\n");
});
复制代码

程序运行结果:

 我们可以总结上面代码的执行顺序:

  1. 请求先到达中间件1,然后输出(中间件1:传入请求)
  2. 然后中间件1调用next()。next()会调用管道中的中间件2。
  3. 中间件2输出(中间件2:传入请求)。
  4. 然后中间件2会调用next()。next()在调用管道中的中间件3。
  5. 中间件3处理请求并生成响应,不在调用下一个中间件,所以我们看到输出(中间件3:处理请求并生成响应)。
  6. 这时管理开始发生逆转。
  7. 此时控制器将交回到中间件2,并将中间件3生成的响应传递给它。中间件2输出(中间件2:传出响应)。
  8. 最后,中间件2在将控制权交给中间件1。
  9. 中间件1最后输出(中间件1:传出响应),这就是我们最后看的的结果。

我们知道:Use()方法中有两个参数,next参数表示调用管道中的下一个中间件,如果不调用next,那么也会使管道发生短路,相当于Run()方法,看下面的代码:

复制代码
// 向应用程序的请求管道中添加一个Func委托,这个委托其实就是所谓的中间件。
// context参数是HttpContext,表示HTTP请求的上下文对象
// next参数表示管道中的下一个中间件委托,如果不调用next,则会使管道短路
// 用Use可以将多个中间件链接在一起
app.Use(async (context, next) =>
{
    // 解决中文乱码问题
    context.Response.ContentType = "text/plain; charset=utf-8";
    await context.Response.WriteAsync(text: "中间件1:传入请求\r\n");
    // 调用下一个委托
    await next();
    await context.Response.WriteAsync(text: "中间件1:传出响应\r\n");
});
app.Use(async (context, next) =>
{
    await context.Response.WriteAsync(text: "中间件2:传入请求\r\n");
    // 调用下一个委托
    await next();
    await context.Response.WriteAsync(text: "中间件2:传出响应\r\n");
});
//app.Run(handler:async context =>
//{
//    await context.Response.WriteAsync(text: "中间件3:处理请求并生成响应\r\n");
//});
// Use方法也可以不调用next,表示发生短路
app.Use(async (context, next) =>
{
    await context.Response.WriteAsync(text: "中间件3:处理请求并生成响应\r\n");
});
复制代码

程序运行结果:

可以看出:如果使用Use()方法,不调用next,实现的效果跟使用Run()方法一样,都会使管道发生短路。

3、Map方法

Map作为惯例,将管道分流。Map根据给定请求路径匹配将请求管道分流。如果请求路径以指定路径开始,则执行分支。看一下Map()方法的定义:

可以看到Map方法有两个参数:第一个参数是匹配规则,第二个参数是Action泛型委托,泛型委托参数是IApplicationBuilder类型,和Configure方法的第一个参数类型相同 。这就表示可以把实现了Action泛型委托的方法添加到中间件管道中执行。

我们首先定义一个方法,该方法的参数是IApplicationBuilder类型:

复制代码
/// <summary>
/// 自定义方法
/// </summary>
/// <param name="app">IApplicationBuilder</param>
private void HandleMap1(IApplicationBuilder app)
{
    app.Run(handler: async context => 
    {
        await context.Response.WriteAsync(text: "Hello Map1");
    });
}

/// <summary>
/// 自定义方法
/// </summary>
/// <param name="app">IApplicationBuilder</param>
private void HandleMap2(IApplicationBuilder app)
{
    app.Run(handler: async context =>
    {
        await context.Response.WriteAsync(text: "Hello Map2");
    });
}
复制代码

然后看一下使用Map方法的代码:

复制代码
// Map可以根据匹配的URL来选择执行,简单来说就是根据URL进行分支选择执行
// 有点类似于MVC中的路由
// 匹配的URL:http://localhost:5000/Map1
app.Map(pathMatch: "/Map1", configuration: HandleMap1);
// 匹配的URL:http://localhost:5000/Map2
app.Map(pathMatch: "/Map2", configuration: HandleMap2);
复制代码

运行程序,然后在浏览器地址栏里面输入:http://localhost:5000/Map1,输出结果:

在地址栏里面在输入:http://localhost:5000/Map2,输出结果:

Map还支持嵌套,看下面的代码:

复制代码
// 嵌套Map
app.Map(pathMatch: "/Map1", configuration: App1 => 
{
    //
    App1.Map("/Map2",action=> 
    {
        action.Run(async context => 
        {
            await context.Response.WriteAsync("This is /Map1/Map2");
        });
    });
    App1.Run(async context => 
    {
        await context.Response.WriteAsync("This is no-map");
    });
});
复制代码

访问http://localhost:5000/Map1/123输出结果:

访问http://localhost:5000/Map1输出结果:

访问http://localhost:5000/Map1/Map2输出结果:

Map也可以同时匹配多个段,看下面的代码:

运行程序,输出结果:

访问http://localhost:5000/Map1/Map2输出结果:

4、Mapwhen方法

MapWhen是基于给定的谓词分支请求管道。任何使Func<HttpContext,bool>返回true的谓词的请求都被映射到新的管道分支。

我们先来看看Mapwhen方法的定义:

可以看出:MapWhen方法有两个参数:第一个参数是Func类型的委托,输入参数是HttpContext,输出参数是bool类型。第二个参数是Action委托,参数是IApplicationBuilder类型,表示也可以把实现Action委托的方法添加到中间件管道中执行。

看下面的例子,如果url中包括name查询参数,则执行HandleName方法,如果包含age查询参数,则执行HandleAge方法,否则执行Run()方法。

HandleName和HandleAge方法定义如下:

复制代码
private void HandleName(IApplicationBuilder app)
{
    app.Run(handler: async context =>
    {
        await context.Response.WriteAsync(text: $"This name is: {context.Request.Query["name"]}");
    });
}

private void HandleAge(IApplicationBuilder app)
{
    app.Run(handler: async context =>
    {
        await context.Response.WriteAsync(text: $"This age is: {context.Request.Query["age"]}");
    });
}
复制代码

对应的MapWhen方法定义如下:

复制代码
// 如果访问的url参数中包含name,则执行HandleName
app.MapWhen(
// Func委托,输入参数是HttpContext,返回bool值    
predicate: context =>
{
    // 判断url参数中是否包含name
    return context.Request.Query.ContainsKey("name");
}, configuration: HandleName);

// 如果访问的url参数中包含name,则执行HandleAge
app.MapWhen(
// Func委托,输入参数是HttpContext,返回bool值
predicate: context =>
{
    // 判断url参数中是否包含age
    return context.Request.Query.ContainsKey("age");
}, configuration: HandleAge);

app.Run(async context => 
{
    await context.Response.WriteAsync("There is non-Map delegate \r\n");
});
复制代码

运行程序,输出结果:

在url里面添加name查询参数输出结果:

在url里面添加age查询参数输出结果:

三、自定义中间件

在上面的例子中,我们都是使用的官方中间件自动的方法,其实我们也可以自己编写一个中间件。

中间件遵循显示依赖原则,并在其构造函数中暴露所有依赖项。中间件能够利用UseMiddleware<T>扩展方法的优势,直接通过它们的构造函数注入服务。依赖注入服务是自动完成填充的。

ASP.NET Core约定中间件类必须包括以下内容:

  1. 具有类型为RequestDelegate参数的公共构造函数。
  2. 必须有名为Invoke或InvokeAsync的公共方法,此方法必须满足两个条件:方法返回类型是Task、方法的第一个参数必须是HttpContext类型。

我们自定义一个记录IP的中间件,新建一个类RequestIPMiddleware,代码如下:

复制代码
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;

namespace MiddlewareDemo.Middleware
{
    /// <summary>
    /// 记录IP地址的中间件
    /// </summary>
    public class RequestIPMiddleware
    {
        // 私有字段
        private readonly RequestDelegate _next;

        /// <summary>
        /// 公共构造函数,参数是RequestDelegate类型
        /// 通过构造函数进行注入,依赖注入服务会自动完成注入
        /// </summary>
        /// <param name="next"></param>
        public RequestIPMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        /// <summary>
        /// Invoke方法
        /// 返回值是Task,参数类型是HttpContext
        /// </summary>
        /// <param name="context">Http上下文</param>
        /// <returns></returns>
        public async Task Invoke(HttpContext context)
        {
            await context.Response.WriteAsync($"User IP:{context.Connection.RemoteIpAddress.ToString()}\r\n");
            // 调用管道中的下一个委托
            await _next.Invoke(context);
        }
    }
}
复制代码

 然后创建一个扩展方法,对IApplicationBuilder进行扩展:

复制代码
using Microsoft.AspNetCore.Builder;

namespace MiddlewareDemo.Middleware
{
    public static class RequestIPExtensions
    {
        /// <summary>
        /// 扩展方法,对IApplicationBuilder进行扩展
        /// </summary>
        /// <param name="builder"></param>
        /// <returns></returns>
        public static IApplicationBuilder UseRequestIP(this IApplicationBuilder builder)
        {
            // UseMiddleware<T>
            return builder.UseMiddleware<RequestIPMiddleware>();
        }
    }
}
复制代码

最后在Startup类的Configure方法中使用自定义中间件:

// 使用自定义中间件
app.UseRequestIP();

运行程序,查看结果:

这样就完成了一个自定义中间件。

四、官方常用中间件

1、异常处理中间件

当应用程序在开发环境中运行时,开发人员异常页中间件( UseDeveloperExceptionPage )报告应用程序运行时的错误。

当应用程序在生产环境中运行时,异常处理中间件( UseExceptionHandler )捕获下面中间件中引发的异常。

2、HTTPS重定向中间件

HTTPS重定向中间件( UseHttpsRedirection )会将HTTP请求重定向到HTTPS。

3、静态文件中间件

静态文件中间件( UseStaticFiles )返回静态文件,并简化进一步请求处理。

4、Cookie中间件

Cookie策略中间件( UseCookiePolicy )使应用符合欧盟一般数据保护条例的规定。

5、路由中间件

路由中间件( UseRouting )用于路由的请求。

6、身份认证中间件

身份认证中间件( UseAuthentication )尝试对用户进行身份验证,验证通过之后才会允许用户访问安全资源。

7、授权中间件

授权中间件( UseAuthorization )用于授权验证通过的用户可以访问哪些资源。

8、会话中间件

会话中间件( UseSession )建立和维护会话状态。如果应用程序使用会话状态,请在Cookie策略中间件之后和MVC中间件之前调用会话中间件。

9、终结点路由中间件

终结点路由中间件( UseEndpoints )用于将 Razor Pages 终结点添加到请求管道。

 更多中间件组件可以到aspnet 的GitHub仓库中查看:https://github.com/aspnet

示例代码GitHub地址:git@github.com:jxl1024/Middleware.git

posted @ 2021-03-10 15:17  dreamw  阅读(209)  评论(0编辑  收藏  举报