注:本文隸屬於《理解ASP.NET Core》系列文章,請檢視置頂部落格或點選此處檢視全文目錄
中介軟體
先借用微軟官方文件的一張圖:
可以看到,中介軟體實際上是一種配置在HTTP請求管道中,用來處理請求和響應的元件。它可以:
- 決定是否將請求傳遞到管道中的下一個中介軟體
- 可以在管道中的下一個中介軟體處理之前和之後進行操作
此外,中介軟體的註冊是有順序的,書寫程式碼時一定要注意!
中介軟體管道
Run
該方法為HTTP請求管道新增一箇中介軟體,並標識該中介軟體為管道終點,稱為終端中介軟體。也就是說,該中介軟體就是管道的末尾,在該中介軟體之後註冊的中介軟體將永遠都不會被執行。所以,該方法一般只會書寫在Configure
方法末尾。
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Hello, World!");
});
}
}
Use
通過該方法快捷的註冊一個匿名的中介軟體
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
// 下一個中介軟體處理之前的操作
Console.WriteLine("Use Begin");
await next();
// 下一個中介軟體處理完成後的操作
Console.WriteLine("Use End");
});
}
}
注意:
- 如果要將請求傳送到管道中的下一個中介軟體,一定要記得呼叫
next.Invoke / next()
,否則會導致管道短路,後續的中介軟體將不會被執行 - 在中介軟體中,如果已經開始給客戶端傳送
Response
,請千萬不要呼叫next.Invoke / next()
,也不要對Response
進行任何更改,否則,將丟擲異常。 - 可以通過
context.Response.HasStarted
來判斷響應是否已開始。
以下都是錯誤的程式碼寫法
- 錯誤1:
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
await context.Response.WriteAsync("Use");
await next();
});
app.Run(context =>
{
// 由於上方的中介軟體已經開始 Response,此處更改 Response Header 會丟擲異常
context.Response.Headers.Add("test", "test");
return Task.CompletedTask;
});
}
}
- 錯誤2:
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
await context.Response.WriteAsync("Use");
// 即使沒有呼叫 next.Invoke / next(),也不能在 Response 開始後對 Response 進行更改
context.Response.Headers.Add("test", "test");
});
}
}
UseWhen
通過該方法針對不同的邏輯條件建立管道分支。需要注意的是:
- 進入了管道分支後,如果管道分支不存在管道短路或終端中介軟體,則會再次返回到主管道。
- 當使用
PathString
時,路徑必須以“/”開頭,且允許只有一個'/'
字元 - 支援巢狀,即UseWhen中巢狀UseWhen等
- 支援同時匹配多個段,如 /get/user
public class Startup
{
public void Configure(IApplicationBuilder app)
{
// /get 或 /get/xxx 都會進入該管道分支
app.UseWhen(context => context.Request.Path.StartsWithSegments("/get"), app =>
{
app.Use(async (context, next) =>
{
Console.WriteLine("UseWhen:Use");
await next();
});
});
app.Use(async (context, next) =>
{
Console.WriteLine("Use");
await next();
});
app.Run(async context =>
{
Console.WriteLine("Run");
await context.Response.WriteAsync("Hello World!");
});
}
}
當訪問 /get 時,輸出如下:
UseWhen:Use
Use
Run
如果你發現輸出了兩遍,別慌,看看是不是瀏覽器傳送了兩次請求,分別是 /get 和 /favicon.ico
Map
通過該方法針對不同的請求路徑建立管道分支。需要注意的是:
- 一旦進入了管道分支,則不會再回到主管道。
- 使用該方法時,會將匹配的路徑從
HttpRequest.Path
中刪除,並將其追加到HttpRequest.PathBase
中。 - 路徑必須以“/”開頭,且不能只有一個
'/'
字元 - 支援巢狀,即Map中巢狀Map、MapWhen(接下來會講)等
- 支援同時匹配多個段,如 /post/user
public class Startup
{
public void Configure(IApplicationBuilder app)
{
// 訪問 /get 時會進入該管道分支
// 訪問 /get/xxx 時會進入該管道分支
app.Map("/get", app =>
{
app.Use(async (context, next) =>
{
Console.WriteLine("Map get: Use");
Console.WriteLine($"Request Path: {context.Request.Path}");
Console.WriteLine($"Request PathBase: {context.Request.PathBase}");
await next();
});
app.Run(async context =>
{
Console.WriteLine("Map get: Run");
await context.Response.WriteAsync("Hello World!");
});
});
// 訪問 /post/user 時會進入該管道分支
// 訪問 /post/user/xxx 時會進入該管道分支
app.Map("/post/user", app =>
{
// 訪問 /post/user/student 時會進入該管道分支
// 訪問 /post/user/student/1 時會進入該管道分支
app.Map("/student", app =>
{
app.Run(async context =>
{
Console.WriteLine("Map /post/user/student: Run");
Console.WriteLine($"Request Path: {context.Request.Path}");
Console.WriteLine($"Request PathBase: {context.Request.PathBase}");
await context.Response.WriteAsync("Hello World!");
});
});
app.Use(async (context, next) =>
{
Console.WriteLine("Map post/user: Use");
Console.WriteLine($"Request Path: {context.Request.Path}");
Console.WriteLine($"Request PathBase: {context.Request.PathBase}");
await next();
});
app.Run(async context =>
{
Console.WriteLine("Map post/user: Run");
await context.Response.WriteAsync("Hello World!");
});
});
}
}
當你訪問 /get/user 時,輸出如下:
Map get: Use
Request Path: /user
Request PathBase: /get
Map get: Run
當你訪問 /post/user/student/1 時,輸出如下:
Map /post/user/student: Run
Request Path: /1
Request PathBase: /post/user/student
其他情況交給你自己去嘗試啦!
MapWhen
與Map
類似,只不過MapWhen
不是基於路徑,而是基於邏輯條件建立管道分支。注意事項如下:
- 一旦進入了管道分支,則不會再回到主管道。
- 當使用
PathString
時,路徑必須以“/”開頭,且允許只有一個'/'
字元 HttpRequest.Path
和HttpRequest.PathBase
不會像Map
那樣進行特別處理- 支援巢狀,即MapWhen中巢狀MapWhen、Map等
- 支援同時匹配多個段,如 /get/user
public class Startup
{
public void Configure(IApplicationBuilder app)
{
// /get 或 /get/xxx 都會進入該管道分支
app.MapWhen(context => context.Request.Path.StartsWithSegments("/get"), app =>
{
app.MapWhen(context => context.Request.Path.ToString().Contains("user"), app =>
{
app.Use(async (context, next) =>
{
Console.WriteLine("MapWhen get user: Use");
await next();
});
});
app.Use(async (context, next) =>
{
Console.WriteLine("MapWhen get: Use");
await next();
});
app.Run(async context =>
{
Console.WriteLine("MapWhen get: Run");
await context.Response.WriteAsync("Hello World!");
});
});
}
}
當你訪問 /get/user 時,輸出如下:
MapWhen get user: Use
可以看到,即使該管道分支沒有終端中介軟體,也不會回到主管道。
Run & Use & UseWhen & Map & Map
一下子接觸了4個命名相似的、與中介軟體管道有關的API,不知道你有沒有暈倒,沒關係,我來幫大家總結一下:
Run
用於註冊終端中介軟體,Use
用來註冊匿名中介軟體,UseWhen
、Map
、MapWhen
用於建立管道分支。UseWhen
進入管道分支後,如果管道分支中不存在短路或終端中介軟體,則會返回到主管道。Map
和MapWhen
進入管道分支後,無論如何,都不會再返回到主管道。UseWhen
和MapWhen
基於邏輯條件來建立管道分支,而Map
基於請求路徑來建立管道分支,且會對HttpRequest.Path
和HttpRequest.PathBase
進行處理。
編寫中介軟體並啟用
上面已經提到過的Run
和Use
就不再贅述了。
基於約定的中介軟體
“約定大於配置”,先來個約法三章:
- 擁有公共(public)建構函式,且該建構函式至少包含一個型別為
RequestDelegate
的引數 - 擁有名為
Invoke
或InvokeAsync
的公共(public)方法,必須包含一個型別為HttpContext
的方法引數,且該引數必須位於第一個引數的位置,另外該方法必須返回Task
型別。 - 建構函式中的其他引數可以通過依賴注入(DI)填充,也可以通過
UseMiddleware
傳參進行填充。- 通過DI填充時,只能接收 Transient 和 Singleton 的DI引數。這是由於中介軟體是在應用啟動時構造的(而不是按請求構造),所以當出現 Scoped 引數時,建構函式內的DI引數生命週期與其他不共享,如果想要共享,則必須將Scoped DI引數新增到
Invoke/InvokeAsync
來進行使用。 - 通過
UseMiddleware
傳參時,建構函式內的DI引數和非DI引數順序沒有要求,傳入UseMiddleware
內的引數順序也沒有要求,但是我建議將非DI引數放到前面,DI引數放到後面。(這一塊感覺微軟做的好牛皮)
- 通過DI填充時,只能接收 Transient 和 Singleton 的DI引數。這是由於中介軟體是在應用啟動時構造的(而不是按請求構造),所以當出現 Scoped 引數時,建構函式內的DI引數生命週期與其他不共享,如果想要共享,則必須將Scoped DI引數新增到
Invoke/InvokeAsync
的其他引數也能夠通過依賴注入(DI)填充,可以接收 Transient、Scoped 和 Singleton 的DI引數。
一個簡單的中介軟體如下:
public class MyMiddleware
{
// 用於呼叫管道中的下一個中介軟體
private readonly RequestDelegate _next;
public MyMiddleware(
RequestDelegate next,
ITransientService transientService,
ISingletonService singletonService)
{
_next = next;
}
public async Task InvokeAsync(
HttpContext context,
ITransientService transientService,
IScopedService scopedService,
ISingletonService singletonService)
{
// 下一個中介軟體處理之前的操作
Console.WriteLine("MyMiddleware Begin");
await _next(context);
// 下一個中介軟體處理完成後的操作
Console.WriteLine("MyMiddleware End");
}
}
然後,你可以通過UseMiddleware
方法將其新增到管道中
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.UseMiddleware<MyMiddleware>();
}
}
不過,一般不推薦直接使用UseMiddleware
,而是將其封裝到擴充套件方法中
public static class AppMiddlewareApplicationBuilderExtensions
{
public static IApplicationBuilder UseMy(this IApplicationBuilder app) => app.UseMiddleware<MyMiddleware>();
}
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.UseMy();
}
}
基於工廠的中介軟體
優勢:
- 按照請求進行啟用。這個就是說,上面基於約定的中介軟體例項是單例的,但是基於工廠的中介軟體,可以在依賴注入時設定中介軟體例項的生命週期。
- 使中介軟體強型別化(因為其實現了介面
IMiddleware
)
該方式的實現基於IMiddlewareFactory
和IMiddleware
。先來看一下介面定義:
public interface IMiddlewareFactory
{
IMiddleware? Create(Type middlewareType);
void Release(IMiddleware middleware);
}
public interface IMiddleware
{
Task InvokeAsync(HttpContext context, RequestDelegate next);
}
你有沒有想過當我們呼叫UseMiddleware
時,它是如何工作的呢?事實上,UseMiddleware
擴充套件方法會先檢查中介軟體是否實現了IMiddleware
介面。 如果實現了,則使用容器中註冊的IMiddlewareFactory
例項來解析該IMiddleware
的例項(這下你知道為什麼稱為“基於工廠的中介軟體”了吧)。如果沒實現,那麼就使用基於約定的中介軟體邏輯來啟用中介軟體。
注意,基於工廠的中介軟體,在應用的服務容器中一般註冊為 Scoped 或 Transient 服務。
這樣的話,我們們就可以放心的將 Scoped 服務注入到中介軟體的建構函式中了。
接下來,我們們就來實現一個基於工廠的中介軟體:
public class YourMiddleware : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
// 下一個中介軟體處理之前的操作
Console.WriteLine("YourMiddleware Begin");
await next(context);
// 下一個中介軟體處理完成後的操作
Console.WriteLine("YourMiddleware End");
}
}
public static class AppMiddlewareApplicationBuilderExtensions
{
public static IApplicationBuilder UseYour(this IApplicationBuilder app) => app.UseMiddleware<YourMiddleware>();
}
然後,在ConfigureServices
中新增中介軟體依賴注入
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<YourMiddleware>();
}
}
最後,在Configure
中使用中介軟體
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.UseYour();
}
}
微軟提供了IMiddlewareFactory
的預設實現:
public class MiddlewareFactory : IMiddlewareFactory
{
// The default middleware factory is just an IServiceProvider proxy.
// This should be registered as a scoped service so that the middleware instances
// don't end up being singletons.
// 預設的中介軟體工廠僅僅是一個 IServiceProvider 的代理
// 該工廠應該註冊為 Scoped 服務,這樣中介軟體例項就不會成為單例
private readonly IServiceProvider _serviceProvider;
public MiddlewareFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IMiddleware? Create(Type middlewareType)
{
return _serviceProvider.GetRequiredService(middlewareType) as IMiddleware;
}
public void Release(IMiddleware middleware)
{
// The container owns the lifetime of the service
// DI容器來管理服務的生命週期
}
}
可以看到,該工廠使用過DI容器來解析出服務例項的。因此,當使用基於工廠的中介軟體時,是無法通過UseMiddleware
向中介軟體的建構函式傳參的。
基於約定的中介軟體 VS 基於工廠的中介軟體
- 基於約定的中介軟體例項都是 Singleton;而基於工廠的中介軟體例項可以是 Singleton、Scoped 和 Transient(當然,不建議註冊為 Singleton)
- 基於約定的中介軟體例項建構函式中可以通過依賴注入傳參,也可以用過
UseMiddleware
傳參;而基於工廠的中介軟體只能通過依賴注入傳參 - 基於約定的中介軟體例項可以在
Invoke/InvokeAsync
中新增更多的依賴注入引數;而基於工廠的中介軟體只能按照IMiddleware
的介面定義進行實現。