ASP.NET Core 3.1 中间件 一、ASP.NET Core 中间件简介 二、写入自定义 ASP.NET Core 中间件 三、基于工厂的中间件(ASP.NET Core 中基于工厂的中间件激活) 四、使用 ASP.NET Core 中的第三方容器激活中间件(待续)

参考微软官方文档 :

https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/?view=aspnetcore-3.1

1.1 定义:中间件是一种装配到应用管道以处理请求和响应的软件。

每个组件:

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

请求委托处理每个 HTTP 请求。

当中间件短路时,它被称为“终端中间件”,因为它阻止中间件进一步处理请求。

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

1.2 使用 IApplicationBuilder 创建中间件管道

沿黑色箭头执行。

ASP.NET Core 3.1 中间件
一、ASP.NET Core 中间件简介
二、写入自定义 ASP.NET Core 中间件
三、基于工厂的中间件(ASP.NET Core 中基于工厂的中间件激活)
四、使用 ASP.NET Core 中的第三方容器激活中间件(待续)

应尽早在管道中调用异常处理委托,这样它们就能捕获在管道的后期阶段发生的异常。

调用单个匿名函数以响应每个 HTTP 请求。

 

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

通常可在下一个委托前后执行操作,如以下示例所示:

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

不过,请参阅下面有关尝试对已发送的响应执行写入操作的警告。

 警告

调用 next 后写入响应正文:

  • 例如,写入的长度超过规定的 Content-Length
  • 例如,向 CSS 文件中写入 HTML 页脚。

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

某些中间件组件可能会公开在管道末尾运行的 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 委托之后添加了另一个 Use 或 Run 委托,则不会调用该委托。

中间件顺序

你可以完全控制如何重新排列现有中间件,或根据场景需要注入新的自定义中间件。

ASP.NET Core 3.1 中间件
一、ASP.NET Core 中间件简介
二、写入自定义 ASP.NET Core 中间件
三、基于工厂的中间件(ASP.NET Core 中基于工厂的中间件激活)
四、使用 ASP.NET Core 中的第三方容器激活中间件(待续)

上图中的“终结点”中间件为相应的应用类型(MVC 或 Razor Pages)执行筛选器管道。

ASP.NET Core 3.1 中间件
一、ASP.NET Core 中间件简介
二、写入自定义 ASP.NET Core 中间件
三、基于工厂的中间件(ASP.NET Core 中基于工厂的中间件激活)
四、使用 ASP.NET Core 中的第三方容器激活中间件(待续)

此顺序对于安全性、性能和功能至关重要。

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

在上述代码中:

  • 单个用户帐户创建新的 Web 应用时未添加的中间件已被注释掉。
  • 例如:
    • UseCorsUseAuthentication 和 UseAuthorization 必须按照上述顺序运行。
    • 此错误,UseCors 当前必须在 UseResponseCaching 之前运行。

例如:

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

使用前面的代码,可以通过缓存压缩的响应来保存 CPU,但你可能最终会使用不同的压缩算法(如 gzip 或 brotli)来缓存资源的多个表示形式。

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

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

以下 Startup.Configure 方法将为常见应用方案添加中间件组件:

  1. 异常/错误处理
    • 当应用在开发环境中运行时:
      • UseDeveloperExceptionPage) 报告应用运行时错误。
      • 数据库错误页中间件报告数据库运行时错误。
    • 当应用在生产环境中运行时:
      • UseExceptionHandler) 捕获以下中间件中引发的异常。
      • UseHsts) 添加 Strict-Transport-Security 标头。
  2. UseHttpsRedirection) 将 HTTP 请求重定向到 HTTPS。
  3. UseStaticFiles) 返回静态文件,并简化进一步请求处理。
  4. UseCookiePolicy) 使应用符合欧盟一般数据保护条例 (GDPR) 规定。
  5. UseRouting)。
  6. UseAuthentication) 尝试对用户进行身份验证,然后才会允许用户访问安全资源。
  7. UseAuthorization)。
  8. 如果应用使用会话状态,请在 Cookie 策略中间件之后和 MVC 中间件之前调用会话中间件。
  9. UseEndpoints)。
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();
    });
}

IApplicationBuilder 上公开。

因此,异常处理程序中间件可捕获稍后调用中发生的任何异常。

