什么是中間件
對於中間件我們其實並不陌生,在.NET CORE出現之前中間件的概念在OWIN應用程序中就已經普遍使用了。
中間件官方定義: 中間件是一種集成到應用管道中間來處理請求和響應的模塊,每個中間件可以:
- 選擇是否將請求傳遞到管道的下一個組件
- 可以在管道的下一個組件前后執行工作
ASP.NETCORE中的中間件本質上是一個請求委托 Func< RequestDelegate, RequestDelegate> middleware
。
RequestDelegate本身也是一個委托,定義為 public delegate Task RequestDelegate(HttpContext Context)
。
在ASP.NETCORE請求管道中,形成一條委托鏈。
請求管道短路:當委托不選擇將請求傳遞到下一個委托時,稱之為“短路”。
如何創建中間件
在ASP.NETCORE中,使用 IApplicationBuilder
來創建/插入中間件管道。提供了 Run和Use 兩類方式。依賴組件包 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
的參數公共構造函數 - 名為
Invoke
或InvokeAsync
的公共方法,且此方法必須:- 返回
Task
- 第一個參數為
HttpContext
- 返回
目前官方是推薦使用約定方式, 注意:該方式加入管道中的生命周期為單例。也因此如果依賴一些Service,建議從Invoke
或 InvokeAsync
的方法參數注入,而不是從構造函數注入。(可以想想為什么?單例構造函數注入對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請求發送模擬可以使用 HttpClient
和 HttpContext
,分別可以驗證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();
}
}
}