最近由于发现奇怪的 System.Data.SqlClient 性能问题(详见之前的博文
<https://www.cnblogs.com/dudu/p/10848435.html>),被迫提前了向 .NET Core 3.0 的升级工作(3.0
Preview 5 中问题已被修复)。郁闷的是,在刚开始对部分项目进行升级的时候就遇到了一个障碍,我们基于 Razor Class Library
实现的自定义错误页面由于属性路由问题无法在 ASP.NET Core 3.0 Preview 5 中正常工作(详见博问
<https://q.cnblogs.com/q/115296/>),一番排查后也没找到解决方法。
为了不影响升级进展,我们被迫采用了一种不常用的解决方法 —— 在中间件中直接调用 Controller Action
渲染视图显示自定义错误页面,也就是将原先由 ASP.NET Core Runtime 自动执行的 Controller Action
(自动挡)改为手工执行(手动挡)。
原以为不就是比踩油门多了踩离合器和挂挡吗,应该不会很难。哪知点火后,挂挡都不知道在哪挂。Action
方法非常特殊,调用它要做很多准备工作,就如挂挡之前要先自己给车安装离合器和挂挡装置,再加上是手动挡新手,开始都不知道从哪下手。
幸亏在 ASP.NET Core 3.0 的源码中翻到了一本小册子 —— ControllerActionDescriptorBuilder.cs
<https://github.com/aspnet/AspNetCore/blob/master/src/Mvc/Mvc.Core/src/ApplicationModels/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 之后,只需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
<https://github.com/cnblogs-dudu/mvc-in-middleware/blob/master/src/MiddlewareAction/Startup.cs>
热门工具 换一换