簡體   English   中英

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

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

有沒有推薦的方法來使用新的

nameof()

ASP.NET MVC 中控制器名稱的表達式?

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

對比

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

顯然它不起作用,因為nameof(HomeController)轉換為"HomeController"而MVC需要的只是"Home"

我喜歡James的使用擴展方法的建議 只有一個問題:盡管您使用了nameof()並消除了魔法字符串,但仍然存在類型安全的一個小問題:您仍在使用字符串。 因此,很容易忘記使用擴展方法,或提供無效的任意字符串(例如,錯誤輸入控制器的名稱)。

我認為我們可以通過使用 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));

考慮一個擴展方法:

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

更糟糕的是,這種方法無法考慮可能應用於控制器和/或更改路由的操作的眾多屬性,例如RouteAttributeRoutePrefixAttribute ,因此對基於屬性的路由的任何更改都可能被忽視。

最后, 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 表達式,所以 action 總是綁定到它的控制器。 (而且您還有 Intellisense!)一旦選擇了操作,它就會強制您指定正確類型的所有參數。

給定的代碼仍然沒有解決路由問題,但是至少可以修復它,因為控制器的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"
});

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

我沒有親自測試過它(但 Intellisense 使它看起來可以編譯)。 總而言之,該代碼查看該方法的所有參數,並創建一個包含所有參數的 ExpandoObject。 這些值是根據傳入的表達式確定的,方法是使用主表達式的原始參數將每個值作為獨立的 lambda 表達式進行調用。 然后編譯並調用表達式,並將結果值存儲在 ExpandoObject 中。 然后將結果傳遞給內置幫助程序。

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM