到目前为止我看到的所有解决方案都有一个缺点:虽然它们使更改控制器或动作的名称安全,但它们不能保证这两个实体之间的一致性。您可以指定来自不同控制器的操作:
public class HomeController : Controller
{
public ActionResult HomeAction() { ... }
}
public class AnotherController : Controller
{
public ActionResult AnotherAction() { ... }
private void Process()
{
Url.Action(nameof(AnotherAction), nameof(HomeController));
}
}
更糟糕的是,这种方法无法考虑可能应用于控制器和/或动作以更改路由的众多属性,例如RouteAttribute
和RoutePrefixAttribute
,因此对基于属性的路由的任何更改都可能被忽视。
最后,它Url.Action()
本身并不能确保 action 方法与其构成 URL 的参数之间的一致性:
public class HomeController : Controller
{
public ActionResult HomeAction(int id, string name) { ... }
private void Process()
{
Url.Action(nameof(HomeAction), new { identity = 1, title = "example" });
}
}
我的解决方案基于Expression
元数据:
public static class ActionHelper<T> where T : Controller
{
public static string GetUrl(Expression<Func<T, Func<ActionResult>>> action)
{
return GetControllerName() + '/' + GetActionName(GetActionMethod(action));
}
public static string GetUrl<U>(
Expression<Func<T, Func<U, ActionResult>>> action, U param)
{
var method = GetActionMethod(action);
var parameters = method.GetParameters();
return GetControllerName() + '/' + GetActionName(method) +
'?' + GetParameter(parameters[0], param);
}
public static string GetUrl<U1, U2>(
Expression<Func<T, Func<U1, U2, ActionResult>>> action, U1 param1, U2 param2)
{
var method = GetActionMethod(action);
var parameters = method.GetParameters();
return GetControllerName() + '/' + GetActionName(method) +
'?' + GetParameter(parameters[0], param1) +
'&' + GetParameter(parameters[1], param2);
}
private static string GetControllerName()
{
const string SUFFIX = nameof(Controller);
string name = typeof(T).Name;
return name.EndsWith(SUFFIX) ? name.Substring(0, name.Length - SUFFIX.Length) : name;
}
private static MethodInfo GetActionMethod(LambdaExpression expression)
{
var unaryExpr = (UnaryExpression)expression.Body;
var methodCallExpr = (MethodCallExpression)unaryExpr.Operand;
var methodCallObject = (ConstantExpression)methodCallExpr.Object;
var method = (MethodInfo)methodCallObject.Value;
Debug.Assert(method.IsPublic);
return method;
}
private static string GetActionName(MethodInfo info)
{
return info.Name;
}
private static string GetParameter<U>(ParameterInfo info, U value)
{
return info.Name + '=' + Uri.EscapeDataString(value.ToString());
}
}
这可以防止您传递错误的参数来生成 URL:
ActionHelper<HomeController>.GetUrl(controller => controller.HomeAction, 1, "example");
因为它是一个 lambda 表达式,所以动作总是绑定到它的控制器上。(而且您还拥有 Intellisense!)一旦选择了动作,它就会强制您指定其所有正确类型的参数。
给定的代码仍然没有解决路由问题,但至少可以修复它,因为有控制器Type.Attributes
和MethodInfo.Attributes
可用的。
编辑:
正如@CarterMedlin 指出的那样,非原始类型的操作参数可能没有与查询参数的一对一绑定。目前,这是通过ToString()
专门为此目的而在参数类中覆盖的调用来解决的。然而,该方法可能并不总是适用,它也不控制参数名称。
要解决此问题,您可以声明以下接口:
public interface IUrlSerializable
{
Dictionary<string, string> GetQueryParams();
}
并在参数类中实现:
public class HomeController : Controller
{
public ActionResult HomeAction(Model model) { ... }
}
public class Model : IUrlSerializable
{
public int Id { get; set; }
public string Name { get; set; }
public Dictionary<string, string> GetQueryParams()
{
return new Dictionary<string, string>
{
[nameof(Id)] = Id,
[nameof(Name)] = Name
};
}
}
以及相应的更改ActionHelper
:
public static class ActionHelper<T> where T : Controller
{
...
private static string GetParameter<U>(ParameterInfo info, U value)
{
var serializableValue = value as IUrlSerializable;
if (serializableValue == null)
return GetParameter(info.Name, value.ToString());
return String.Join("&",
serializableValue.GetQueryParams().Select(param => GetParameter(param.Key, param.Value)));
}
private static string GetParameter(string name, string value)
{
return name + '=' + Uri.EscapeDataString(value);
}
}
如您所见,ToString()
当参数类未实现接口时,它仍然有一个回退。
用法:
ActionHelper<HomeController>.GetUrl(controller => controller.HomeAction, new Model
{
Id = 1,
Name = "example"
});