ASP.NET Core 中间件(Middleware)详解

什么是中间件(Middleware)?

中间件是组装到应用程序管道中以处理请求和响应的软件。 每个组件:

  • 选择是否将请求传递给管道中的下一个组件。
  • 可以在调用管道中的下一个组件之前和之后执行工作。

请求委托(Request delegates)用于构建请求管道,处理每个HTTP请求。

请求委托使用RunMapUse扩展方法进行配置。单独的请求委托可以以内联匿名方法(称为内联中间件)指定,或者可以在可重用的类中定义它。这些可重用的类和内联匿名方法是中间件或中间件组件。请求流程中的每个中间件组件都负责调用流水线中的下一个组件,如果适当,则负责链接短路。

将HTTP模块迁移到中间件解释了ASP.NET Core和以前版本(ASP.NET)中的请求管道之间的区别,并提供了更多的中间件示例。

用IApplicationBuilder创建一个中间件管道

ASP.NET Core请求流程由一系列请求委托组成,如下图所示(执行流程遵循黑色箭头):

每个委托可以在下一个委托之前和之后执行操作。委托还可以决定不将请求传递给下一个委托,这称为请求管道的短路。短路通常是可取的,因为它避免了不必要的工作。例如,静态文件中间件可以返回一个静态文件的请求,并使管道的其余部分短路。需要在管道早期调用异常处理委托,因此它们可以捕获后面管道的异常。

最简单的可能是ASP.NET Core应用程序建立一个请求的委托,处理所有的请求。此案例不包含实际的请求管道。相反,针对每个HTTP请求都调用一个匿名方法。

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;

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

第一个 app.Run 委托终止管道。

有如下代码:

通过浏览器访问,发现确实在第一个app.Run终止了管道。

您可以将多个请求委托与app.Use连接在一起。 next参数表示管道中的下一个委托。 (请记住,您可以通过不调用下一个参数来结束流水线。)通常可以在下一个委托之前和之后执行操作,如下例所示:

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.Use(async (context, next) =>
            {
                await context.Response.WriteAsync("进入第一个委托 执行下一个委托之前\r\n");
                //调用管道中的下一个委托
                await next.Invoke();
                await context.Response.WriteAsync("结束第一个委托 执行下一个委托之后\r\n");
            });
            app.Run(async context =>
            {
                await context.Response.WriteAsync("进入第二个委托\r\n");
                await context.Response.WriteAsync("Hello from 2nd delegate.\r\n");
                await context.Response.WriteAsync("结束第二个委托\r\n");
            });
    }
}

使用浏览器访问有如下结果:

可以看出请求委托的执行顺序是遵循上面的流程图的。

注意: 响应发送到客户端后,请勿调用next.Invoke。 响应开始之后,对HttpResponse的更改将抛出异常。 例如,设置响应头,状态代码等更改将会引发异常。在调用next之后写入响应体。

  • 可能导致协议违规。 例如,写入超过content-length所述内容长度。
  • 可能会破坏响应内容格式。 例如,将HTML页脚写入CSS文件。

HttpResponse.HasStarted是一个有用的提示,指示是否已发送响应头和/或正文已写入。

订购

Configure方法中添加中间件组件的顺序定义了在请求上调用它们的顺序,以及响应的相反顺序。 此排序对于安全性,性能和功能至关重要。

Configure方法(如下所示)添加了以下中间件组件:

  1. 异常/错误处理
  2. 静态文件服务
  3. 身份认证
  4. MVC
public void Configure(IApplicationBuilder app)
{
    app.UseExceptionHandler("/Home/Error"); // Call first to catch exceptions
                                                                            // thrown in the following middleware.

    app.UseStaticFiles();                   // Return static files and end pipeline.

    app.UseAuthentication();               // Authenticate before you access
                                                            // secure resources.

    app.UseMvcWithDefaultRoute();          // Add MVC to the request pipeline.
}

上面的代码,UseExceptionHandler是添加到管道中的第一个中间件组件,因此它捕获以后调用中发生的任何异常。

静态文件中间件在管道中提前调用,因此可以处理请求和短路,而无需通过剩余的组件。 静态文件中间件不提供授权检查。 由其提供的任何文件,包括wwwroot下的文件都是公开的。

