Expression Tree 扩展MVC中的 HtmlHelper 和 UrlHelper
目录
结束语:本文使用Expression tree 扩展了HtmlHelper和UrlHelper,给出了一个具有静态检查的API实现方式。本文章所使用的源码提供下载,转载请注明出处
表达式树是LINQ To everything 的基础,同时各种类库的Fluent API也 大量使用了Expression Tree。还记得我在不懂expression tree时,各种眼花缭乱的API 看的我各种膜拜,当我熟悉expression tree 后恍然大悟,不用看代码也能知道别人的API 是如何设计的(^_^)。 接下来这篇博客就谈谈如何使用expression tree扩展MVC中的HtmlHelper和UrlHelper。
场景
当我们在MVC中生成一个action的url会这样写:var url=UrlHelper.Action("index", "Home"); 如果要render一个action时会这样写:Html.RenderAction("index", "Home");
这样的写法瑕疵在于我们传递了两个字符串类型的参数在代码中,而我们又免不了对action和controller做重命名操作:index->default, 即便是你用resharper这样的工具重命名也无法将UrlHelper.Action("index", "Home"); 改变为UrlHelper.Action("default", "Home");
vs甚至在编译时都不会检查出来这个错误。 所以我们的目标是:设计出具有静态检查的API,让vs 提示出这个错误来,甚至是重命名时直接把相关代码都能重命名。
使用Expression Tree 重新设计这两组API
目标:设计出类似的API:Url.Action((HomeController c) => c.Index());
1.很明显我们需要在UrlHelper上写个扩展方法:
1 2 3 4 5 6 7 8 9 10 11 12 | public static string Action<TController>( this UrlHelper url, Expression<Func<TController, ActionResult>> actionSelector, string protocol = null , string hostname = null ) { var action = "Index" ; //待解析 var controller = "Home" ; //带解析 var routeValues = new RouteValueDictionary(); //待解析 return url.Action(action, controller, routeValues, protocol, hostname); } |
现在只需要根据表达式Expression<Func<TController, ActionResult>> actionSelector 解析出action,controller,还有routeValues即可
2.解析出controller 的名称
分析:controller的名称可以根据泛型方法中的泛型参数TController得到
1 2 3 4 5 6 7 | private static string GetControllerName(Type controllerType) { var controllerName = controllerType.Name.EndsWith( "Controller" ) ? controllerType.Name.Substring(0, controllerType.Name.Length - "Controller" .Length) : controllerType.Name; return controllerName; } |
3.解析action的名称
分析:由于表达式Expression<Func<TController, ActionResult>> actionSelector 是一个MethodCallExpression, 所以可以很容易得到action的名称:var action=call.Method.Name;
这样已经完成了最简单的url构造方案, 但是我们还没有处理带有参数的action类型,例如:Url.Action((HomeController c) => c.Detail(10, 20));
4.解析routeValues
分析:action中的参数实际上就是MethodCallExpression中的参数,我们解析这个expression的参数即可,然后得到RouteValues
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | private static RouteValueDictionary GetRouteValues(MethodCallExpression call) { var routeValues = new RouteValueDictionary(); var args = call.Arguments; ParameterInfo[] parameters = call.Method.GetParameters(); var pairs = args.Select((a, i) => new { Argument = a, ParamName = parameters[i].Name }); foreach ( var argumentParameterPair in pairs) { string name = argumentParameterPair.ParamName; object value = argumentParameterPair.Argument.GetValue(); if (value != null ) { var valueType = value.GetType(); if (valueType.IsValueType) { routeValues.Add(name, value); } throw new NotSupportedException( "unsoupported parameter type {0}" .FormatWith(value.ToString())); } } return routeValues; } |
如此一来,类似Url.Action((HomeController c) => c.Detail(10, 20));这样的action也可以构造出Url了。
1 2 3 4 | if (valueType.IsValueType) { routeValues.Add(name, value); } |
此代码并不支持复杂类型的参数,对于action中传入复杂的类型,比如:
1 2 3 4 5 | public class User { public int Age { get ; set ; } public string Email { get ; set ; } } |
如果Action中的参数使用了User类型:
1 2 3 4 | public ActionResult SayHelloToUser(User user) { return new EmptyResult(); } |
如何解析呢?
1 2 3 4 5 | var properties = PropertyInfoHelper.GetProperties(valueType); foreach ( var propertyInfo in properties) { routeValues.Add(propertyInfo.Name, propertyInfo.GetValue(value)); } |
大功告成,现在已经完美解决了各种类型的参数传入。
同样的道理,我们可以扩展HtmlHelper 的 RenderAction(), ActionLink()….
缺陷
早在09年,jeffery zhao就发表了lambda方式生成url的博客,对比了几种方案的性能问题,并且给出了优化方案,当然,我在写这篇博客的时候还没有真正尝试去优化这个方案,只是再次拜读了大神的方案,记得早些年就读过这些文章,但是今天重新读过仍然获益匪浅,不由得感叹几句,莫非跑题了;-);-)
接下来我会思考这个优化的问题。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· 展开说说关于C#中ORM框架的用法!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?