ASP.NET Core 托管和部署

ASP.NET Core 托管和部署

翻译自 https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-5.0

中间件是集成到应用程序通道用来处理请求和返回的软件。每一个组件:

  • 决定是否在管道中传递请求到下一个组件
  • 可以在管道中在下一个组件之前和之后执行工作

请求代理用来建立请求管道。请求代理处理每一个 HTTP 请求。

请求代理使用 RunMap 和 Use 的扩展方法配置。私有请求代理可以通过匿名方法(叫做行内中间件)在行内指定,或者可以定义在一个重用的类中。这些重用的类和行内匿名方法时中间件,也称作中间件组件。每一个请求管道中的中间件组件负责调用下一个组件或者结束管道。当一个中间件短路的时候,它会调用一个终结中间件,因为它阻止了处理请求。

Migrate HTTP handlers and modules to ASP.NET Core middleware 中解释了 ASP.NET Core 和 ASP.NET 4.x 中的请求管道的不同,并且提供了额外的中间件示例。

使用 IApplicationBuilder 创建管道中间件

ASP.NET Core 请求管道由一组一个接一个的请求代理组成。下面的图标展示了这中概念。执行的线程跟随返回的箭头。

翻译 - ASP.NET Core 基本知识 - 中间件(Middleware)-LMLPHP

每一个代理都可以在下一个代理之前和之后执行操作。异常代理应在管道调用较早调用,这样就可以在管道的早期阶段捕获出现的异常。

最简单的 ASP.NET Core 应用程序可能只设置一个请求单利去处理所有的请求。这种情况不包含一个真实的请求管道。相反的,一个匿名方法被调用响应每一个 HTTP 请求。

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

使用 Use 把多个请求代理链接起来。next 参数代表管道中的下一个代理。你可以通过不调用 next 参数使管道短路。也可以像通常一样在下一个代理之前和之后执行一些操作,就像下面示例展示的一样:

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

当一个代理不把请求传递到下一个代理的情况,这叫做请求代理短路。短路经常有需求因为它可以避免不必要的工作。例如 Static File Middleware 会通过处理一个静态文件请求表现为一个终结中间件,然后短路管道的剩余部分。在中间件之前添加到管道中的中间件终结了之后的处理过程,但是它仍然处理它的 next.Invoke 语句之后的代码。然而,注意下面关于试图往已经发送出去的响应里写数据的警告。

⚠️  警告

不要在响应已经发送给客户端之后调用 next.Invoke。在响应已经开始后更改 HttpResponse 会抛出异常。例如,设置头部和状态码就会抛出异常 setting headers and a status code throw an exception。在调用 next 之后往响应体中写入数据会有以下影响:

  • 可能会违反协议。例如,比声明的 Content-Length 写入更多的数据
  • 可能会破坏 body 内容格式。例如,往 CSS 文件中写入一个 HTML footer

HasStarted 是一个很有用,用来示意头部是否已经被发送或者 body 是否已经被写入。

Run 代理不接收 next 参数。第一个 Run 代理总是终点,结束管道。Run 是一个约定。一些中间件组件可能使用 Run[Middleware] 方法运行在管道的终点:

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

在上面这个例子中,Run 代理往响应中写入 "Hello from 2nd delegate",然后结束了管道。如果另外一个 Use 或者 Run 代理在 Run 代理之后添加,它不会被调用。

中间件顺序

下面的图显示了 ASP.NET Core MVC 和 Razor Pages 应用程序请求处理管道的全部。你可以看到,在一个典型的应用程序中,现存的中间件是怎么排序的,以及自定义的中间件被添加到哪里。你能够完全控制如何重新排列现存的中间件或者根据你的应用场景按需注入新的自定义的中间件。

翻译 - ASP.NET Core 基本知识 - 中间件(Middleware)-LMLPHP

上图中的 Endpoint 中间件执行对应应用程序类型 - MVC 或者 Razor 管道过滤。

MVC Endpoint

中间件组件在 Startup.Configure 方法中被添加顺序决定了中间件组件在请求中被调用的顺序和响应中相反的顺序。顺序在安全,性能和功能方面至关重要。

下面的 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?}");
    });
}

在上面的代码中:

  • 当使用 individual users accounts 创建一个新的 web 应用程序时,没有被添加的中间件被注释掉了
  • 不是每一个中间件都需要按照这个精确的顺序执行,但是很多都是。例如
    UseCors, UseAuthentication, 和 UseAuthorization 必须按照展示的顺序执行
    UseCors 因为这个bug (this bug) 必须在 UseResponseCaching 之前

在一些情境中,中间件会有不同的顺序。例如,caching  和 compression 的顺序根据情境而定,可以有多种有效的顺序。例如:

app.UseResponseCaching();
app.UseResponseCompression();

