new - Cómo usar C#nameof() con ASP.NET MVC Url.Action
nameof in c# (4)
¿Hay alguna forma recomendada de usar el nuevo?
nameof()
¿Expresión en ASP.NET MVC para nombres de controlador?
Url.Action("ActionName", "Home") <------ works
vs
Url.Action(nameof(ActionName), nameof(HomeController)) <----- doesn''t work
obviamente no funciona debido a que nameof (HomeController) se convierte en "HomeController" y lo que MVC necesita es solo "Home" .
Considere un método de extensión:
public static string UrlName(this Type controller)
{
var name = controller.Name;
return name.EndsWith("Controller") ? name.Substring(0, name.Length - 10) : name;
}
Entonces puedes usar:
Url.Action(nameof(ActionName), typeof(HomeController).UrlName())
Me gusta la sugerencia de James de usar un método de extensión. Hay un solo problema: aunque está usando nameof()
y ha eliminado cadenas mágicas, todavía hay un pequeño problema de seguridad de tipos: todavía está trabajando con cadenas. Como tal, es muy fácil olvidar usar el método de extensión, o proporcionar una cadena arbitraria que no es válida (por ejemplo, escribir mal el nombre de un controlador).
Creo que podemos mejorar la sugerencia de James utilizando un método de extensión genérico para el Controlador, donde el parámetro genérico es el controlador de destino:
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);
}
}
El uso es ahora mucho más limpio:
this.Action<HomeController>(nameof(ActionName));
Necesito asegurarme de que los routeValues
se procesen correctamente y que no siempre se traten como valores de querystring
. Pero, todavía quiero asegurarme de que las acciones coincidan con los controladores.
Mi solución es crear sobrecargas de extensión para Url.Action
.
<a href="@(Url.Action<MyController>(x=>x.MyAction))">Button Text</a>
Tengo sobrecargas para acciones de un solo parámetro para diferentes tipos. Si necesito pasar routeValues
...
<a href="@(Url.Action<MyController>(x=>x.MyAction, new { myRouteValue = myValue }))">Button Text</a>
Para acciones con parámetros complicados para los cuales no he creado explícitamente sobrecargas, los tipos deben especificarse con el tipo de controlador para que coincida con la definición de acción.
<a href="@(Url.Action<MyController,int,string>(x=>x.MyAction, new { myRouteValue1 = MyInt, MyRouteValue2 = MyString}))">Button Text</a>
Por supuesto, la mayoría de las veces la acción permanece dentro del mismo controlador, por lo que sigo usando nameof
para esos.
<a href="@Url.Action(nameof(MyController.MyAction))">Button Text</a>
Dado que routeValues
no coincide necesariamente con los parámetros de acción, esta solución permite esa flexibilidad.
Código de extensión
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);
}
}
Todas las soluciones que he visto hasta ahora tienen un inconveniente: si bien es seguro cambiar el nombre de la acción o del controlador, no garantizan la coherencia entre esas dos entidades. Puede especificar una acción desde un controlador diferente:
public class HomeController : Controller
{
public ActionResult HomeAction() { ... }
}
public class AnotherController : Controller
{
public ActionResult AnotherAction() { ... }
private void Process()
{
Url.Action(nameof(AnotherAction), nameof(HomeController));
}
}
Para hacerlo aún peor, este enfoque no puede tener en cuenta los numerosos atributos que se pueden aplicar a los controladores y / o acciones para cambiar el enrutamiento, por ejemplo, RouteAttribute
y RoutePrefixAttribute
, por lo que cualquier cambio en el enrutamiento basado en atributos puede pasar desapercibido.
Finalmente, el Url.Action()
sí mismo no garantiza la consistencia entre el método de acción y sus parámetros que constituyen la URL:
public class HomeController : Controller
{
public ActionResult HomeAction(int id, string name) { ... }
private void Process()
{
Url.Action(nameof(HomeAction), new { identity = 1, title = "example" });
}
}
Mi solución se basa en la Expression
y los metadatos:
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());
}
}
Esto evita que pases parámetros incorrectos para generar una URL:
ActionHelper<HomeController>.GetUrl(controller => controller.HomeAction, 1, "example");
Dado que es una expresión lambda, la acción siempre está vinculada a su controlador. (¡Y también tienes Intellisense!) Una vez que se elige la acción, te obliga a especificar todos sus parámetros del tipo correcto.
El código dado aún no resuelve el problema de enrutamiento, sin embargo, solucionarlo es al menos posible, ya que hay tanto Type.Attributes
de Type.Attributes
de controlador como de Type.Attributes
de Type.Attributes
MethodInfo.Attributes
disponibles.
EDITAR:
Como señaló @CarterMedlin, es posible que los parámetros de acción de tipo no primitivo no tengan un enlace uno a uno para consultar los parámetros. Actualmente, esto se resuelve llamando a ToString()
que puede ser anulado en la clase de parámetro específicamente para este propósito. Sin embargo, el enfoque puede no ser siempre aplicable, ni tampoco controla el nombre del parámetro.
Para resolver el problema, puede declarar la siguiente interfaz:
public interface IUrlSerializable
{
Dictionary<string, string> GetQueryParams();
}
e implementarlo en la clase de parámetro:
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
};
}
}
Y los cambios respectivos a 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);
}
}
Como puede ver, todavía tiene un ToString()
a ToString()
, cuando la clase de parámetro no implementa la interfaz.
Uso:
ActionHelper<HomeController>.GetUrl(controller => controller.HomeAction, new Model
{
Id = 1,
Name = "example"
});