如何在ASP.NET MVC Url.Action中使用C#nameof()

时间:2022-11-30 22:38:31

Is there a recommended way to use the new

是否有推荐的方式来使用新的

nameof()

expression in ASP.NET MVC for controller names?

在ASP.NET MVC中表达控制器名称?

Url.Action("ActionName", "Home")  <------ works

vs

VS

Url.Action(nameof(ActionName), nameof(HomeController)) <----- doesn't work

obviously it doesn't work because of nameof(HomeController) converts to "HomeController" and what MVC needs is just "Home".

显然它不起作用因为nameof(HomeController)转换为“HomeController”而MVC需要的只是“Home”。

4 个解决方案

#1


17  

I like James' suggestion of using an extension method. There is just one problem: although you're using nameof() and have eliminated magic strings, there's still a small issue of type safety: you're still working with strings. As such, it is very easy to forget to use the extension method, or to provide an arbitrary string that isn't valid (e.g. mistyping the name of a controller).

我喜欢詹姆斯关于使用扩展方法的建议。只有一个问题:虽然你正在使用nameof()并且已经消除了魔术字符串,但仍然存在类型安全的小问题:你仍在使用字符串。因此,很容易忘记使用扩展方法,或者提供无效的任意字符串(例如,错误输入控制器的名称)。

I think we can improve James' suggestion by using a generic extension method for Controller, where the generic parameter is the target controller:

我认为我们可以通过对Controller使用通用扩展方法来改进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);
    }
}

The usage is now much cleaner:

使用现在更清洁:

this.Action<HomeController>(nameof(ActionName));

#2


6  

Consider an extension method:

考虑一种扩展方法:

public static string UrlName(this Type controller)
{
  var name = controller.Name;
  return name.EndsWith("Controller") ? name.Substring(0, name.Length - 10) : name;
}

Then you can use:

然后你可以使用:

Url.Action(nameof(ActionName), typeof(HomeController).UrlName())

#3


1  

All the solutions I have seen so far have one drawback: while they make changing controller's or action's name safe, they do not guarantee consistency between those two entities. You may specify an action from a different controller:

到目前为止,我所看到的所有解决方案都有一个缺点:虽然它们使控制器或动作的名称变得安全,但它们并不能保证这两个实体之间的一致性。您可以从其他控制器指定操作:

public class HomeController : Controller
{
    public ActionResult HomeAction() { ... }
}

public class AnotherController : Controller
{
    public ActionResult AnotherAction() { ... }

    private void Process()
    {
        Url.Action(nameof(AnotherAction), nameof(HomeController));
    }
}

To make it even worse, this approach cannot take into account the numerous attributes one may apply to controllers and/or actions to change routing, e.g. RouteAttribute and RoutePrefixAttribute, so any change to the attribute-based routing may go unnoticed.

更糟糕的是,这种方法不能考虑可能应用于控制器的多种属性和/或改变路由的动作,例如, RouteAttribute和RoutePrefixAttribute,因此对基于属性的路由的任何更改都可能会被忽视。

Finally, the Url.Action() itself does not ensure consistency between action method and its parameters that constitute the URL:

最后,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" });
    }
}

My solution is based on Expression and metadata:

我的解决方案基于表达式和元数据:

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());
    }
}

This prevents you from passing wrong parameters to generate a URL:

这可以防止您传递错误的参数来生成URL:

ActionHelper<HomeController>.GetUrl(controller => controller.HomeAction, 1, "example");

Since it is a lambda expression, action is always bound to its controller. (And you also have Intellisense!) Once the action is chosen, it forces you to specify all of its parameters of correct type.

由于它是lambda表达式,因此action始终绑定到其控制器。 (而且你也有Intellisense!)一旦选择了动作,它就会强制你指定所有正确类型的参数。

The given code still does not address the routing issue, however fixing it is at least possible, as there are both controller's Type.Attributes and MethodInfo.Attributes available.

给定的代码仍然没有解决路由问题,但是修复它至少是可能的,因为控制器的Type.Attributes和MethodInfo.Attributes都可用。

EDIT:

编辑:

As @CarterMedlin pointed out, action parameters of non-primitive type may not have a one-to-one binding to query parameters. Currently, this is resolved by calling ToString() that may be overridden in the parameter class specifically for this purpose. However the approach may not always be applicable, neither does it control the parameter name.

