如何将 C# nameof() 与 ASP.NET MVC Url.Action 一起使用
本文关键字:Url MVC NET Action 一起 ASP nameof | 更新日期: 2023-09-27 18:34:02
有没有推荐的方法来使用新的
nameof()
控制器名称的 MVC ASP.NET 表达式?
Url.Action("ActionName", "Home") <------ works
与
Url.Action(nameof(ActionName), nameof(HomeController)) <----- doesn't work
显然它不起作用,因为名称(HomeController(转换为">HomeController">,而MVC需要的只是"Home"。
我喜欢詹姆斯关于使用扩展方法的建议。只有一个问题:尽管您使用的是nameof()
并且已经消除了魔术字符串,但仍然存在一个小的类型安全性问题:您仍在使用字符串。因此,很容易忘记使用扩展方法,或者提供无效的任意字符串(例如,错误键入控制器的名称(。
我认为我们可以通过使用控制器的通用扩展方法来改进 James 的建议,其中通用参数是目标控制器:
public static class ControllerExtensions
{
public static string Action<T>(this Controller controller, string actionName)
where T : Controller
{
var name = typeof(T).Name;
string controllerName = name.EndsWith("Controller")
? name.Substring(0, name.Length - 10) : name;
return controller.Url.Action(actionName, controllerName);
}
}
用法现在更简洁了:
this.Action<HomeController>(nameof(ActionName));
考虑一个扩展方法:
public static string UrlName(this Type controller)
{
var name = controller.Name;
return name.EndsWith("Controller") ? name.Substring(0, name.Length - 10) : name;
}
然后,您可以使用:
Url.Action(nameof(ActionName), typeof(HomeController).UrlName())
我需要确保正确处理routeValues
,而不是总是像querystring
值一样处理。 但是,我仍然想确保操作与控制器匹配。
我的解决方案是为Url.Action
创建扩展重载。
<a href="@(Url.Action<MyController>(x=>x.MyAction))">Button Text</a>
我对不同类型的单参数操作有重载。 如果我需要通过routeValues
...
<a href="@(Url.Action<MyController>(x=>x.MyAction, new { myRouteValue = myValue }))">Button Text</a>
对于具有复杂参数且尚未显式创建重载的操作,需要使用控制器类型指定类型以匹配操作定义。
<a href="@(Url.Action<MyController,int,string>(x=>x.MyAction, new { myRouteValue1 = MyInt, MyRouteValue2 = MyString}))">Button Text</a>
当然,大多数时候操作都保留在同一个控制器中,所以我仍然只使用nameof
。
<a href="@Url.Action(nameof(MyController.MyAction))">Button Text</a>
由于routeValues
不一定与操作参数匹配,因此此解决方案具有这种灵活性。
扩展码
namespace System.Web.Mvc {
public static class UrlExtensions {
// Usage : <a href="@(Url.Action<MyController>(x=>x.MyActionNoVars, new {myroutevalue = 1}))"></a>
public static string Action<T>(this UrlHelper helper,Expression<Func<T,Func<ActionResult>>> expression,object routeValues = null) where T : Controller
=> helper.Action<T>((LambdaExpression)expression,routeValues);
// Usage : <a href="@(Url.Action<MyController,vartype1>(x=>x.MyActionWithOneVar, new {myroutevalue = 1}))"></a>
public static string Action<T, P1>(this UrlHelper helper,Expression<Func<T,Func<P1,ActionResult>>> expression,object routeValues = null) where T : Controller
=> helper.Action<T>(expression,routeValues);
// Usage : <a href="@(Url.Action<MyController,vartype1,vartype2>(x=>x.MyActionWithTwoVars, new {myroutevalue = 1}))"></a>
public static string Action<T, P1, P2>(this UrlHelper helper,Expression<Func<T,Func<P1,P2,ActionResult>>> expression,object routeValues = null) where T : Controller
=> helper.Action<T>(expression,routeValues);
// Usage : <a href="@(Url.Action<MyController>(x=>x.MyActionWithOneInt, new {myroutevalue = 1}))"></a>
public static string Action<T>(this UrlHelper helper,Expression<Func<T,Func<int,ActionResult>>> expression,object routeValues = null) where T : Controller
=> helper.Action<T>((LambdaExpression)expression,routeValues);
// Usage : <a href="@(Url.Action<MyController>(x=>x.MyActionWithOneString, new {myroutevalue = 1}))"></a>
public static string Action<T>(this UrlHelper helper,Expression<Func<T,Func<string,ActionResult>>> expression,object routeValues = null) where T : Controller
=> helper.Action<T>((LambdaExpression)expression,routeValues);
//Support function
private static string Action<T>(this UrlHelper helper,LambdaExpression expression,object routeValues = null) where T : Controller
=> helper.Action(
((MethodInfo)((ConstantExpression)((MethodCallExpression)((UnaryExpression)expression.Body).Operand).Object).Value).Name,
typeof(T).Name.Replace("Controller","").Replace("controller",""),
routeValues);
}
}
到目前为止,我看到的所有解决方案都有一个缺点:虽然它们使更改控制器或操作的名称变得安全,但它们不能保证这两个实体之间的一致性。您可以从其他控制器指定操作:
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()
本身不能确保操作方法与其构成 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 表达式,因此操作始终绑定到其控制器。(而且你也有智能感知!选择操作后,它会强制您指定其所有正确类型的参数。
给定的代码仍然没有解决路由问题,但是至少可以修复它,因为控制器的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"
});
基于Gigi的答案(它为控制器引入了类型安全(,我又迈出了一步。我非常喜欢T4MVC,但我从来不喜欢运行T4一代。我喜欢代码生成,但它不是 MSBuild 的原生代码,因此构建服务器很难使用它。
我重用了通用概念并添加了Expression
参数:
public static class ControllerExtensions
{
public static ActionResult RedirectToAction<TController>(
this Controller controller,
Expression<Func<TController, ActionResult>> expression)
where TController : Controller
{
var fullControllerName = typeof(TController).Name;
var controllerName = fullControllerName.EndsWith("Controller")
? fullControllerName.Substring(0, fullControllerName.Length - 10)
: fullControllerName;
var actionCall = (MethodCallExpression) expression.Body;
return controller.RedirectToAction(actionCall.Method.Name, controllerName);
}
}
上述调用的示例如下所示:
public virtual ActionResult Index()
{
return this.RedirectToAction<JobController>( controller => controller.Index() );
}
如果JobController
没有Index
,则会遇到编译器错误。这可能是它比之前的答案唯一的优势 - 所以这是另一个愚蠢的检查。如果JobController
没有Index
,它将帮助您停止使用JobController
。此外,在寻找动作时,它会给你智能感。
--
我还在此签名中添加了:
public static ActionResult RedirectToAction<TController>(this TController controller, Expression<Func<TController, ActionResult>> expression)
where TController : Controller
这允许以更简单的方式键入当前控制器的操作,而无需指定类型。两者可以并排使用:
public virtual ActionResult Index()
{
return this.RedirectToAction(controller => controller.Test());
}
public virtual ActionResult Test()
{
...
}
--
我在评论中被问及这是否支持参数。上述问题的答案是否定的。但是,我非常快速地创建了一个可以解析参数的版本。这是调整后的方法:
public static ActionResult RedirectToAction<TController>(this Controller controller, Expression<Func<TController, ActionResult>> expression)
where TController : Controller
{
var fullControllerName = typeof(TController).Name;
var controllerName = fullControllerName.EndsWith("Controller")
? fullControllerName.Substring(0, fullControllerName.Length - 10)
: fullControllerName;
var actionCall = (MethodCallExpression)expression.Body;
var routeValues = new ExpandoObject();
var routeValuesDictionary = (IDictionary<String, Object>)routeValues;
var parameters = actionCall.Method.GetParameters();
for (var i = 0; i < parameters.Length; i++)
{
var arugmentLambda = Expression.Lambda(actionCall.Arguments[i], expression.Parameters);
var arugmentDelegate = arugmentLambda.Compile();
var argumentValue = arugmentDelegate.DynamicInvoke(controller);
routeValuesDictionary[parameters[i].Name] = argumentValue;
}
return controller.RedirectToAction(actionCall.Method.Name, controllerName, routeValues);
}
我还没有亲自测试过它(但智能感知使它看起来可以编译(。总而言之,代码会查看方法的所有参数,并创建一个包含所有参数的 ExpandoObject。这些值是从传入的表达式确定的,方法是使用主表达式的原始参数将每个值作为独立的 lambda 表达式进行调用。然后编译并调用表达式,并将结果值存储在 ExpandoObject 中。然后将结果传递到内置帮助程序。
一个简单的常量就可以完成这项工作,或者,如果你愿意,一个接口:
internal interface IBaseController { public string Name { get; } }
public class MyController : IBaseController
{
public const string NAME = "My";
// or
public string Name { get => "My"; }
...
}
无需计算:
<a asp-controller="@MyController.Name" asp-action="@nameof(MyController.Index)">Close</a>
对@James答案的看法:
相反,使用字符串扩展方法:返回控制器名称前缀,否则返回传入的参数。
/// <summary>
/// Gets the prefix of the controller name.
/// <para> <see langword="Usage:"/>
/// <code>var <paramref name="controllerNamePrefix"/> =
/// <see langword="nameof"/>(ExampleController).
/// <see cref="GetControllerPrefix()"/>;
/// </code>
/// </para>
/// </summary>
/// <param name="fullControllerName"></param>
/// <returns></returns>
public static string GetControllerPrefix(this string fullControllerName)
{
const string Controller = nameof(Controller);
if (string.IsNullOrEmpty(fullControllerName) || !fullControllerName.EndsWith(Controller))
return fullControllerName;
return fullControllerName.Substring(0, fullControllerName.Length - Controller.Length);
}
正在寻找如何在 ASP.NET Core中执行此操作的人,请尝试以下操作: https://github.com/ivaylokenov/AspNet.Mvc.TypedRouting
@(Html.ActionLink<HomeController>("Home page", c => c.Index()))
我使用这样的东西,它有效:Url.Action(nameof(ActionName(, nameof(HomeController(.替换("控制器",字符串。空((