简体   繁体   English

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

[英]How to use C# nameof() with ASP.NET MVC Url.Action

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对比

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"

I like James' suggestion of using an extension method.我喜欢James的使用扩展方法的建议 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.只有一个问题:尽管您使用了nameof()并消除了魔法字符串,但仍然存在类型安全的一个小问题:您仍在使用字符串。 As such, it is very easy to forget to use the extension method, or to provide an arbitrary string that isn't valid (eg mistyping the name of a controller).因此,很容易忘记使用扩展方法,或提供无效的任意字符串(例如,错误输入控制器的名称)。

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

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

I need to make sure routeValues are processed properly, and not always treated like querystring values.我需要确保正确处理routeValues ,而不总是像querystring值一样处理。 But, I still want to make sure the actions match the controllers.但是,我仍然想确保操作与控制器匹配。

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

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, eg RouteAttribute and RoutePrefixAttribute , so any change to the attribute-based routing may go unnoticed.更糟糕的是,这种方法无法考虑可能应用于控制器和/或更改路由的操作的众多属性,例如RouteAttributeRoutePrefixAttribute ,因此对基于属性的路由的任何更改都可能被忽视。

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:我的解决方案基于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());
    }
}

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.因为它是一个 lambda 表达式,所以 action 总是绑定到它的控制器。 (And you also have Intellisense!) Once the action is chosen, it forces you to specify all of its parameters of correct type. (而且您还有 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.AttributesMethodInfo.Attributes可用。

EDIT:编辑:

As @CarterMedlin pointed out, action parameters of non-primitive type may not have a one-to-one binding to query parameters.正如@CarterMedlin 指出的那样,非原始类型的操作参数可能没有与查询参数的一对一绑定。 Currently, this is resolved by calling ToString() that may be overridden in the parameter class specifically for this purpose.目前,这是通过调用ToString()解决的,该ToString()可能专门为此目的在参数类中被覆盖。 However the approach may not always be applicable, neither does it control the parameter name.然而,该方法可能并不总是适用,它也不控制参数名称。

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

Building off of Gigi's answer (which introduced type-safety for controllers), I went an extra step.基于Gigi 的答案(它为控制器引入了类型安全),我又迈出了一步。 I very much like T4MVC, but I never liked having to run the T4 generation.我非常喜欢 T4MVC,但我从不喜欢必须运行 T4 代。 I like code generation, but it's not native to MSBuild, so build servers have a hard time with it.我喜欢代码生成,但它不是 MSBuild 原生的,因此构建服务器很难使用它。

I re-used the generic concept and added in an Expression parameter:我重新使用了通用概念并添加了一个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);
    }
}

An example call for the above would look like:上述调用的示例如下所示:

    public virtual ActionResult Index()
    {
        return this.RedirectToAction<JobController>( controller => controller.Index() );
    }

If JobController didn't have Index , you'd run into a compiler error.如果JobController没有Index ,您将遇到编译器错误。 That's probably the only advantage this has over the previous answer - so it's another stupidity check.这可能是与上一个答案相比的唯一优势 - 所以这是另一个愚蠢的检查。 It'd help you stop using JobController if JobController didn't have Index .如果JobController没有Index它会帮助您停止使用JobController Also, it'll give you intellisense when looking for the action.此外,它会在寻找动作时为您提供智能感知。

-- ——

I also added in this signature:我还在这个签名中添加了:

    public static ActionResult RedirectToAction<TController>(this TController controller, Expression<Func<TController, ActionResult>> expression)
        where TController : Controller

This allows a simpler way of typing in actions for the current controller, without needing to specify the type.这允许以更简单的方式为当前控制器输入动作,而无需指定类型。 The two can be used side-by-side:两者可以并排使用:

    public virtual ActionResult Index()
    {
        return this.RedirectToAction(controller => controller.Test());
    }
    public virtual ActionResult Test()
    {
         ...
    }

-- ——

I was asked in a comment if this supported parameters.我在评论中被问到这是否支持参数。 The answer for the above is no.上面的答案是否定的。 However, I hacked away real fast to create a version that could parse the parameters.但是,我很快就创建了一个可以解析参数的版本。 This is the adjusted method:这是调整后的方法:

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

I haven't personally tested it (but Intellisense makes it appear that it would compile).我没有亲自测试过它(但 Intellisense 使它看起来可以编译)。 To sum up, the code looks at all the parameters for the method, and creates an ExpandoObject that contains all of the parameters.总而言之,该代码查看该方法的所有参数,并创建一个包含所有参数的 ExpandoObject。 The values are determined from the passed in expression, by calling each as an independent lambda expression by using the original parameters of the master expression.这些值是根据传入的表达式确定的,方法是使用主表达式的原始参数将每个值作为独立的 lambda 表达式进行调用。 You then compile and invoke the expression, and store the resulting value in the ExpandoObject.然后编译并调用表达式,并将结果值存储在 ExpandoObject 中。 The results are then passed into the built-in helpers.然后将结果传递给内置帮助程序。

A take on @James answer:对@James 的回答:

Instead, using a string extension method: Returns the controller names prefix otherwise the parameter passed in.相反,使用字符串扩展方法:返回控制器名称前缀,否则返回传入的参数。

    /// <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).Replace("Controller", string.Empty))

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM