Reputation: 3679
With the introduction of the nameof
operator in C# 6, you can get the action name programmatically without needing a magic string:
<p>@Html.ActionLink("Contact", nameof(HomeController.Contact), "Home")</p>
This works great if you do not change the name of the view.
However, is there a way to get the correct action name (and avoid magic strings) if an action method is using the [ActionName]
attribute? Perhaps through the combination of nameof()
and an extension method?
[ActionName("Contact2")]
public ActionResult Contact()
{
// ...
}
In this example, the nameof(HomeController.Contact)
will return the string "Contact" and the URL "http://localhost:2222/Home/Contact" whereas the correct URL should be "http://localhost:2222/Home/Contact2" because of the [ActionName("Contact2")]
attribute.
Upvotes: 2
Views: 4264
Reputation: 18125
No. You can't. Because the attribute name is already a magic string, and for all intents and purposes the name of the controller method is itself a magic string (in the case that you are using an implicit name for the action). Magic strings aren't bad, they're just commonly misused. In this case, you're probably better off just using a constant.
internal const string ContactActionName2 = nameof(ContactActionName2);
and
[ActionName(ContactActionName2)]
and
HomeController.ContactActionName2
should be sufficient for your use case.
However, since everyone is making a big stink about this I decided to go and find a solution which simply doesn't rely on strings (except for the one you cannot avoid relying on - the action name). I don't like this solution because 1) it's overkill, 2) it's still just accessing a string value, which can be done more simply using a constant, 3) you actually have to write out an entire method call as an expression, and 4) it allocates an expression every time you use it.
public static class ActionNameExtensions<TController>
{
public static string FindActionName<T>(Expression<Func<TController, T>> expression)
{
MethodCallExpression outermostExpression = expression.Body as MethodCallExpression;
if (outermostExpression == null)
{
throw new ArgumentException("Not a " + nameof(MethodCallExpression));
}
return outermostExpression.Method.GetCustomAttribute<ActionNameAttribute>().Name;
}
}
Example usage:
public class HomeController : Controller
{
[ActionName("HelloWorld")]
public string MyCoolAction(string arg1, string arg2, int arg4)
{
return ActionNameExtensions<HomeController>.FindActionName(
controller => controller.MyCoolAction("a", "b", 3)
);
}
}
An overload can be written to accept methods without void
returns. Although that's sort of weird, because this is suppose to be used for controller methods, which usually return a value.
Upvotes: 3
Reputation: 54656
If you don't like you can do some pretty fancy logic with Generics:
public static class HtmlHelperExtensions
{
public static IHtmlContent ActionLink<TController>(
this IHtmlHelper htmlHelper,
string linkText,
string actionName)
where TController : ControllerBase
{
var suffix = nameof(Controller);
var controllerName = typeof(TController).Name.Replace(suffix, "");
var method = typeof(TController).GetMethod(actionName);
var attributeType = typeof(ActionNameAttribute);
var attribute = method.GetCustomAttributes(attributeType, false);
actionName = attribute.Length == 0
? actionName
: (attribute[0] as ActionNameAttribute).Name;
return htmlHelper.ActionLink(linkText, actionName);
}
}
Did test this, it may complain (pretty sure it will) that the signature already exists so you may have to rename the method. In any case, you would use it like:
@(Html.ActionLink<HomeController>("Link Text", nameof(HomeController.Index)))
Upvotes: 1
Reputation: 141742
...is there a way to get the correct action name (and avoid magic strings) if an action method is using the [ActionName] attribute? Perhaps through the combination of nameof() and an extension method?
You can use reflection. Here is an inline version:
<p>@(
(
(ActionNameAttribute)(
typeof(HomeController)
.GetMethod(nameof(HomeController.Contact))
.GetCustomAttributes(typeof(ActionNameAttribute), false)[0]
)
).Name
)</p>
Here is the same operation as a razor function:
@functions {
public string GetActionName(Type controller, string methodName) {
var method = controller.GetMethod(methodName);
var attributeType = typeof(ActionNameAttribute);
var attribute = method.GetCustomAttributes(attributeType, false)[0];
return (attribute as ActionNameAttribute).Name;
}
}
<p>@GetActionName(typeof(HomeController), nameof(HomeController.Contact))</p>
Here is the same operation as a generic razor function:
@functions {
public string GetActionName<T>(string methodName) {
var controllerType = typeof(T);
var method = controllerType.GetMethod(methodName);
var attributeType = typeof(ActionNameAttribute);
var attribute = method.GetCustomAttributes(attributeType, false)[0];
return (attribute as ActionNameAttribute).Name;
}
}
<p>@(GetActionName<HomeController>(nameof(HomeController.Contact)))</p>
All that is left is to add defensive programming (e.g. null
checks) to the GetActionName
function.
Your question asked specifically about an extension method. As far as I can tell, an extension method would not provide much improvement, because we are working with types and methods whereas extension methods work on objects.
Upvotes: 2