Reputation: 10749
Is there a recommended way to use the new
nameof()
expression in ASP.NET MVC for controller names?
Url.Action("ActionName", "Home") <------ works
vs
Url.Action(nameof(ActionName), nameof(HomeController)) <----- doesn't work
obviously it doesn't work because of nameof(HomeController) converts to "HomeController" and what MVC needs is just "Home".
Upvotes: 50
Views: 14447
Reputation: 246
A simple constant does the job or, if you prefer, an interface:
internal interface IBaseController { public string Name { get; } }
public class MyController : IBaseController
{
public const string NAME = "My";
// or
public string Name { get => "My"; }
...
}
No calculations needed:
<a asp-controller="@MyController.Name" asp-action="@nameof(MyController.Index)">Close</a>
Upvotes: 1
Reputation: 125
I use something like this and it works: Url.Action(nameof(ActionName), nameof(HomeController).Replace("Controller", string.Empty))
Upvotes: 0
Reputation: 2481
For those looking how to do this in ASP.NET Core, try this: https://github.com/ivaylokenov/AspNet.Mvc.TypedRouting
@(Html.ActionLink<HomeController>("Home page", c => c.Index()))
Upvotes: 0
Reputation: 1245
A take on @James answer:
Instead, using a string extension method: Returns the controller names prefix otherwise the parameter passed in.
/// <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);
}
Upvotes: 0
Reputation: 1497
Building off of Gigi's answer (which introduced type-safety for controllers), I went an extra step. I very much like T4MVC, but I never liked having to run the T4 generation. I like code generation, but it's not native to MSBuild, so build servers have a hard time with it.
I re-used the generic concept and added in an Expression
parameter:
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);
}
}
An example call for the above would look like:
public virtual ActionResult Index()
{
return this.RedirectToAction<JobController>( controller => controller.Index() );
}
If JobController
didn't have Index
, you'd run into a compiler error. That's probably the only advantage this has over the previous answer - so it's another stupidity check. It'd help you stop using JobController
if JobController
didn't have Index
. Also, it'll give you intellisense when looking for the action.
--
I also added in this signature:
public static ActionResult RedirectToAction<TController>(this TController controller, Expression<Func<TController, ActionResult>> expression)
where TController : Controller
This allows a simpler way of typing in actions for the current controller, without needing to specify the type. The two can be used side-by-side:
public virtual ActionResult Index()
{
return this.RedirectToAction(controller => controller.Test());
}
public virtual ActionResult Test()
{
...
}
--
I was asked in a comment if this supported parameters. The answer for the above is no. However, I hacked away real fast to create a version that could parse the parameters. This is the adjusted method:
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);
}
I haven't personally tested it (but Intellisense makes it appear that it would compile). To sum up, the code looks at all the parameters for the method, and creates an ExpandoObject that contains all of the parameters. The values are determined from the passed in expression, by calling each as an independent lambda expression by using the original parameters of the master expression. You then compile and invoke the expression, and store the resulting value in the ExpandoObject. The results are then passed into the built-in helpers.
Upvotes: 1
Reputation: 2292
All the solutions I have seen so far have one drawback: while they make changing controller's or action's name safe, they do not guarantee consistency between those two entities. You may specify an action from a different controller:
public class HomeController : Controller
{
public ActionResult HomeAction() { ... }
}
public class AnotherController : Controller
{
public ActionResult AnotherAction() { ... }
private void Process()
{
Url.Action(nameof(AnotherAction), nameof(HomeController));
}
}
To make it even worse, this approach cannot take into account the numerous attributes one may apply to controllers and/or actions to change routing, e.g. RouteAttribute
and RoutePrefixAttribute
, so any change to the attribute-based routing may go unnoticed.
Finally, the Url.Action()
itself does not ensure consistency between action method and its parameters that constitute the URL:
public class HomeController : Controller
{
public ActionResult HomeAction(int id, string name) { ... }
private void Process()
{
Url.Action(nameof(HomeAction), new { identity = 1, title = "example" });
}
}
My solution is based on Expression
and metadata:
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());
}
}
This prevents you from passing wrong parameters to generate a URL:
ActionHelper<HomeController>.GetUrl(controller => controller.HomeAction, 1, "example");
Since it is a lambda expression, action is always bound to its controller. (And you also have Intellisense!) Once the action is chosen, it forces you to specify all of its parameters of correct type.
The given code still does not address the routing issue, however fixing it is at least possible, as there are both controller's Type.Attributes
and MethodInfo.Attributes
available.
EDIT:
As @CarterMedlin pointed out, action parameters of non-primitive type may not have a one-to-one binding to query parameters. Currently, this is resolved by calling ToString()
that may be overridden in the parameter class specifically for this purpose. However the approach may not always be applicable, neither does it control the parameter name.
To resolve the issue, you can declare the following interface:
public interface IUrlSerializable
{
Dictionary<string, string> GetQueryParams();
}
and implement it in the parameter class:
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
};
}
}
And respective changes to 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);
}
}
As you can see, it still has a fallback to ToString()
, when the parameter class does not implement the interface.
Usage:
ActionHelper<HomeController>.GetUrl(controller => controller.HomeAction, new Model
{
Id = 1,
Name = "example"
});
Upvotes: 3
Reputation: 12495
I need to make sure routeValues
are processed properly, and not always treated like querystring
values. But, I still want to make sure the actions match the controllers.
My solution is to create extension overloads for Url.Action
.
<a href="@(Url.Action<MyController>(x=>x.MyAction))">Button Text</a>
I have overloads for single parameter actions for different types. If I need to pass routeValues
...
<a href="@(Url.Action<MyController>(x=>x.MyAction, new { myRouteValue = myValue }))">Button Text</a>
For actions with complicated parameters that I haven't explicitly created overloads for, the types need to be specified with the controller type to match the action definition.
<a href="@(Url.Action<MyController,int,string>(x=>x.MyAction, new { myRouteValue1 = MyInt, MyRouteValue2 = MyString}))">Button Text</a>
Of course, most of the time the action stays within the same controller, so I still just use nameof
for those.
<a href="@Url.Action(nameof(MyController.MyAction))">Button Text</a>
Since routeValues
don't necessarily match the action parameters, this solution allows for that flexibility.
Extension Code
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);
}
}
Upvotes: 6
Reputation: 29501
I like James' suggestion of using an extension method. There is just one problem: although you're using nameof()
and have eliminated magic strings, there's still a small issue of type safety: you're still working with strings. As such, it is very easy to forget to use the extension method, or to provide an arbitrary string that isn't valid (e.g. mistyping the name of a controller).
I think we can improve James' suggestion by using a generic extension method for Controller, where the generic parameter is the target controller:
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);
}
}
The usage is now much cleaner:
this.Action<HomeController>(nameof(ActionName));
Upvotes: 27
Reputation: 7543
Consider an extension method:
public static string UrlName(this Type controller)
{
var name = controller.Name;
return name.EndsWith("Controller") ? name.Substring(0, name.Length - 10) : name;
}
Then you can use:
Url.Action(nameof(ActionName), typeof(HomeController).UrlName())
Upvotes: 8