ASP.NET Core 中的静态文件。

虽然身份验证中间件对请求进行身份验证,但仅在 MVC 选择特定 Razor Page 或 MVC 控制器和操作后,才发生授权(和拒绝)。

可以压缩 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();
    });
}

SPA 中间件处于最后的作用是:

  • 允许所有其他中间件首先响应匹配的请求。
  • 允许具有客户端侧路由的 SPA 针对服务器应用无法识别的所有路由运行。

Angular 项目模板的指南。

转接头中间件顺序

转接头中间件顺序。

对中间件管道进行分支

如果请求路径以给定路径开头,则执行分支。

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

对中间件管道进行分支
请求 响应
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 中删除匹配的路径段,并针对每个请求将该路径段追加到 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.");
        });
    }
}

在以下示例中,谓词用于检测查询字符串变量 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 的请求和响应:

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

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

如果请求中包含查询字符串变量 branch,则在重新加入主管道之前会记录其值。

内置中间件

使用 IApplicationBuilder 创建中间件管道部分。

内置中间件
中间件 描述 顺序
身份验证 提供身份验证支持。 OAuth 回叫的终端。
授权 提供身份验证支持。 紧接在身份验证中间件之后。
Cookie 策略 跟踪用户是否同意存储个人信息,并强制实施 cookie 字段(如 secure 和 SameSite)的最低标准。 示例:身份验证、会话、MVC (TempData)。
CORS 配置跨域资源共享。 此错误,UseCors 当前必须在 UseResponseCaching 之前运行。
诊断 提供新应用的开发人员异常页、异常处理、状态代码页和默认网页的几个单独的中间件。 异常终端或为新应用提供默认网页的终端。
转接头 将代理标头转发到当前请求。 示例:方案、主机、客户端 IP、方法。
运行状况检查 检查 ASP.NET Core 应用及其依赖项的运行状况,如检查数据库可用性。 如果请求与运行状况检查终结点匹配,则为终端。
标头传播 将 HTTP 标头从传入的请求传播到传出的 HTTP 客户端请求中。  
HTTP 方法重写 允许传入 POST 请求重写方法。 在使用已更新方法的组件之前。
HTTPS 重定向 将所有 HTTP 请求重定向到 HTTPS。 在使用 URL 的组件之前。
HTTP 严格传输安全性 (HSTS) 添加特殊响应标头的安全增强中间件。 示例:转接头、URL 重写。
MVC 用 MVC/Razor Pages 处理请求。 如果请求与路由匹配,则为终端。
OWIN 与基于 OWIN 的应用、服务器和中间件进行互操作。 如果 OWIN 中间件处理完请求,则为终端。
响应缓存 提供对缓存响应的支持。 UseCORS 必须在 UseResponseCaching 之前。
响应压缩 提供对压缩响应的支持。 在需要压缩的组件之前。
请求本地化 提供本地化支持。 在对本地化敏感的组件之前。
终结点路由 定义和约束请求路由。 用于匹配路由的终端。
SPA 通过返回单页应用程序 (SPA) 的默认页面,在中间件链中处理来自这个点的所有请求 在链中处于靠后位置,因此其他服务于静态文件、MVC 操作等内容的中间件占据优先位置。
会话 提供对管理用户会话的支持。 在需要会话的组件之前。
静态文件 为提供静态文件和目录浏览提供支持。 如果请求与文件匹配,则为终端。
URL 重写 提供对重写 URL 和重定向请求的支持。 在使用 URL 的组件之前。
WebSockets 启用 WebSockets 协议。 在接受 WebSocket 请求所需的组件之前。

二、写入自定义 ASP.NET Core 中间件

首先创建一个ASP.NET Core 3.1 的WebApi 应用程序 MiddlewareDemo的项目

中间件类

通常,中间件封装在类中,并且通过扩展方法公开,在Startup类中Configure方法如下: 如果中间件比较简单可以直接下面使用 app.Use 或者app.Run 

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseHttpsRedirection();

    app.UseRouting();

    app.UseAuthorization();
    app.Use(async (context, next) =>
    {
       var cultureQuery = context.Request.Query["culture"];
       if (!string.IsNullOrWhiteSpace(cultureQuery))
       {
            await context.Response.WriteAsync($"{cultureQuery} 	 ");
       }//调用管道中的下一个委托/中间件
       await next();
   });
   app.Run(async(context)=> {
          await context.Response.WriteAsync("Hello, World!");
   });


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