如果请求没有被静态文件中间件处理,它将被传递给执行身份验证的Identity中间件(app.UseAuthentication)。 身份不会使未经身份验证的请求发生短路。 虽然身份认证请求,但授权(和拒绝)仅在MVC选择特定的剃刀页面或控制器和操作之后才会发生。

授权(和拒绝)仅在MVC选择特定的Razor页面或Controller和Action之后才会发生。

以下示例演示了中间件顺序,其中静态文件的请求在响应压缩中间件之前由静态文件中间件处理。 静态文件不会按照中间件的顺序进行压缩。 来自UseMvcWithDefaultRoute的MVC响应可以被压缩。

public void Configure(IApplicationBuilder app)
{
    app.UseStaticFiles();         // Static files not compressed
    app.UseResponseCompression();
    app.UseMvcWithDefaultRoute();
}

Use, Run, 和 Map

你可以使用UseRunMap配置HTTP管道。Use方法可以使管道短路(即,可以不调用下一个请求委托)。Run方法是一个约定, 并且一些中间件组件可能暴露在管道末端运行的Run [Middleware]方法。Map*扩展用作分支管道的约定。映射根据给定的请求路径的匹配来分支请求流水线,如果请求路径以给定路径开始,则执行分支。

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

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

    public void Configure(IApplicationBuilder app)
    {
        app.Map("/map1", HandleMapTest1);

        app.Map("/map2", HandleMapTest2);

        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello from non-Map delegate. <p>");
        });
    }
}

下表显示了使用以前代码的 http://localhost:19219 的请求和响应:

请求

响应

localhost:1234

Hello from non-Map delegate.

localhost:1234/map1

Map Test 1

localhost:1234/map2

Map Test 2

localhost:1234/map3

Hello from non-Map delegate.

当使用Map时,匹配的路径段将从HttpRequest.Path中删除,并为每个请求追加到Http Request.PathBase

MapWhen根据给定谓词的结果分支请求流水线。 任何类型为Func<HttpContext,bool>的谓词都可用于将请求映射到管道的新分支。 在以下示例中,谓词用于检测查询字符串变量分支的存在:

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

    public void Configure(IApplicationBuilder app)
    {
        app.MapWhen(context => context.Request.Query.ContainsKey("branch"),
                               HandleBranch);

        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello from non-Map delegate. <p>");
        });
    }
}

以下下表显示了使用上面代码 http://localhost:19219 的请求和响应:

请求

响应

localhost:1234

Hello from non-Map delegate.

localhost:1234/?branch=1

Branch used = master

Map支持嵌套,例如:

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

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

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

内置中间件

ASP.NET Core附带以下中间件组件:

中间件

描述

Authentication

提供身份验证支持

CORS

配置跨域资源共享

Response Caching

提供缓存响应支持

Response Compression

提供响应压缩支持

Routing

定义和约束请求路由

Session

提供用户会话管理

Static Files

为静态文件和目录浏览提供服务提供支持

URL Rewriting Middleware

用于重写 Url,并将请求重定向的支持

编写中间件

中间件通常封装在一个类中,并使用扩展方法进行暴露。 查看以下中间件,它从查询字符串设置当前请求的Culture:

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.Use((context, next) =>
        {
            var cultureQuery = context.Request.Query["culture"];
            if (!string.IsNullOrWhiteSpace(cultureQuery))
            {
                var culture = new CultureInfo(cultureQuery);

                CultureInfo.CurrentCulture = culture;
                CultureInfo.CurrentUICulture = culture;
            }

            // Call the next delegate/middleware in the pipeline
            return next();
        });

        app.Run(async (context) =>
        {
            await context.Response.WriteAsync(
                $"Hello {CultureInfo.CurrentCulture.DisplayName}");
        });

    }
}

您可以通过传递Culture来测试中间件,例如 http://localhost:19219/?culture=zh-CN

以下代码将中间件委托移动到一个类:

using Microsoft.AspNetCore.Http;
using System.Globalization;
using System.Threading.Tasks;

namespace Culture
{
    public class RequestCultureMiddleware
    {
        private readonly RequestDelegate _next;

        public RequestCultureMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public Task Invoke(HttpContext context)
        {
            var cultureQuery = context.Request.Query["culture"];
            if (!string.IsNullOrWhiteSpace(cultureQuery))
            {
                var culture = new CultureInfo(cultureQuery);

                CultureInfo.CurrentCulture = culture;
                CultureInfo.CurrentUICulture = culture;

            }

            // Call the next delegate/middleware in the pipeline
            return this._next(context);
        }
    }
}

