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

dudu發表於2019-05-18

由於遭遇 System.Data.SqlClient 的效能問題(詳見之前的博文),向 .NET Core 3.0 的升級工作被迫提前了。在升級過程中遇到了一個問題,我們在 Razor Class Library 中實現的自定義錯誤頁面無法在 ASP.NET Core 3.0 Preview 5 中正常工作,問題原因詳見博問 Razor Class Library 中的屬性路由在 ASP.NET Core 3.0 中不起作用 。

由於屬性路由不起作用的問題沒找到解決方法,於是被迫採用了另外一種解決方法 —— 在中介軟體中呼叫 Razor Class Library 中的 Controller Action 顯示自定義錯誤頁面。這就需要將原先由 ASP.NET Core Runtime 自動執行的 Controller 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 的操作方法如下

var actionDesciptor = new ControllerActionDescriptor()
{
    ControllerName = controllerType.Name,
    ActionName = actionName,
    FilterDescriptors = new List<FilterDescriptor>(),
    MethodInfo = typeof(HomeController).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 的名稱,不然檢視引擎找不到檢視檔案。

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 之後,只需4步操作,可以把車開起來了。

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 的 EndpointRouting 跑在了 middleware 中。

app.UseEndpoints(endpoints =>
{
    endpoints.MapGet("/", async context =>
    {
        var routeData = new RouteData();
        routeData.Values.Add("message", "Hello World!");
        await DriveControllerAction(context, routeData, app);
    });
});

看看手動擋開車的效果,Contorller 的示例程式碼如下

public class HomeController : Controller
{
    public IActionResult Index(string message)
    {
        ViewBag.Message = message;
        return View();
    }
}

執行結果

手動擋駕駛 ASP.NET Core 3.0 Preview 5 版 Contoller Action 型新車成功!

完整程式碼見 github 上的 Startup.cs 

相關文章