ASP.NET Core 中间件

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

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

请求委托用于构建请求管道。请求委托处理每个 HTTP 请求。

请求委托是使用Run、Map和Use扩展方法配置的。单个请求委托可以内联指定为匿名方法(称为内联中间件),也可以在可重用类中定义。这些可重用的类和内联匿名方法就是中间件,也称为中间件组件。请求管道中的每个中间件组件负责调用管道中的下一个组件或短路管道。当中间件短路时,它被称为终端中间件,因为它会阻止其他中间件处理请求。

将 HTTP 处理程序和模块迁移到 ASP.NET Core 中间件解释了 ASP.NET Core 和 ASP.NET 4.x 中请求管道之间的差异,并提供了其他中间件示例。

中间件代码分析

ASP.NET Core 包含许多编译器平台分析器,用于检查应用程序代码的质量。有关详细信息,请参阅ASP.NET Core 应用中的代码分析

创建中间件管道WebApplication

ASP.NET Core 请求管道由一系列请求委托组成,依次调用。下图演示了这个概念。执行线程遵循黑色箭头。

ASP.NET Core 中间件_第1张图片

每个委托可以在下一个委托之前和之后执行操作。异常处理委托应在管道的早期调用,以便它们可以捕获管道后期发生的异常。

最简单的 ASP.NET Core 应用程序设置一个处理所有请求的请求委托。这种情况不包括实际的请求管道。相反,会调用单个匿名函数来响应每个 HTTP 请求。

C#复制
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

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

app.Run();

使用 Use将多个请求委托链接在一起。该next参数代表管道中的下一个委托。您可以通过不调用参数来短路管道next。您通常可以在委托之前和之后执行操作next,如以下示例所示:

C#复制
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Use(async (context, next) =>
{
    // Do work that can 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.");
});

app.Run();

当一个委托不将请求传递给下一个委托时,称为短路请求管道。短路通常是可取的,因为它可以避免不必要的工作。例如,静态文件中间件可以通过处理静态文件的请求并短路管道的其余部分来充当终端中间件。在终止进一步处理的中间件之前添加到管道的中间件仍然处理其next.Invoke语句之后的代码。但是,请参阅以下有关尝试写入已发送的响应的警告。

 警告

next.Invoke响应发送给客户后请勿致电。响应开始后对HttpResponse的更改会引发异常。例如,设置标头和状态代码会引发异常。调用后写入响应正文next

  • 可能会导致违反协议。例如,写得比规定的多Content-Length
  • 可能会损坏正文格式。例如,将 HTML 页脚写入 CSS 文件。

HasStarted是一个有用的提示,用于指示标头是否已发送或正文是否已写入。

运行委托不接收next参数。第一个Run委托始终是终端并终止管道。Run是一个约定。某些中间件组件可能会公开Run[Middleware]在管道末尾运行的方法:

C#复制
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Use(async (context, next) =>
{
    // Do work that can 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.");
});

app.Run();

如果您希望将代码注释翻译成英语以外的语言,请在此 GitHub 讨论问题中告知我们。

在前面的示例中,Run委托写入"Hello from 2nd delegate."响应,然后终止管道。如果在委托之后添加另一个Use或委托,则不会调用它。RunRun

更喜欢 app.Use 需要将上下文传递给 next 的重载

非分配app.Use扩展方法:

  • 需要将上下文传递给next.
  • 保存使用其他重载时所需的两个内部每个请求分配。

有关更多信息,请参阅此 GitHub 问题。

中间件订单

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

ASP.NET Core 中间件_第2张图片

上图中的端点中间件执行相应应用程序类型(MVC 或 Razor Pages)的过滤器管道。

上图中的路由中间件显示在静态文件后面。这是项目模板通过显式调用app.UseRouting实现的顺序。如果不调用app.UseRouting路由中间件默认在管道的开头运行。有关详细信息,请参阅路由。

ASP.NET Core 中间件_第3张图片

中间件组件在文件中添加的顺序Program.cs定义了请求时调用中间件组件的顺序以及响应的相反顺序。该顺序对于安全性、性能和功能至关重要。

以下突出显示的代码Program.cs按典型推荐顺序添加了与安全相关的中间件组件:

C#复制
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebMiddleware.Data;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
    ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContext(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores();
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

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

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

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

app.MapRazorPages();
app.MapDefaultControllerRoute();

app.Run();

在前面的代码中:

  • 使用个人用户帐户创建新的 Web 应用程序时未添加的中间件将被注释掉。
  • 并非每个中间件都按照这个确切的顺序出现,但很多中间件都是如此。例如:
    • UseCorsUseAuthentication、 和UseAuthorization必须按所示顺序出现。
    • UseCors当前必须出现在 之前UseResponseCaching。此要求在GitHub 问题 dotnet/aspnetcore #23218中进行了解释。
    • UseRequestLocalization必须出现在任何可能检查请求区域性的中间件之前,例如app.UseStaticFiles().
    • UseRouting使用速率限制端点特定 API 后,必须调用UseRateLimiter 。例如,如果[EnableRateLimiting]使用该属性,则UseRateLimiter必须在 后调用UseRouting。仅调用全局限制器时,UseRateLimiter可以在 之前调用UseRouting

在某些场景下,中间件有不同的顺序。例如,缓存和压缩排序是特定于场景的,并且存在多个有效排序。例如:

C#复制
app.UseResponseCaching();
app.UseResponseCompression();

使用前面的代码,可以通过缓存压缩响应来减少 CPU 使用率,但最终可能会使用不同的压缩算法(例如 Gzip 或 Brotli)缓存资源的多种表示形式。

以下顺序组合静态文件以允许缓存压缩的静态文件:

C#复制
app.UseResponseCaching();
app.UseResponseCompression();
app.UseStaticFiles();

以下Program.cs代码为常见应用场景添加了中间件组件:

  1. 异常/错误处理
    • 当应用程序在开发环境中运行时:
      • 开发人员异常页面中间件 ( UseDeveloperExceptionPage ) 报告应用程序运行时错误。
      • 数据库错误页中间件 ( UseDatabaseErrorPage ) 报告数据库运行时错误。
    • 当应用程序在生产环境中运行时:
      • 异常处理程序中间件 ( UseExceptionHandler ) 捕获以下中间件中引发的异常。
      • HTTP 严格传输安全协议 (HSTS) 中间件 ( UseHsts ) 添加Strict-Transport-Security标头。
  2. HTTPS 重定向中间件 ( UseHttpsRedirection ) 将 HTTP 请求重定向到 HTTPS。
  3. 静态文件中间件 ( UseStaticFiles ) 返回静态文件并短路进一步的请求处理。
  4. Cookie 策略中间件 ( UseCookiePolicy ) 使应用程序符合欧盟通用数据保护条例 (GDPR) 的规定。
  5. 路由中间件 ( UseRouting ) 来路由请求。
  6. 身份验证中间件 ( UseAuthentication ) 在允许用户访问安全资源之前尝试对用户进行身份验证。
  7. 授权中间件 ( UseAuthorization ) 授权用户访问安全资源。
  8. 会话中间件(UseSession)建立并维护会话状态。如果应用程序使用会话状态,请在 Cookie 策略中间件之后、MVC 中间件之前调用会话中间件。
  9. 端点路由中间件(UseEndpoints和MapRazorPages)将 Razor Pages 端点添加到请求管道。
C#复制
if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
    app.UseDatabaseErrorPage();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseSession();
app.MapRazorPages();

在前面的示例代码中,每个中间件扩展方法都通过Microsoft.AspNetCore.Builder命名空间在WebApplicationBuilder上公开。

UseExceptionHandler是添加到管道中的第一个中间件组件。因此,异常处理程序中间件会捕获后续调用中发生的任何异常。

静态文件中间件在管道的早期被调用,以便它可以处理请求和短路,而无需通过其余组件。静态文件中间件提供授权检查。静态文件中间件提供的任何文件(包括wwwroot下的文件)都是公开可用的。有关保护静态文件的方法,请参阅ASP.NET Core 中的静态文件。

如果静态文件中间件未处理该请求,则会将其传递到身份验证中间件 ( UseAuthentication ),后者执行身份验证。身份验证不会短路未经身份验证的请求。尽管身份验证中间件对请求进行身份验证,但仅在 MVC 选择特定 Razor 页面或 MVC 控制器和操作后才会发生授权(和拒绝)。

以下示例演示了一个中间件顺序,其中静态文件请求先由静态文件中间件处理,然后再由响应压缩中间件处理。静态文件不会使用此中间件顺序进行压缩。Razor Pages 响应可以被压缩。

C#复制
// Static files aren't compressed by Static File Middleware.
app.UseStaticFiles();

app.UseRouting();

app.UseResponseCompression();

app.MapRazorPages();

有关单页应用程序的信息,请参阅React和Angular项目模板指南。

UseCors 和 UseStaticFiles 顺序

UseCors调用和 的顺序UseStaticFiles取决于应用程序。有关详细信息,请参阅UseCors 和 UseStaticFiles 顺序

转发标头中间件顺序

转发标头中间件应在其他中间件之前运行。此顺序确保依赖转发标头信息的中间件可以使用标头值进行处理。要在诊断和错误处理中间件之后运行转发标头中间件,请参阅转发标头中间件顺序。

中间件管道分支

映射扩展用作管道分支的约定。Map根据给定请求路径的匹配对请求管道进行分支。如果请求路径以给定路径开头,则执行分支。

C#复制
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Map("/map1", HandleMapTest1);

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

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

app.Run();

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

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

http://localhost:1234下表显示了使用上述代码的请求和响应。

要求 回复
本地主机:1234 非地图代表您好。
本地主机:1234/map1 地图测试1
本地主机:1234/map2 地图测试2
本地主机:1234/map3 非地图代表您好。

使用时,将为每个请求Map删除HttpRequest.Path或附加匹配的路径段。HttpRequest.PathBase

Map支持嵌套,例如:

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

Map也可以一次匹配多个段:

C#复制
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Map("/map1/seg1", HandleMultiSeg);

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

app.Run();

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

MapWhen根据给定谓词的结果对请求管道进行分支。任何类型的谓词Func都可用于将请求映射到管道的新分支。在以下示例中,谓词用于检测查询字符串变量是否存在branch

C#复制
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapWhen(context => context.Request.Query.ContainsKey("branch"), HandleBranch);

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

app.Run();

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

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

要求 回复
localhost:1234 Hello from non-Map delegate.
localhost:1234/?branch=main Branch used = main

UseWhen还根据给定谓词的结果对请求管道进行分支。与 with 不同MapWhen,如果该分支没有短路或包含终端中间件,则它会重新加入主管道:

C#复制
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.UseWhen(context => context.Request.Query.ContainsKey("branch"),
    appBuilder => HandleBranchAndRejoin(appBuilder));

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

app.Run();

void HandleBranchAndRejoin(IApplicationBuilder app)
{
    var logger = app.ApplicationServices.GetRequiredService>(); 

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

在前面的示例中,Hello from non-Map delegate.为所有请求写入响应。如果请求包含查询字符串变量branch,则在重新加入主管道之前记录其值。

你可能感兴趣的:(core,asp.net,中间件,后端)