以下通过IApplicationBuilder的扩展方法暴露中间件:

using Microsoft.AspNetCore.Builder;

namespace Culture
{
    public static class RequestCultureMiddlewareExtensions
    {
        public static IApplicationBuilder UseRequestCulture(
            this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<RequestCultureMiddleware>();
        }
    }
}

以下代码从Configure调用中间件:

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.UseRequestCulture();

        app.Run(async (context) =>
        {
            await context.Response.WriteAsync(
                $"Hello {CultureInfo.CurrentCulture.DisplayName}");
        });

    }
}

中间件应该遵循显式依赖原则,通过在其构造函数中暴露其依赖关系。 中间件在应用程序生命周期构建一次。 如果您需要在请求中与中间件共享服务,请参阅以下请求相关性。

中间件组件可以通过构造方法参数来解析依赖注入的依赖关系。 UseMiddleware也可以直接接受其他参数。

每个请求的依赖关系

因为中间件是在应用程序启动时构建的,而不是每个请求,所以在每个请求期间,中间件构造函数使用的作用域生命周期服务不会与其他依赖注入类型共享。 如果您必须在中间件和其他类型之间共享作用域服务,请将这些服务添加到Invoke方法的签名中。 Invoke方法可以接受由依赖注入填充的其他参数。 例如:

public class MyMiddleware
{
    private readonly RequestDelegate _next;

    public MyMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext httpContext, IMyScopedService svc)
    {
        svc.MyProperty = 1000;
        await _next(httpContext);
    }
}

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏QQ音乐技术团队的专栏

Unity Android Plugin开发指南

本文将介绍如何在Unity工程中使用Android或者Java的库,包括: 如何在Unity项目中使用Android Plugin Unity-Android相...

1.1K7
来自专栏码农阿宇

Asp.Net WebApi 调试利器“单元测试”

当我们编辑好一个WebApi应用程序后,需要对该Api接口进行调试,传统的调试办法是在方法内设置断点,然后用PostMan等http工具模拟访问进行查看WebA...

3025
来自专栏Java技术栈

Java高级篇——深入浅出Java类加载机制

类加载器 简单讲,类加载器ClassLoader的功能就是负责将class文件加载到jvm内存。 类加载器分类 从虚拟机层面讲分为两大类型的类加载器,一是Boo...

2647
来自专栏青青天空树

jersey之get,put,post,delete简单使用

  要使用jersey首先要有相应的依赖包,获取方法有很多,本地下载依赖文件或maven获取,这里假设你的环境已经搭建好了。要使用jersey首先要初始化一个c...

872
来自专栏java一日一条

关于 ASP.NET 内存缓存你需要知道的 10 点

缓存机制的主要目的是提高应用程序的性能。作为 ASP.NET 开发人员,你可能会意识到 ASP.NET Web 窗体以及 ASP.NET MVC 可以使用 Ca...

662
来自专栏技术博客

Asp.Net Web API 2第五课——Web API路由

    Asp.Net Web API第一课——入门 http://www.cnblogs.com/aehyok/p/3432158.html

1245
来自专栏林德熙的博客

Roslyn 在项目文件使用条件判断 判断不相等判断大小判断文件存在判断多个条件使用的范围

本文是 手把手教你写 Roslyn 修改编译 的文章,在阅读本文之前,希望已经知道了大多数关于 msbuild 的知识

841
来自专栏GreenLeaves

Common.Logging源码解析一

Common.Logging是Apache下的一个开源日志接口组件,主要用于切换不同的日志库,因为当前流行的日志库有很多向log4j、log4net(log4j...

18510
来自专栏梦魇小栈

笔记:NPM版本号自增,自动化发布NPM包

Description 在一个包的目录下执行此命令,会提升版本号,并把这个新的版本号写进package.json文件,如果存在npm-shrinkwrap.js...

1392
来自专栏直播开发

SRS开源直播服务 - StateThreads微线程框架学习

SRS是一个开源流媒体服务器,在目前大火的直播行业中较多的被使用。笔者作为直播行业的后台开发,对SRS的学习必不可少,本文主要讲解SRS底层使用的微线程开源...

6754

扫码关注云+社区