如何 将 C # nameof ( ) 与 ASP.NET MVC Url.Action 一起 使用

u0njafvf  于 2022-11-19  发布在  .NET
关注(0)|答案(9)|浏览(173)

是否有推荐的使用新

nameof()

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

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

对比

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

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

sqserrrh

sqserrrh1#

我喜欢使用扩展方法的James' suggestion。只是有一个问题:虽然您使用的是nameof(),并且已经消除了幻字符串,但仍然存在一个类型安全的小问题:你仍然在使用字符串。因此,很容易忘记使用扩展方法,或者提供一个任意的无效字符串(例如,错误地键入了控制器的名称)。
我认为我们可以通过使用generic extension method for 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);
    }
}

现在的用法更加简洁:

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

nqwrtyyt2#

考虑扩展方法:

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

6vl6ewon3#

我需要确保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);
    }
}
ny6fqffe

ny6fqffe4#

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

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

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

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

更糟糕的是,该方法不能考虑可以应用于控制器和/或动作以改变路由的许多属性,例如RouteAttributeRoutePrefixAttribute,因此对基于属性的路由的任何改变可能不被注意。
最后,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表达式,所以action总是绑定到它的控制器。(而且你也有Intellisense!)一旦选择了action,它就会强制你指定正确类型的所有参数。
给定的代码仍然没有解决路由问题,但是修复它至少是可能的,因为有控制器的Type.AttributesMethodInfo.Attributes可用。

编辑:

正如@CarterMedlin所指出的,非基元类型的操作参数可能没有与查询参数的一对一绑定。目前,这是通过调用ToString()来解决的,该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"
});
w46czmvw

w46czmvw5#

Gigi's answer(它为控制器引入了类型安全)的基础上,我又前进了一步。我非常喜欢T4 MVC,但我从不喜欢必须运行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);
    }

我还没有亲自测试过(但Intellisense会让它看起来像是会编译)。总而言之,程式码会查看方法的所有参数,并建立包含所有参数的ExpandoObject。这些值是从传入的运算式决定的,方法是使用主运算式的原始参数,将每个值当做独立的Lambda运算式呼叫。然后您会编译并叫用运算式,并将结果值存储在ExpandoObject中,然后将结果传递到内置的帮助器中。

px9o7tmv

px9o7tmv6#

@詹姆斯答上一句:
而是使用字符串扩展方法:返回控制器名称前缀,否则返回传入的参数。

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

ugmeyewa7#

对于那些想知道如何在ASP.NET核心中实现这一点的人,请尝试以下操作:https://github.com/ivaylokenov/AspNet.Mvc.TypedRouting

@(Html.ActionLink<HomeController>("Home page", c => c.Index()))
5vf7fwbs

5vf7fwbs8#

我用了这样的方法,它很有效:(“控制器”,字符串.空))

f8rj6qna

f8rj6qna9#

一个简单的常量就可以完成这项工作,或者,如果您愿意,也可以使用一个接口:

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>

相关问题