运行项目首先会在页面显示 Hello, World! ,然后在当前路径后面加入?culture=Dean  页面会显示 Dean    Hello, World! 

当中间件的业务比较复杂,代码量很大的时候,通常要中间件将委托移动到类:

1、新建一个类RequestCultureMiddleware 代码如下:

using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;

namespace MiddlewareDemo
{
    public class RequestCultureMiddleware
    {
        private readonly RequestDelegate _next;
        public RequestCultureMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task InvokeAsync(HttpContext context)
        {
            var cultureQuery = context.Request.Query["culture"];
            if (!string.IsNullOrWhiteSpace(cultureQuery))
            {
                await context.Response.WriteAsync($"{cultureQuery} 	 ");
                //var culture = new CultureInfo(cultureQuery);
                //CultureInfo.CurrentCulture = culture;
                //CultureInfo.CurrentUICulture = culture;
            }

            // Call the next delegate/middleware in the pipeline
            await _next(context);
        }
    }
}

注意必须包括中间件类:

  • RequestDelegate 的参数的公共构造函数。
  • 此方法必须:
    • 返回 Task
    • HttpContext 的第一个参数。

依赖关系注入 (DI) 填充。

2、 把在上面的Configure方法里的app.Use() 里的中间件注释掉 然后改成 

 app.UseMiddleware<RequestCultureMiddleware>();

运行后,结果不变

中间件依赖项

在每个应用程序生存期构造一次中间件。

UseMiddleware<T> 也可直接接受其他参数。

按请求中间件依赖项

Invoke 方法可接受由 DI 填充的其他参数:

1、新建一个类代码如下:

 1 #region 按请求中间件依赖项
 2 
 3     public class GetValue
 4     {
 5         public static int Value { get; set; }
 6     }
 7 
 8     public interface IMyScopedService
 9     {
10 
11         void SetMyProperty(int value);
12 
13     }
14 
15     public class MyScopedService : IMyScopedService
16     {
17         public void SetMyProperty(int value)
18         {
19             GetValue.Value = value;
20         }
21     }
22 
23     public class CustomMiddleware
24     {
25         private readonly RequestDelegate _next;
26 
27         public CustomMiddleware(RequestDelegate next)
28         {
29             _next = next;
30         }
31 
32         // IMyScopedService is injected into Invoke
33         public async Task Invoke(HttpContext httpContext, IMyScopedService svc)
34         {
35             svc.SetMyProperty(1000);
36             string str = $" GetValue.Value : { GetValue.Value } 	";
37             await httpContext.Response.WriteAsync(str, Encoding.UTF8);
38             await _next(httpContext);
39         }
40     }
41 
42 
43     #endregion
View Code

2、在Startup类Configure方法注释掉  app.UseMiddleware<RequestCultureMiddleware>(); 

3、在注释掉  app.UseMiddleware<RequestCultureMiddleware>();  下添加下面的

app.UseMiddleware<CustomMiddleware>();

4、运行后页面打印  

 GetValue.Value : 1000     Hello, World!

IApplicationBuilder 公开中间件:

using Microsoft.AspNetCore.Builder;

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

以下代码通过 Startup.Configure 调用中间件:

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

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

https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/extensibility?view=aspnetcore-3.1

中间件激活的扩展点。

作用域或瞬态服务。

优点:

  • 按客户端请求(作用域服务的注入)激活
  • 让中间件强类型化

IMiddleware 按客户端请求(连接)激活,因此作用域服务可以注入到中间件的构造函数中。

IMiddleware

Task。

