ASP.NET Core 3.0 自動擋換手動擋:在 Middleware 中執行 Controller Action

最近因爲發現奇怪的 System.Data.SqlClient 性能問題(詳見以前的博文),被迫提早了向 .NET Core 3.0 的升級工做(3.0 Preview 5 中問題已被修復)。鬱悶的是,在剛開始對部分項目進行升級的時候就遇到了一個障礙,咱們基於 Razor Class Library 實現的自定義錯誤頁面因爲屬性路由問題沒法在 ASP.NET Core 3.0 Preview 5 中正常工做(詳見博問),一番排查後也沒找到解決方法。html

爲了避免影響升級進展,咱們被迫採用了一種不經常使用的解決方法 —— 在中間件中直接調用 Controller Action 渲染視圖顯示自定義錯誤頁面,也就是將原先由 ASP.NET Core Runtime 自動執行的 Controller Action (自動擋)改成手工執行(手動擋)。git

原覺得不就是比踩油門多了踩離合器和掛擋嗎,應該不會很難。哪知點火後,掛擋都不知道在哪掛。Action 方法很是特殊,調用它要作不少準備工做,就如掛擋以前要先本身給車安裝離合器和掛擋裝置,再加上是手動擋新手,開始都不知道從哪下手。github

幸好在 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) 後,總算把用手動擋把車開了起來,因而有了這篇隨筆分享一點駕車小經驗。mvc

手動擋的操做杆主要有:RouteData, ActionDescriptor, ActionContext, ActionInvokerFactory, ControllerActionInvokerapp

其中最難操做的也是最重要的是 ActionDescriptor ,絕大多數的熄火都是在操做它時發生的,它有8個屬性須要賦值,有些屬性即便沒用到也要進行初始化賦值,否則立馬熄火(null引用異常)。異步

ActionDescriptor 的操做方法以下async

private static ActionDescriptor CreateActionDescriptor<TController>(string actionName, RouteData routeData)
{
    var controllerType = typeof(TController);
    var actionDesciptor = new ControllerActionDescriptor()
    {
        ControllerName = controllerType.Name,
        ActionName = actionName,
        FilterDescriptors = new List<FilterDescriptor>(),
        MethodInfo = controllerType.GetMethod(actionName, BindingFlags.Public | BindingFlags.Instance),
        ControllerTypeInfo = controllerType.GetTypeInfo(),
        Parameters = new List<ParameterDescriptor>(),
        Properties = new Dictionary<object, object>(),
        BoundProperties = new List<ParameterDescriptor>()
    };

    //...
}

ControllerActionDescriptor 繼承自 ActionDescriptor ,上面的賦值操做中真正傳遞有價值數據的是 ControllerName, ActionName, MethodInfo, ControllerTypeInfo 。一開始不知道要對哪些屬性賦值,只能一步一步試,根據熄火狀況一個一個添加,最終獲得了上面的最少賦值操做。性能

第二重要的是 RouteData ,它是數據傳輸帶,不只要經過它向 ActionDescriptor 傳送 BindingInfo 以及向 Action 方法傳遞參數值,並且要向視圖引擎(好比ViewEngineResult,ViewResultExecutor)傳送 controller 與 action 的名稱,否則視圖引擎找不到視圖文件。ui

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<IActionInvokerFactory>(); //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 

相關文章
相關標籤/搜索