最近由於發現奇怪的 System.Data.SqlClient 效能問題(詳見之前的博文),被迫提前了向 .NET Core 3.0 的升級工作(3.0 Preview 5 中問題已被修複)。鬱悶的是,在剛開始對部分專案進行升級的時候就遇到了一個障礙,我們基於 Razor Class Library 實現的自定義錯誤頁面由於屬性路由問題無法在 ASP.NET Core 3.0 Preview 5 中正常工作(詳見博問),一番排查後也沒找到解決方法。
為了不影響升級進展,我們被迫採用了一種不常用的解決方法 —— 在中介軟體中直接呼叫 Controller Action 渲染檢視顯示自定義錯誤頁面,也就是將原先由 ASP.NET Core Runtime 自動執行的 Controller Action (自動擋)改為手工執行(手動擋)。
原以為不就是比踩油門多了踩離合器和掛擋嗎,應該不會很難。哪知點火後,掛擋都不知道在哪掛。Action 方法非常特殊,呼叫它要做很多準備工作,就如掛擋之前要先自己給車安裝離合器和掛擋裝置,再加上是手動擋新手,開始都不知道從哪下手。
幸虧在 ASP.NET Core 3.0 的原始碼中翻到了一本小冊子 —— ControllerActionDescriptorBuilder.cs 中的 CreateActionDescriptor 方法,才有了點參考。
private static ControllerActionDescriptor CreateActionDescriptor(...) { var actionDescriptor = new ControllerActionDescriptor { ActionName = action.ActionName, MethodInfo = action.ActionMethod, }; actionDescriptor.ControllerName = controller.ControllerName; actionDescriptor.ControllerTypeInfo = controller.ControllerType; AddControllerPropertyDescriptors(actionDescriptor, controller); AddActionConstraints(actionDescriptor, selector); AddEndpointMetadata(actionDescriptor, selector); AddAttributeRoute(actionDescriptor, selector); AddParameterDescriptors(actionDescriptor, action); AddActionFilters(actionDescriptor, action.Filters, controller.Filters, application.Filters); AddApiExplorerInfo(actionDescriptor, application, controller, action); AddRouteValues(actionDescriptor, controller, action); AddProperties(actionDescriptor, action, controller, application); return actionDescriptor; }
在這本小手冊的指導下,經過無數次熄火(NullReferenceException) 後,總算把用手動擋把車開了起來,於是有了這篇隨筆分享一點駕車小經驗。
手動擋的操作桿主要有:RouteData, ActionDescriptor, ActionContext, ActionInvokerFactory, ControllerActionInvoker
其中最難操作的也是最重要的是 ActionDescriptor ,絕大多數的熄火都是在操作它時發生的,它有8個屬性需要賦值,有些屬性即使沒用到也要進行初始化賦值,不然立馬熄火(null取用異常)。
ActionDescriptor 的操作方法如下
private static ActionDescriptor CreateActionDescriptor(string actionName, RouteData routeData) { var controllerType = typeof(TController); var actionDesciptor = new ControllerActionDescriptor() { ControllerName = controllerType.Name, ActionName = actionName, FilterDescriptors = new List(), MethodInfo = controllerType.GetMethod(actionName, BindingFlags.Public | BindingFlags.Instance), ControllerTypeInfo = controllerType.GetTypeInfo(), Parameters = new List(), Properties = new Dictionary<object, object>(), BoundProperties = new List() }; //... }
ControllerActionDescriptor 繼承自 ActionDescriptor ,上面的賦值操作中真正傳遞有價值資料的是 ControllerName, ActionName, MethodInfo, ControllerTypeInfo 。一開始不知道要對哪些屬性賦值,只能一步一步試,根據熄火情況一個一個新增,最終得到了上面的最少賦值操作。
第二重要的是 RouteData ,它是資料傳輸帶,不僅要透過它向 ActionDescriptor 傳送 BindingInfo 以及向 Action 方法傳遞引數值,而且要向檢視引擎(比如ViewEngineResult,ViewResultExecutor)傳送 controller 與 action 的名稱,不然檢視引擎找不到檢視檔案。
RouteData 的操作方法如下
//For searching View routeData.Values.Add("controller", actionDesciptor.ControllerName.Replace("Controller", "")); routeData.Values.Add("action", actionDesciptor.ActionName); //For binding action parameters foreach (var routeValue in routeData.Values) { var parameter = new ParameterDescriptor(); parameter.Name = routeValue.Key; var attributes = new object[] { new FromRouteAttribute { Name = parameter.Name }, }; parameter.BindingInfo = BindingInfo.GetBindingInfo(attributes); parameter.ParameterType = routeValue.Value.GetType(); actionDesciptor.Parameters.Add(parameter); }
有了 ActionDescriptor 與 RouteData 之後,只需3步操作:
1)ActionContext 把離合器和掛擋裝置組合起來;
2)ActionInvokerFactory 將 ActionContext 安裝到車上並提供了掛擋桿 ControllerActionInvoker;
3)拉動 InvokeAsync 非同步掛擋。
就可以把車開起來。
var actionContext = new ActionContext(context, routeData, actionDesciptor); var actionInvokerFactory = app.ApplicationServices.GetRequiredService(); //ActionInvokerFactory var invoker = actionInvokerFactory.CreateInvoker(actionContext); //ControllerActionInvoker await invoker.InvokeAsync();
但車沒有跑在高速上,而是透過 ASP.NET Core 3.0 的 Endpoint Routing 跑在了中介軟體(middleware)中。
app.UseEndpoints(endpoints => { endpoints.MapGet("/", async context => { var routeData = new RouteData(); routeData.Values.Add("message", "Hello World!"); await DriveControllerAction(context, routeData, app); }); });
Contorller Action 的示例程式碼如下,就是將引數值傳遞給檢視顯示出來。
public class HomeController : Controller { public IActionResult Index(string message) { ViewBag.Message = message; return View(); } }
當程式一執行,瀏覽器請求一發出, DriveControllerAction 就開始手動擋操作,將車開起來,開車效果如下:
雖然開手動擋比自動擋麻煩很多,但駕駛時那種自主把控的感覺還是不錯的,更重要的是這樣的自主解決了我們的實際問題。雖然大多數情況下都只要開自動擋,但會開手動擋會給你在解決問題時多一種選擇。
完整程式碼見 github 上的 Startup.cs
朋友會在“發現-看一看”看到你“在看”的內容