表达式树是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上写个扩展方法:
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得到
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
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了。
if (valueType.IsValueType)
{
routeValues.Add(name, value);
}
此代码并不支持复杂类型的参数,对于action中传入复杂的类型,比如:
public class User
{
public int Age { get; set; }
public string Email { get; set; }
}
如果Action中的参数使用了User类型:
public ActionResult SayHelloToUser(User user)
{
return new EmptyResult();
}
如何解析呢?
var properties = PropertyInfoHelper.GetProperties(valueType);
foreach (var propertyInfo in properties)
{
routeValues.Add(propertyInfo.Name, propertyInfo.GetValue(value));
}
大功告成,现在已经完美解决了各种类型的参数传入。
同样的道理,我们可以扩展HtmlHelper
的 RenderAction()
, ActionLink()
….