代码改变世界

通过表达式树构建URL时正确识别ActionNameAttribute

2009-09-01 14:25  Jeffrey Zhao  阅读(5777)  评论(5编辑  收藏  举报

在MvcFutures项目中提供了一个辅助方法,可以将一个表达式树对象转化成一个RouteValueDictionary集合。只可惜,这个辅助方法的毛病比较多。例如,它直接把方法名作为action的值,而忽略了其上标记的ActionNameAttribute。这导致了某个被“改名”的Action方法一旦用在了表达式树中,最终得到的URL便是错误的。例如有一个Action方法:

public class HomeController : Controller
{
    [ActionName("Default")
    public ViewResult Index()
    {
        ...
    }
}

如果您使用这样的方式来生成URL(ActionEx方法的实现请参考《使用表达式树构建DomainRoute的URL》):

<a href="<%= Url.ActionEx<HomeController>(c => c.Index()) %>">Home</a>

则最终得到的代码是:

<a href="/Home/Index">Home</a>

而我们需要的结果应该是:

<a href="/Home/Default">Home</a>

正是因为这个原因(以及一些其他因素),许多朋友放弃使用强类型的方式构造URL。不过,如果您继续看下去,就会发现这个功能其实非常简单。只要做稍微一点点修改就可以了。不过现在,让我们来观察MvcFutures是如何实现这部分功能的。我已经把相关的代码复制到自己的RouteExpression类中:

public static class RouteExpression
{
    public static RouteValueDictionary GetRouteValues<TController>(
        Expression<Action<TController>> action)
        where TController : Controller
    {
        if (action == null)
        {
            throw new ArgumentNullException("action");
        }

        MethodCallExpression call = action.Body as MethodCallExpression;
        if (call == null)
        {
            throw new ArgumentException(
                "The action must be a method call.", "action");
        }

        string controllerName = typeof(TController).Name;
        if (!controllerName.EndsWith("Controller", StringComparison.OrdinalIgnoreCase))
        {
            throw new ArgumentException(
                "The controller name must end with 'Controller'.", "action");
        }

        controllerName = controllerName.Substring(
            0, controllerName.Length - "Controller".Length);
        if (controllerName.Length == 0)
        {
            throw new ArgumentException(
                "Cannot route to the Controller class", "action");
        }

        var rvd = new RouteValueDictionary();
        rvd.Add("controller", controllerName);
        rvd.Add("action", call.Method.Name);

        AddParameterValuesFromExpressionToDictionary(rvd, call);
        return rvd;
    }

    private static void AddParameterValues(
        RouteValueDictionary rvd, MethodCallExpression call)
    {
        ...
    }
}

这段代码大部分内容都是进行参数校验,一旦出现以下情况之一,便会抛出异常:

  • 表达式树为null。
  • 表达式树不是一个MethodCallExpression(应该是一个Action方法的调用)
  • 如果控制器类型的名称不以Controller结尾(破坏了约定)
  • 如果控制器类型的名称就是Controller

经过校验之后,这个方法根据控制器类型的名称计算出controller(HomeController => Home),再把所调方法的名称作为action(Index() => Index)。最后,再使用AddParameterValues方法获得参数,并填充RouteValueDictionary(关于这点我们下次再来讨论)。

不过,问题就出现在从Action方法的MethodInfo“直接获取”名称这个步骤上。这个MethodInfo可能还标记着ActionNameAttribute呢,它的Name属性可不是action的名称。为此,我们必须多做这么一步:

private static ReaderWriterLockSlim s_rwLock = new ReaderWriterLockSlim();
private static Dictionary<MethodInfo, string> s_actionNames = 
    new Dictionary<MethodInfo, string>();

private static string GetActionName(MethodInfo methodInfo)
{
    string actionName = null;

    s_rwLock.EnterReadLock();
    try
    {
        if (s_actionNames.TryGetValue(methodInfo, out actionName))
        {
            return actionName;
        }
    }
    finally
    {
        s_rwLock.ExitReadLock();
    }

    var attribute = (ActionNameAttribute)methodInfo
        .GetCustomAttributes(typeof(ActionNameAttribute), false)
        .SingleOrDefault();
    actionName = attribute == null ? methodInfo.Name : attribute.Name;

    s_rwLock.EnterWriteLock();
    try
    {
        s_actionNames[methodInfo] = actionName;
    }
    finally
    {
        s_rwLock.ExitWriteLock();
    }

    return actionName;
}

在GetActionName方法的中部则是获得action名称的代码。它会根据methodInfo上的ActionNameAttribute标记情况来确定。如果标记了ActionNameAttribute,则使用Attribute的Name属性作为action名称,否则就使用MethodInfo对象的Name属性。获得action名称之后,我们会将其保存在一个字典中。至于使用ReaderWriterLockSlim来控制并发读写的方式已经成为了标准,您甚至可以将其封装为一个组件避免重复编写相同的代码。

最后,我们把原来GetRouteValues方法中的一行代码加以替换即可:

public static RouteValueDictionary GetRouteValues<TController>(
    Expression<Action<TController>> action)
    where TController : Controller
{
    ...
    rvd.Add("action", call.Method.Name);
    rvd.Add("action", GetActionName(call.Method));
    ...
}

ASP.NET MVC给了我们充分的自由度定制需要的组件。从中我们也可以了解到如何在项目中编写合适的API。其实很多东西只要多走一步就会美好很多,例如这个例子,需要花费您超过半小时的时间吗?