ASP.NET Core中间件

ASP.NET Core中间件

中间件(Middleware)是装配到应用程序管道中以处理请求和响应的组件。

在ASP.NET Core中通常在Startup类的Configure方法中配置中间件,它接受一个IApplicationBuilder类型的参数。IApplicationBuilder使用三扩展方法——RunMapUse来配置中间件。

中间件可以是一个匿名方法,也可以是一个可重用的类。下面的例子使用一个匿名中间件处理所有请求。

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

使用Use扩展方法,可以链接多个中间件,形处理管道。

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 middle ware.");
        });
    }
}

每个中间件都可以决定以下两个问题:

  • 选择是否将请求传递给管道中的下一个组件;
  • 能在管道中的下一个组件之前和之后执行一些工作;

next参数代表管道中的下一个中间件。您可以不调用next方法使管道中的中间件短路,不在执行后续的中单间件。也可以添加在下一个中间件执行前和执行后执行的逻辑。中间件依次调用形成了ASP.NET Core的请求处理管道。

ASP.NET Core请求管道

异常处理中间件通常是最先调用的中间件,这样才能捕获管道中后续执行的中间件发生的异常。

当中间件不将请求传递给下一个中间件时,被称为短路。短路设计的初衷是为了避免不必要的工作。例如,静态文件中间件处理完静态文件然后短路,不再调用之后的中间件,被称为未端中间件

Use扩展方法不同的是,Run扩展方法不接受next参数,第一个Run中间件就是管道中最后一个中间件。在它之后调用的RunUse扩展方法不会被 执行。Run是一种约定,暴露Run[Middleware]方法的中间件一般都在管道的中末端执行。

中间件的执行顺序

下图显示了 ASP.NET Core MVC 和 Razor Pages 应用程序的完整请求处理管道。 您可以看到在一个典型的应用程序中,现有的中间件是如何排序的,以及自定义中间件的添加位置。 您可以完全控制如何根据您的场景对现有中间件重新排序或注入新的自定义中间件。

middleware-pipeline

MVC 或 Razor Pages应用程序中,过滤器管道在上图中的 Endpoint 中间件的位置执行。

mvc-endpoint

中间件在管道中被调用的顺序对功能、性能和安全是至关重要的。

以下Startup.Configure方法的中间件组件顺序是推荐的顺序:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseDatabaseErrorPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();
    // app.UseCookiePolicy();

    app.UseRouting();
    // app.UseRequestLocalization();
    // app.UseCors();

    app.UseAuthentication();
    app.UseAuthorization();
    // app.UseSession();
    // app.UseResponseCompression();
    // app.UseResponseCaching();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");
    });
}

中间件管道的分支

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

使用上面的代码,将得到如下所示的请求和响应的对应关系

| 请求 | 响应 | | 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支持嵌套:

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

Map也支持多段路径:

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

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

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

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

使用上面的代码,将得到如下所示的请求和响应的对应关系

| 请求 | 响应 | | localhost:1234 | Hello from non-Map delegate. | | localhost:1234/?branch=main | Branch used = main |

UseWhen也根据给定断言的结果对请求管道进行分支。 与MapWhen不同,如果此分支不短路或不包含终端中间件,则会重新加入主管道:

public class Startup
{
    private void HandleBranchAndRejoin(IApplicationBuilder app, ILogger<Startup> logger)
    {
        app.Use(async (context, next) =>
        {
            var branchVer = context.Request.Query["branch"];
            logger.LogInformation("Branch used = {branchVer}", branchVer);

            // Do work that doesn't write to the Response.
            await next();
            // Do other work that doesn't write to the Response.
        });
    }

    public void Configure(IApplicationBuilder app, ILogger<Startup> logger)
    {
        app.UseWhen(context => context.Request.Query.ContainsKey("branch"),
                               appBuilder => HandleBranchAndRejoin(appBuilder, logger));

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

在前面的示例中,所有请求都会在响应中写入响应“Hello from main pipeline.”。 如果请求包含查询字符串变量branch,则在重新加入主管道之前在日志中写入其值。