随笔 - 18  文章 - 0 评论 - 3 trackbacks - 0

什么是中间件

对于中间件我们其实并不陌生,在.NET CORE出现之前中间件的概念在OWIN应用程序中就已经普遍使用了。
中间件官方定义: 中间件是一种集成到应用管道中间来处理请求和响应的模块,每个中间件可以:

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

ASP.NETCORE中的中间件本质上是一个请求委托 Func< RequestDelegate, RequestDelegate> middleware
RequestDelegate本身也是一个委托,定义为 public delegate Task RequestDelegate(HttpContext Context)
在ASP.NETCORE请求管道中,形成一条委托链。
委托链

请求管道短路:当委托不选择将请求传递到下一个委托时,称之为“短路”。

如何创建中间件

在ASP.NETCORE中,使用 IApplicationBuilder 来创建/插入中间件管道。提供了 RunUse 两类方式。依赖组件包 Microsoft.AspNetCore.Http.Abstractions
Run是一种 约定 的终端管道,即短路,不再执行下一个委托

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

		
        app.Run(async context => { await context.Response.WriteAsync("hello world 1"); });
		//这里不会执行到!!
		app.Run(async context => { await context.Response.WriteAsync("hello world 2"); });

    }

Use通常以扩展方法提供中间件,很适合处理一些AOP的事务。

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

        app.Use(async (context, next) =>
        {
            //可以在invoke之前做一些事
            await next.Invoke();
            //可以在invoke之后做一些事
        });

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

实际开发中我们通常需要自己定义中间件,有两种方式可以实现。

约定方式

public class RequestIdInRequestMiddleware
{
    private readonly RequestDelegate _next;

    public RequestIdInRequestMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public Task Invoke(HttpContext httpContext,IService service)
    {
		service.SayHello();
        //request head 加入requestid
        var requestId = Guid.NewGuid().ToString("n");
        httpContext.Request.Headers.Add("REQUESTID", requestId);

        return _next(httpContext);
    }
}

如上有以下约定:

  • 具有类型为 RequestDelegate 的参数公共构造函数
  • 名为 InvokeInvokeAsync 的公共方法,且此方法必须:
    • 返回 Task
    • 第一个参数为 HttpContext

目前官方是推荐使用约定方式, 注意:该方式加入管道中的生命周期为单例。也因此如果依赖一些Service,建议从InvokeInvokeAsync的方法参数注入,而不是从构造函数注入。(可以想想为什么?单例构造函数注入对Service的生命周期有要求~~)。

强类型

官方也提供了IMiddleware接口,用于扩展创建中间件。这种方式有两个优点:

  • 可以按需(生命周期)注入

  • 中间件强类型话,更易理解

      public class RequestIdInResponseMiddleware:IMiddleware
      {
          private readonly IService _service;
    
          public RequestIdInResponseMiddleware(IService service)
          {
              _service = service;
          }
    
          public Task InvokeAsync(HttpContext context, RequestDelegate next)
          {
              var requestId = Guid.NewGuid().ToString("n");
              context.Response.Headers.Add("REQUESTID", requestId);
    
              return next(context);
          }
      }
    

中间件加入管道

中间件一般都是基于IApplicationBuilder扩展方法加入管道。

public static class RequestIdMiddlewareExtensions
{
    public static IApplicationBuilder UseRequestIdInResponseMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<RequestIdInResponseMiddleware>();
    }
}

可以在 Configure 方法中调用加入 app.UseRequestIdInResponseMiddleware();
如果是 强类型 方式创建的Middleware,还需要在 ConfigureServices 中注册 services.AddSingleton<RequestIdInResponseMiddleware>();

中间件的顺序

中间件显著受加入的顺序影响,官方提供的默认中间件顺序图
中间件顺序

中间件分支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>");
        });
    }
}

根据请求会响应不同结果

请求 响应
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.

另外还可以使用 UseWhen 创建管道分支,只有匹配一定条件才会短路管道。

public void Configure(IApplicationBuilder app)
{
	//只有请求url包含查询字符串变量 branch,才会短路管道
    app.UseWhen(context => context.Request.Query.ContainsKey("branch"),
                builder => builder.Use(async (context, next) =>
                     {
                         var branchVer = context.Request.Query["branch"];
                         // Do work that doesn't write to the Response.
                         await next();
                         // Do other work that doesn't write to the Response.
                     }));

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

中间件的单元测试

针对中间件的单元测试,可以使用 TestServer 来进行。它有以下几个优点:

  • 请求会发送到内存中,而不是通过网络进行序列化
  • 避免产生额外的问题,例如端口号或Https等
  • 中间件中的异常可以直接流回调用测试
  • 可以直接在测试中自定义服务器数据结构,如 HttpContext

http请求发送模拟可以使用 HttpClientHttpContext ,分别可以验证Response和Request Context相关功能。下面分别测试RequestIdInRequestMiddleware,RequestIdInResponseMiddleware。
新建xunit单元测试项目,加入依赖包: Microsoft.AspNetCore.TestHost , Microsoft.Extensions.Hosting
测试代码如下:

public class MiddlewareTest
{
    /// <summary>
    /// HttpContext模拟,验证request header是否成功加入requestId
    /// </summary>
    [Fact]
    public void MiddlewareTest_RequestHeaderExistRequestId()
    {
        var hostBuilder = new HostBuilder()
            .ConfigureWebHost(webBuilder =>
            {
                webBuilder
                    .UseTestServer()
                    .ConfigureServices((context, services) =>
                    {
                        services.AddTransient<IService, MyService>();
                    })
                    .Configure(app =>
                    {
                        app.UseRequestIdInRequestMiddleware();
                    });
            });
        using (var host = hostBuilder.Start())
        {
            var context = host.GetTestServer().SendAsync(c =>
                    {
                        c.Request.Path = "/map";
                        c.Request.Method = HttpMethods.Get;
                    }).Result;

            Assert.True(context.Request.Headers.ContainsKey("REQUESTID"));
        }
    }
    /// <summary>
    /// HttpClient模拟,验证response header是否成功加入requestId
    /// </summary>
    [Fact]
    public void MiddlewareTest_ResponseHeaderExistRequestId()
    {
        var hostBuilder = new HostBuilder()
            .ConfigureWebHost(webBuilder =>
            {
                webBuilder
                    .UseTestServer()
                    .ConfigureServices((context, services) =>
                    {
                        services.AddSingleton<RequestIdInResponseMiddleware>();
                        services.AddTransient<IService, MyService>();
                    })
                    .Configure(app =>
                    {
                        app.UseRequestIdInResponseMiddleware();
                    });
            });
        using (var host = hostBuilder.Start())
        {
            host.GetTestServer().CreateRequest("/map").GetAsync()
                .ContinueWith(task =>
                {
                    var response = task.Result;
                    Assert.True(response.Headers.Contains("REQUESTID"));
                }).Wait();
        }
    }
}
posted on 2020-06-04 11:06  xiaogui340  阅读(...)  评论(...编辑  收藏