正如@CarterMedlin指出的那样,非基本类型的动作参数可能与查询参数没有一对一的绑定。目前,这可以通过调用ToString()来解决,该ToString()可以在参数类中专门为此目的而重写。但是,该方法可能并不总是适用,也不控制参数名称。

To resolve the issue, you can declare the following interface:

要解决此问题,您可以声明以下界面:

public interface IUrlSerializable
{
    Dictionary<string, string> GetQueryParams();
}

and implement it in the parameter class:

并在参数类中实现它:

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
        };
    }
}

And respective changes to ActionHelper:

对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);
    }
}

As you can see, it still has a fallback to ToString(), when the parameter class does not implement the interface.

如您所见,当参数类未实现接口时,它仍然具有ToString()的回退。

Usage:

用法:

ActionHelper<HomeController>.GetUrl(controller => controller.HomeAction, new Model
{
    Id = 1,
    Name = "example"
});

#4


0  

I need to make sure routeValues are processed properly, and not always treated like querystring values. But, I still want to make sure the actions match the controllers.

我需要确保正确处理routeValues,并不总是像querystring值一样对待。但是,我仍然希望确保操作与控制器匹配。

My solution is to create extension overloads for Url.Action.

我的解决方案是为Url.Action创建扩展重载。

<a href="@(Url.Action<MyController>(x=>x.MyAction))">Button Text</a>

I have overloads for single parameter actions for different types. If I need to pass routeValues...

我有针对不同类型的单个参数操作的重载。如果我需要传递routeValues ...

<a href="@(Url.Action<MyController>(x=>x.MyAction, new { myRouteValue = myValue }))">Button Text</a>

For actions with complicated parameters that I haven't explicitly created overloads for, the types need to be specified with the controller type to match the action definition.

对于我没有明确创建重载的复杂参数的操作,需要使用控制器类型指定类型以匹配操作定义。

<a href="@(Url.Action<MyController,int,string>(x=>x.MyAction, new { myRouteValue1 = MyInt, MyRouteValue2 = MyString}))">Button Text</a>

Of course, most of the time the action stays within the same controller, so I still just use nameof for those.

当然,大多数时候动作都停留在同一个控制器中,所以我仍然只使用nameof。

<a href="@Url.Action(nameof(MyController.MyAction))">Button Text</a>

Since routeValues don't necessarily match the action parameters, this solution allows for that flexibility.

由于routeValues不一定与动作参数匹配,因此该解决方案允许这种灵活性。

Extension Code

扩展代码

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);
    }
}

#1


17  

I like James' suggestion of using an extension method. There is just one problem: although you're using nameof() and have eliminated magic strings, there's still a small issue of type safety: you're still working with strings. As such, it is very easy to forget to use the extension method, or to provide an arbitrary string that isn't valid (e.g. mistyping the name of a controller).

我喜欢詹姆斯关于使用扩展方法的建议。只有一个问题:虽然你正在使用nameof()并且已经消除了魔术字符串,但仍然存在类型安全的小问题:你仍在使用字符串。因此,很容易忘记使用扩展方法,或者提供无效的任意字符串(例如,错误输入控制器的名称)。

I think we can improve James' suggestion by using a generic extension method for Controller, where the generic parameter is the target controller:

我认为我们可以通过对Controller使用通用扩展方法来改进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);
    }
}

The usage is now much cleaner:

使用现在更清洁:

this.Action<HomeController>(nameof(ActionName));

#2


6  

Consider an extension method:

考虑一种扩展方法:

public static string UrlName(this Type controller)
{
  var name = controller.Name;
  return name.EndsWith("Controller") ? name.Substring(0, name.Length - 10) : name;
}

Then you can use:

然后你可以使用:

Url.Action(nameof(ActionName), typeof(HomeController).UrlName())

#3


1  

All the solutions I have seen so far have one drawback: while they make changing controller's or action's name safe, they do not guarantee consistency between those two entities. You may specify an action from a different controller:

到目前为止,我所看到的所有解决方案都有一个缺点:虽然它们使控制器或动作的名称变得安全,但它们并不能保证这两个实体之间的一致性。您可以从其他控制器指定操作:

public class HomeController : Controller
{
    public ActionResult HomeAction() { ... }
}

public class AnotherController : Controller
{
    public ActionResult AnotherAction() { ... }

