ASP.NET Core 中间件

ASP.NET Core 中间件原理


翻译:

时间:11/24/2017

示例代码下载 (也可在原文下载全部实例,在aspnetcore-doc-cn-dev\aspnetcore\fundamentals\middleware中找到该章节代码)

环境 :vs2017,ASP.NET Core 2x

什么是中间件

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

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

请求委托用于构建请求管道。请求委托处理每个HTTP请求(任何的http request 都会调用请求委托绑定的方法,类似于HttpModule).


使用Run,Map, 和 Use扩展方法配置请求委托。一个单独的请求委托可以被指定为内嵌匿名方法(称为内嵌中间件),或者可以在可重用类中定义。这些可重用类和内嵌匿名方法就是中间件或中间件组件。请求管道中的每个中间件组件负责调用管道中的下一个组件,或者如果适当的话,将链短路(直接Response).

Migrating HTTP Modules to Middleware 讲解在ASP.NET Core和以前的.NET版本请求管道的不同,并提供了更多的中间件的例程.

用IApplicationBuilder创建一个中间件管道

ASP.NET Core请求管道由一系列请求委托组成,这些委托被为一个接一个的调用,如下图所示(黑色箭头指示执行顺序):

IApplicationBuilder:定义一个类,该类提供配置应用程序请求管道的机制。

Request processing pattern showing a request arriving, processing through three middlewares, and the response leaving the application. Each middleware runs its logic and hands off the request to the next middleware at the next() statement. After the third middleware processes the request, it's handed back through the prior two middlewares for additional processing after the next() statements each in turn before leaving the application as a response to the client.


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

下面建立了一个可能是最简单的ASP.NET Core app请求委托,处理所有的请求。此案例不包含实际的请求管道。取而代之的是一个针对所以HTTP请求的匿名函数(该函数每次请求都会调用)。

C#
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.Use将多个请求委托链接在一起。参数 next 表示管道的下一个委托。 (请记住,你可以通过不调用下一个参数使管道短路)  通常可以在下一个委托之前和之后执行操作,就行下面演示的:

C#
public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.Use(async (context, next) =>
        {
            // Do work that doesn't write to the Response.
            await next.Invoke();
            // Do logging or other work that doesn't write to the Response.
        });

        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello from 2nd delegate.");
        });
    }
}

警告:

不要在response发送客户端后调用 下一个委托(next)。在应答(response)已经开始后改变HttpResponse会抛出异常。例如,设置 headers、状态码等待,都将抛出异常。 调用next后写入 response body :

  • 可能导致协议违例(非法)。例如,超过规定的内容长度。
  • .可能破坏身体格式。例如,将HTML页脚写入CSS文件。

HttpResponse.HasStarted 是一个有用的标识,用来指示 headers是否发送或者body是否被写入。

排序

中间件组件在Configure方法中添加的顺序决定它们在请求时被调用的顺序,以及响应返回的顺序。这种排序对于安全性、性能和功能是至关重要的。

(下面示例展示)Configure方法添加了以下几个中间件组件:

  1. Exception/error handling(异常/错误处理)
  2. Static file server(静态文件服务器)
  3. Authentication(身份验证)
  4. MVC
C#( ASP.NET Core 2x
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下的文件,都是公有可用的。静态文件的使用请参考Working with static files



如果请求不被静态文件中间件处理,它将被传递到身份验证中间件(app.useauthentication),进行认证。 身份认证不短路未经身份验证的请求。 虽然请求要进行身份验证 ,但只有在MVC选择具体的Razor页或controller和action时,才会发生授权 (和拒绝)。

下面的示例演示中间件顺序,在响应压缩中间件之前,静态文件中间件处理静态文件请求。 使用这种中间件顺序,静态文件不会被压缩。 来自UseMvcWithDefaultRoute 的MVC responses 能被压缩.

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

Use, Run, and Map

使用Use,Run, 和Map配置HTTP管道。Use方法可以使管道短路(也就是说,它不调用下一个请求委托)。Run 是一个约定, 一些中间件组件可能会暴露他们自己的 Run[Middleware] 方法,而这些方法只能在管道末尾处运行。

Map* 作为惯例,将管道分流。Map 根据给定请求路径匹配将请求管道分流。如果请求路径以指定路径开始,则执行分支。

C#
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:1234的应请求和响应:

RequestResponse
localhost:1234Hello from non-Map delegate.
localhost:1234/map1Map Test 1
localhost:1234/map2Map Test 2
localhost:1234/map3Hello from non-Map delegate.

使用 Map ,会使每个匹配路径段 匹配的请求从HttpRequest.Path 中删除并添加的HttpRequest.PathBase.



MapWhen 基于给定的谓词分支请求管道。任何 使Func<HttpContext, bool> 返回true的谓语的请求都被求映射到新的管道分支。下面的示例用于检测query字符串变量branch的存在:

C#
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:1234的应请求和响应:

RequestResponse
localhost:1234Hello from non-Map delegate.
localhost:1234/?branch=masterBranch used = master

Map支持嵌套,例如:

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

也可以同时匹配多个段,例如:

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


内置中间件

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

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

编写中间件

中间件一般封装在类中,并用扩展方法公开。 观察下面的中间件,他通过当前请求query字符串来设置culture变量(System.Globalization.CultureInfo: 提供有关特定区域性的信息):

C#
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}");
        });

    }
}

注意:上面的示例代码用于演示创建中间件组件。关于ASP.NET Core 内置的本地化支持,请参阅Globalization and localization

中文乱报解决:

                //以下2行代码选其一即可让浏览器用utf8来解析数据
                context.Response.ContentType = "text/html;charset=UTF-8";
                context.Response.Headers.Add("Content-type", "text/html;charset=UTF-8");


你可以通过在请求中传递culture参数来测试上面的中间件, 例如,http://localhost:7997/?culture=no

下面的代码将中间件的委托转移到类中:

C#
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公开中间件:

C#
using Microsoft.AspNetCore.Builder;

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

下面的代码从Configure方法调用中间件:

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

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

    }
}

中间件应遵循显式依赖关系原则,在其构造函数公开其依赖项。每个应用程序的生命周期中间件都被构建。如果你需要在一次请求中与中间件共享服务,请参考下面每个请求的依赖关系

中间件组件可以通过构造函数参数来解析依赖注入的依赖关系。UseMiddleware<T> 也可以直接接受额外的参数.

每个请求的依赖关系

由于中间件是在APP启动构建而不是每次请求(构建),在每次请求过程中,中间件构造函数使用的作用域生命周期服务不能和其他依赖注入类型共享。 如果必须在中间件和其他类型之间共享作用域服务,可以将这些服务添加到Invoke方法的签名中。 Invoke 方法能够接受依赖注入填充的附加参数。 例如:

c#
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);
    }
}

资源


水平有限,错漏之处望大家批评指正。


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值