web-dev-qa-db-ja.com

ASP.NET MVC Url.ActionでC#nameof()を使用する方法

新しいを使用する推奨方法はありますか

nameof()

コントローラ名のASP.NET MVCでの式?

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

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

nameof(HomeController)"HomeController"に変換され、MVCが必要とするものは"Home"であるため、明らかに機能しません。

34
Mikeon

拡張メソッドを使用する Jamesの提案 が好きです。ただ1つの問題があります。nameof()を使用していて、マジックストリングを削除しましたが、タイプセーフの小さな問題がまだあります。それでも、ストリングを操作しています。そのため、拡張メソッドの使用を忘れたり、無効な任意の文字列を提供したりすることは非常に簡単です(たとえば、コントローラーの名前の入力ミス)。

コントローラに generic extension method を使用することで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));
24
Gigi

拡張メソッドを考えてみましょう:

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

これまでに見たすべてのソリューションには1つの欠点があります。それらは、変更するコントローラーまたはアクションの名前を安全にしますが、これら2つのエンティティ間の一貫性を保証するものではありません。別のコントローラーからのアクションを指定できます。

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

これはラムダ式なので、アクションは常にそのコントローラーにバインドされます。 (そしてIntellisenseもあります!)アクションが選択されると、正しいタイプのすべてのパラメーターを指定するように強制されます。

与えられたコードはまだルーティングの問題に対処していませんが、コントローラーの_Type.Attributes_と_MethodInfo.Attributes_の両方が利用可能であるため、少なくとも修正は可能です。

編集:

@CarterMedlinが指摘したように、非プリミティブ型のアクションパラメータは、クエリパラメータに1対1でバインドされていない場合があります。現在、これは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"
});
_
3
Herman Kan

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);
    }
}
3
Carter Medlin

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

JobControllerIndexがない場合、コンパイラエラーが発生します。これはおそらく、これが前の回答よりも優れている唯一の利点であるため、別の愚かさのチェックです。 JobControllerJobControllerがなかった場合、Indexの使用を停止するのに役立ちます。また、アクションを探すときにインテリセンスを提供します。

-

私はこの署名にも追加しました:

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

これにより、タイプを指定する必要なく、現在のコントローラーのアクションを入力する簡単な方法が可能になります。 2つは並べて使用できます。

    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を作成します。値は、渡された式から、マスター式の元のパラメーターを使用してそれぞれを独立したラムダ式として呼び出すことにより決定されます。次に、式をコンパイルして呼び出し、結果の値をExpandoObjectに格納します。その後、結果は組み込みヘルパーに渡されます。

1
James Haug

@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);
    }
0
Reap