Подтвердить что ты не робот

Как использовать С# nameof() с 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, просто "Главная" .

4b9b3361

Ответ 1

Мне нравится предложение Джеймса использовать метод расширения. Есть только одна проблема: хотя вы используете nameof() и устранили магические строки, все еще существует небольшая проблема безопасности типов: вы все еще работаете со строками. Таким образом, очень легко забыть использовать метод расширения или предоставить произвольную строку, которая недействительна (например, ошибочное имя контроллера).

Я думаю, что мы можем улучшить предложение Джеймса, используя общий метод расширения для контроллера, где общий параметр является целевым контроллером:

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

Ответ 2

Рассмотрим метод расширения:

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

Ответ 3

Все решения, которые я видел до сих пор, имеют один недостаток: хотя они делают безопасным изменение имени контроллера или имени действия, они не гарантируют согласованности между этими двумя объектами. Вы можете указать действие от другого контроллера:

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.

EDIT:

Как отметил @CarterMedlin, параметры действия не-примитивного типа могут не иметь привязки к параметрам запроса "один-к-одному". В настоящее время это разрешается путем вызова 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"
});

Ответ 4

Мне нужно убедиться, что 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);
    }
}

Ответ 5

Создавая ответ 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, если бы JobController не было Index. Кроме того, это даст вам интеллектуальный смысл при поиске действия.

-

Я также добавил в этой подписи:

    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()
    {
         ...
    }