    private void Process()
    {
        Url.Action(nameof(AnotherAction), nameof(HomeController));
    }
}

To make it even worse, this approach cannot take into account the numerous attributes one may apply to controllers and/or actions to change routing, e.g. RouteAttribute and RoutePrefixAttribute, so any change to the attribute-based routing may go unnoticed.

更糟糕的是,这种方法不能考虑可能应用于控制器的多种属性和/或改变路由的动作,例如, RouteAttribute和RoutePrefixAttribute,因此对基于属性的路由的任何更改都可能会被忽视。

Finally, the Url.Action() itself does not ensure consistency between action method and its parameters that constitute the URL:

最后,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" });
    }
}

My solution is based on Expression and metadata:

我的解决方案基于表达式和元数据:

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());
    }
}

This prevents you from passing wrong parameters to generate a URL:

这可以防止您传递错误的参数来生成URL:

ActionHelper<HomeController>.GetUrl(controller => controller.HomeAction, 1, "example");

Since it is a lambda expression, action is always bound to its controller. (And you also have Intellisense!) Once the action is chosen, it forces you to specify all of its parameters of correct type.

由于它是lambda表达式,因此action始终绑定到其控制器。 (而且你也有Intellisense!)一旦选择了动作,它就会强制你指定所有正确类型的参数。

The given code still does not address the routing issue, however fixing it is at least possible, as there are both controller's Type.Attributes and MethodInfo.Attributes available.

给定的代码仍然没有解决路由问题,但是修复它至少是可能的,因为控制器的Type.Attributes和MethodInfo.Attributes都可用。

EDIT:

编辑:

As @CarterMedlin pointed out, action parameters of non-primitive type may not have a one-to-one binding to query parameters. Currently, this is resolved by calling ToString() that may be overridden in the parameter class specifically for this purpose. However the approach may not always be applicable, neither does it control the parameter name.

正如@CarterMedlin指出的那样,非基本类型的动作参数可能与查询参数没有一对一的绑定。目前,这可以通过调用ToString()来解决,该ToString()可以在参数类中专门为此目的而重写。但是,该方法可能并不总是适用,也不控制参数名称。

To resolve the issue, you can declare the following interface:

要解决此问题,您可以声明以下界面:

public interface IUrlSerializable
{
    Dictionary<string, string> GetQueryParams();
}

and implement it in the parameter class:

并在参数类中实现它:

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
        };
    }
}

And respective changes to ActionHelper:

对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);
    }
}

As you can see, it still has a fallback to ToString(), when the parameter class does not implement the interface.

如您所见,当参数类未实现接口时,它仍然具有ToString()的回退。

Usage:

用法:

ActionHelper<HomeController>.GetUrl(controller => controller.HomeAction, new Model
{
    Id = 1,
    Name = "example"
});

#4


0  

I need to make sure routeValues are processed properly, and not always treated like querystring values. But, I still want to make sure the actions match the controllers.

我需要确保正确处理routeValues,并不总是像querystring值一样对待。但是,我仍然希望确保操作与控制器匹配。

My solution is to create extension overloads for Url.Action.

我的解决方案是为Url.Action创建扩展重载。

<a href="@(Url.Action<MyController>(x=>x.MyAction))">Button Text</a>

I have overloads for single parameter actions for different types. If I need to pass routeValues...

我有针对不同类型的单个参数操作的重载。如果我需要传递routeValues ...

<a href="@(Url.Action<MyController>(x=>x.MyAction, new { myRouteValue = myValue }))">Button Text</a>

For actions with complicated parameters that I haven't explicitly created overloads for, the types need to be specified with the controller type to match the action definition.

对于我没有明确创建重载的复杂参数的操作,需要使用控制器类型指定类型以匹配操作定义。

<a href="@(Url.Action<MyController,int,string>(x=>x.MyAction, new { myRouteValue1 = MyInt, MyRouteValue2 = MyString}))">Button Text</a>

Of course, most of the time the action stays within the same controller, so I still just use nameof for those.

当然,大多数时候动作都停留在同一个控制器中,所以我仍然只使用nameof。

<a href="@Url.Action(nameof(MyController.MyAction))">Button Text</a>

Since routeValues don't necessarily match the action parameters, this solution allows for that flexibility.

由于routeValues不一定与动作参数匹配,因此该解决方案允许这种灵活性。

Extension Code

扩展代码

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);
    }
}