zoukankan      html  css  js  c++  java
  • 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 中正常工作(详见博问),一番排查后也没找到解决方法。

    为了不影响升级进展,我们被迫采用了一种不常用的解决方法 —— 在中间件中直接调用 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<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 的名称,不然视图引擎找不到视图文件。

    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 

  • 相关阅读:
    Sed的使用方法简介
    Shell脚本基础
    网络配置与内核模块相关
    RPM管理,计划任务与性能监控
    SSH服务
    LVM与RAID阵列
    网络存储服务器
    FTP服务
    网络安全之iptables防火墙
    MySQL使用笔记(七)排序和限制数据记录查询
  • 原文地址:https://www.cnblogs.com/dudu/p/10885094.html
Copyright © 2011-2022 走看看