2

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.

Shaun Luttin
  • 133,272
  • 81
  • 405
  • 467
Daniel Congrove
  • 3,519
  • 2
  • 35
  • 59

3 Answers3

3

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.

cwharris
  • 17,835
  • 4
  • 44
  • 64
  • 1
    His point was using the attribute *without needing a magic string*. Moving a magic string to a constant, still makes it a magic string and if the controller changes then the code breaks at runtime. – Erik Philips Aug 20 '18 at 20:10
  • 1
    @ErikPhilips Do you not think the magic string is there already with `[ActionName("Contact2")]`? – Jeppe Stig Nielsen Aug 20 '18 at 20:20
  • ASP.NET MVC Attribute-based routing is based on magic strings. I try to avoid answering xy questions with an answer to x. – cwharris Aug 20 '18 at 20:35
  • 1
    @ErikPhilips actually, no it doesn't break, since the constant is the source of truth for the whole operation. You use the constant in the action name. It can't change unless the constant changes. The only way this breaks is if another library compiles against the constant, and then the constant changes without the other library recompiling against the new value. – cwharris Aug 20 '18 at 21:05
  • You're right in terms of the controller. I've done stuff like this a lot. It's *a* solution, probably the best solution if he really doesn't care about the required magic string, but it doesn't solve that problem. – Erik Philips Aug 20 '18 at 21:42
  • This answer provides a good alternative approach but provides an incorrect answer. It's incorrect for two reasons. First, it says we can't programmatically get an attribute value without using magic strings when in fact we can do that. Second, it shows how to get a constant value, which happens also to set the attribute value, but which might not always set the attribute value in the future; if a developer changes the attribute to `ActionName("Contact3")`, then the code will produce an incorrect result. – Shaun Luttin Aug 20 '18 at 21:56
  • 1
    @ShaunLuttin although I agree that someone could change it, when I go this route with other developers, it becomes a convention that we don't change these things... similarly we don't make controllers that don't end in `Controller`. – Erik Philips Aug 20 '18 at 22:07
  • Actually, the answer does not state state you *can't get* the attribute value. It says that the attribute value *itself* is a magic string. – cwharris Aug 20 '18 at 22:07
  • You probably shouldn't go around removing constants and replacing them with inline strings. I imagine more than just your controllers would break if you started doing that. At the very least, it's a maintenance nightmare. It also defeats the purpose using a constant in the first place. it's akin to removing part of your app config and expecting things to still work. – cwharris Aug 20 '18 at 22:09
  • @cwharris You make good points: 1. it is a bad idea to replace constants with string literals, and 2. the attribute name already is a string literal. The problem remains, though, that the answer to the question-as-stated is incorrect, and I downvoted on that basis. I would change my down from an up-vote if you were to say something like this: "Yes. You can programmatically get the attribute without using magic strings; but, I would recommend setting the attribute value via a constant and getting the constant instead." – Shaun Luttin Aug 20 '18 at 22:31
2

...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.

Shaun Luttin
  • 133,272
  • 81
  • 405
  • 467
  • 2
    It could be `typeof(HomeController).GetMethod(nameof(HomeController.Contact)).GetCustomAttribute().Name` (requires using directive to `System.Reflection`). – Jeppe Stig Nielsen Aug 20 '18 at 20:17
1

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)))
Erik Philips
  • 53,428
  • 11
  • 128
  • 150
  • Does the syntax need to be `@{ ... }` or `@( ... )` for the generic call to work? See https://learn.microsoft.com/en-us/aspnet/core/mvc/views/razor?view=aspnetcore-2.1#implicit-razor-expressions for what I mean. – Shaun Luttin Aug 20 '18 at 20:56
  • 1
    Whats to say someone doesn't use `Controller` at the end of their class name, or that they mistype it? `"Controller"` is still a magic string. – cwharris Aug 20 '18 at 21:03
  • @ShaunLuttin Yes it needs (), thanks, I do not have VS in front of me. – Erik Philips Aug 20 '18 at 21:23
  • @cwharris it's an obvious down-vote for hard coding "controller" which is nether in the OP requires and is also *[hard-coded* asp.net-mvc](https://social.technet.microsoft.com/wiki/contents/articles/37419.asp-net-core-understanding-controller-suffix-in-model-view-controller.aspx).. which it seems you are unfamiliar with... so no it's not a concern to anyone. – Erik Philips Aug 20 '18 at 21:35
  • 1
    @ErikPhilips I am familiar with the convention, but it does not strictly apply if/when using attributed based routing.. which it seems you are unfamiliar with... so it is a concern to some. – cwharris Aug 20 '18 at 21:51
  • How does creating a controller have anything to do with routing? Those are completely two different independent features. You asked *Whats to say someone doesn't use Controller at the end of their class name, or that they mistype it* and my point was *it won't work then*... mvc is **hard-coded** to look for controller suffix (included link to source code). – Erik Philips Aug 20 '18 at 21:54
  • I guess I should point out that ASP.NET MVC is highly customizable, and that class can be overridden or swapped out through dependency injection. Although that's probably not going to happen. However, ASP.NET Core doesn't care if your controller ends with "Controller" if you're using attribute-based routing. – cwharris Aug 20 '18 at 22:03
  • In that case everything can be overwritten and customized to the point nothing is recognizable, your argument is really no were near the OP question at this point. – Erik Philips Aug 20 '18 at 22:05
  • This is a question about `asp-net-core`. It applies, and it is near the OPs question, because this answer wouldn't work if someone misspelled `"Controller"` in their class name, and figuring out why everything works correctly except the route could end up being a very tedious debugging process. – cwharris Aug 20 '18 at 22:15
  • @cwharris again you miss the point if someone mistypes it, IT WON'T WORK as a controller. The OP has not mentioned anything about changed MVC defaults about how controllers are created so your point is completely irrelevant. I'm sorry you don't get it, I'm not explaining it again. – Erik Philips Aug 20 '18 at 22:16
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/178383/discussion-between-cwharris-and-erik-philips). – cwharris Aug 20 '18 at 22:17
  • @ErikPhilips OP is talking about mvc-core, and the controller class name doesn't matter at all when using attribute based routing. I'm sorry you don't get it. I'm not explaining it again. :) – cwharris Aug 21 '18 at 19:32