自动挡换手动挡:在 ASP.NET Core 3.0 Middleware 中手动执行 Controller Action
由于遭遇 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