上面的代码,可以通过缓存压缩的响应节省 CPU 的开销,但是你最终可能会使用多种不同的压缩算法缓存资源的多个表示形式,例如 gzip 或者 brotli。

下面的顺序结合了静态文件允许缓存压缩静态文件:

app.UseResponseCaching();
app.UseResponseCompression();
app.UseStaticFiles();

下面的 Startup.Configure 方法添加常见应用程序场景的中间件组件:

1. 异常/错误处理

   当应用程序运行在 Development environment:

       Developer Exception Page Middleware (UseDeveloperExceptionPage) 报告应用程序运行时错误

       Database Error Page Middleware 报告数据库运行时错误

    当应用程序运行在 Production environment:

       Exception Handler Middleware (UseExceptionHandler) 捕获下列中间件抛出的异常

       HTTP Strict Transport Security Protocol (HSTS) Middleware (UseHsts) 添加 Strict-Transport-Security 头部

2. HTTPS Redirection Middleware (UseHttpsRedirection) 重定向 HTTP 请求到 HTTPS

3. Static File Middleware (UseStaticFiles) 返回静态文件,短路更远的请求处理

4. Cookie Policy Middleware (UseCookiePolicy) 使得应用程序符合 EU General Data Protection Regulation (GDPR) 法规

5. Routing Middleware (UseRouting) 路由请求

6. Authentication Middleware (UseAuthentication) 试图在用户被允许访问安全资源之前对他们认证

7. Authorization Middleware (UseAuthorization) 授权一个用户访问安全资源

8. Session Middleware (UseSession) 建立和维护回话状态。如果应用程序使用会话状态,在 Cookie Policy Middleware 之后,MVC Middleware 之前调用 Session Middleware。

9. Endpoint Routing Middleware (UseEndpoints with MapRazorPages) 添加 Razor Pages endpoint 到请求管道。

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.UseAuthentication();
    app.UseAuthorization();
    app.UseSession();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

在前面的示例代码中,每一个中间件扩展方法通过命名空间 Microsoft.AspNetCore.Builder 暴露在 IApplicationBuilder 上。

UseExceptionHandler 是第一个添加到管道中的中间件组件。因此,Exception Handler Middleware 会捕获在之后调用中出现的任何异常。

Static File Middleware 在管道中调用的较早,因此它可以处理请求并且短路而不用执行剩余的组件。Static File Middleware 没有提供授权检查。任何由 Static File Middleware 提供的文件,包含那些 wwwroot 下面的文件,都是公开可用的。一种保护静态文件的方法,请查看 Static files in ASP.NET Core

如果 Static File Middleware 中间件没有处理请求,请求将会传递到 Authentication Middleware (UseAuthentication) 认证中间件认证。尽管 Authentication Middleware 中间件认证请求,但是授权(拒绝)只有在 MVC 选择了一个特定的 Razor Page 或者 MVC 控制器和方法后才会发生。

下面的示例展示了 Static File Middleware 处理静态文件请求在 Response Compression Middleware 之前执行的中间件顺序。在这种中间件顺序下,静态文件不会被压缩。Razor Pages 的响应会被压缩。

public void Configure(IApplicationBuilder app)
{
    // Static files aren't compressed by Static File Middleware.
    app.UseStaticFiles();

    app.UseRouting();

    app.UseResponseCompression();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

对于单页应用程序 (SPAs),SPA 中间件 UseSpaStaticFiles 通常处在中间件管道的最末端。SPA 中间件出现在最后:

  • 允许所有其它中间件首先响应请求匹配
  • 允许带有客户端路由的 SPAs 运行所有没有被服务端应用程序识别的路由

更多关于 SPAs 详细信息,查看 React 和 Angular 工程模板指南。

Forwarded Headers Middleware 顺序

Forwarded Headers Middleware 应在在其它中间件之前运行。这种顺序保证了其它依赖于 forwarded headers 信息的中间件可以使用 header values。在诊断和错误处理中间件之后运行 Forwarded Headers Middleware,请查看 Forwarded Headers Middleware order

分支中间件管道

约定使用 Map 扩展来分支管道。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:1234 的请求和响应。

当使用 Map 的时候,每一个请求匹配的路径分段从 HttpRequest.Path 中移除,附加到 HttpRequest.PathBase 上。

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> 的谓词可以被用来映射请求到管道的一个新的分支。在下面的例子中,谓词被用来检测查询字符串中变量 branch:

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

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,它的值会在主管道重新加入之前被输出。

内置中间件

ASP.NET Core 带有下面的中间件组件。Order 列备注了关于中间件在请求处理管道中放置的位置和在什么情况下中间件可能会结束请求处理。当一个中间件短路了请求处理管道,阻止了之后的下行中间件处理请求,它被称为终端中间件。更多关于短路的信息,查看 Create a middleware pipeline with IApplicationBuilder 部分。

01-22 08:14