3.1使用约定激活的中间件:

 
 1 using Microsoft.AspNetCore.Http;
 2 using System;
 3 using System.Collections.Generic;
 4 using System.Linq;
 5 using System.Threading.Tasks;
 6 
 7 namespace MiddlewareDemo
 8 {
 9     public class ConventionalMiddleware
10     {
11         private readonly RequestDelegate _next;
12 
13         public ConventionalMiddleware(RequestDelegate next)
14         {
15             _next = next;
16         }
17 
18         public async Task InvokeAsync(HttpContext context, AppDbContext db)
19         {
20             var keyValue = context.Request.Query["key"];
21 
22             if (!string.IsNullOrWhiteSpace(keyValue))
23             {
24                 db.Add(new Request()
25                 {
26                     DT = DateTime.UtcNow,
27                     MiddlewareActivation = "ConventionalMiddleware",
28                     Value = keyValue
29                 });
30                 await context.Response.WriteAsync($"ConventionalMiddleware count : {DB.DbList.Count.ToString()} 	");
31                 //await db.SaveChangesAsync();
32             }
33 
34             await _next(context);
35         }
36     }
37    
38 
39 }
View Code

3.2使用 MiddlewareFactory 激活的中间件:  

 

 1 using Microsoft.AspNetCore.Http;
 2 using System;
 3 using System.Collections.Generic;
 4 using System.Linq;
 5 using System.Threading.Tasks;
 6 
 7 namespace MiddlewareDemo
 8 {
 9     public class FactoryActivatedMiddleware : IMiddleware
10     {
11         private readonly AppDbContext _db;
12 
13         public FactoryActivatedMiddleware(AppDbContext db)
14         {
15             _db = db;
16         }
17 
18         public async Task InvokeAsync(HttpContext context, RequestDelegate next)
19         {
20             var keyValue = context.Request.Query["key"];
21 
22             if (!string.IsNullOrWhiteSpace(keyValue))
23             {
24                 _db.Add(new Request()
25                 {
26                     DT = DateTime.UtcNow,
27                     MiddlewareActivation = "FactoryActivatedMiddleware",
28                     Value = keyValue
29                 });
30 
31                await context.Response.WriteAsync($"FactoryActivatedMiddleware count : {DB.DbList.Count.ToString()} 	");
32                 //await _db.SaveChangesAsync();
33             }
34 
35             await next(context);
36         }
37     }
38 }
View Code

3.3创建AppDbContext类,代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace MiddlewareDemo
{
    public class AppDbContext
    {
        
        public void Add(Request request)
        {
            DB.DbList.Add(request);
        }
    }

    public class DB
    {
        public static List<Request> DbList = new List<Request>();
    }
}

添加Request类

  public class Request
    {
        public DateTime DT { get; set; }
        public string MiddlewareActivation { get; set; }

        public string Value { get; set; }

    }

3.4创建一个IApplicationBuilder的扩展方法MiddlewareExtensions,代码如下:

 1 using Microsoft.AspNetCore.Builder;
 2 using System;
 3 using System.Collections.Generic;
 4 using System.Linq;
 5 using System.Threading.Tasks;
 6 
 7 namespace MiddlewareDemo
 8 {
 9     public static class MiddlewareExtensions
10     {
11         public static IApplicationBuilder UseConventionalMiddleware(
12        this IApplicationBuilder builder)
13         {
14             return builder.UseMiddleware<ConventionalMiddleware>();
15         }
16 
17         public static IApplicationBuilder UseFactoryActivatedMiddleware(
18             this IApplicationBuilder builder)
19         {
20             return builder.UseMiddleware<FactoryActivatedMiddleware>();
21         }
22 
23         //    public static IApplicationBuilder UseFactoryActivatedMiddleware(
24         //this IApplicationBuilder builder, bool option)
25         //    {
26         //        // Passing 'option' as an argument throws a NotSupportedException at runtime.
27         //        return builder.UseMiddleware<FactoryActivatedMiddleware>(option);
28         //    }
29     }
30 }
View Code

3.5在Startup类ConfigureServices方法中注入下面的

  services.AddScoped<AppDbContext>();
  services.AddTransient<FactoryActivatedMiddleware>();

3.6在Startup类Configure方法中添加下面的

 #region 基于工厂的中间件
            app.UseConventionalMiddleware();
            app.UseFactoryActivatedMiddleware();
            //app.UseFactoryActivatedMiddleware(false);
 #endregion

运行程序,在程序后面加入参数 例如:https://localhost:44349/weatherforecast?key=p

结果为: web页面显示 

ConventionalMiddleware count : 1     FactoryActivatedMiddleware count : 2     Hello, World!

 代码地址:https://github.com/hudean/MiddlewareDemo.git

四、使用 ASP.NET Core 中的第三方容器激活中间件(待续)