How a Method Becomes An Action

asp.net mvc, asp.net 0 comments suggest edit

This is one of them “coming of age” stories about how a lowly method becomes a full fledged Action in ASP.NET MVC. You might think the two things are the same thing, but that’s not the case. It is not just any method gets to take the mantle of being an Action method.

Routing

Like any good story, it all begins at the beginning with Routing. By default, one of the routes defined in the MVC project template has the following URL pattern:

{controller}/{action}/{id}

When a request comes in and matches that route, we populate a dictionary of route values (accessible via the RequestContext) based on this route. For example, if a request comes in for:

/home/list/123

We add the key “action” with the value “list” to the route values dictionary (We’ve also added “home” as the value for “controller”, but that’s for another story. This is the story of the action.) At the heart of it, an action is just a string. That’s how it starts out after all, as a sub string of the URL.

Later on, when the request is handed of to MVC, MVC interprets the value in the route values for “action” to be the action name. In this case, it knows that the request should be handled by the action “list”. Contrary to popular belief, this does not necessarily mean that a method named List will handle this request,as we’ll soon see.

Action Method Selection

Once we’ve identified the name of the action, we need to identify a method that can respond to that action. This is the job of the ControllerActionInvoker.

By default, the invoker simply uses reflection to find a public method on a class that derives from Controller which has the same name (case insensitive) as the current action.

Like many things within this framework, you can tweak this default behavior.

ActionNameAttribute

Introduced in ASP.NET MVC CodePlex Preview 5 which we just released, applying this attribute to a method allows you to specify the action that the method handles.

For example, suppose you want to have an action named View, this would conflict with the View method of Controller. An easy way to work around this issue without having to futz with routing or method hiding is to do the following:

[ActionName("View")]
public ActionResult ViewSomething(string id)
{
  return View();
}

The ActionNameAttribute redefines the name of this action to be “View”. Thus this method is invoked in response to requests for /home/view, but not for /home/viewsomething. In the latter case, as far as the action invoker is concerned, an action method named “ViewSomething” does not exist.

One consequence of this is that if you’re using our conventional approach to locate the view that corresponds to this action, the view should be named after the action, not after the method. In the above example (assuming this is a method of HomeController), we would look for the view ~/Views/Home/View.aspx by default.

This attribute is not required on an action method. Implicitly, the name of a public method serves as the action name for that method.

ActionSelectionAttribute

We’re not done yet matching the action to a method. Once we’ve identified all methods of the Controller class that match the current action name, we need to whittle the list down further by looking at all instances of the ActionSelectionAttribute applied to the methods in the list.

This attribute is an abstract base class for attributes which provide fine grained control over which requests an action method can respond to. The API for this method is quite simple and consists of a single method.

public abstract class ActionSelectionAttribute : Attribute
{
  public abstract bool IsValidForRequest(ControllerContext controllerContext
    , MethodInfo methodInfo);
}

At this point, the invoker looks for any methods in the list which contain attributes which derive from this attribute and calls the IsValidForRequest() method on each attribute. If any attribute returns false, the method that the attribute is applied to is removed from the list of potential action methods for the current request.

At the end, we should be left with one method in the list, which the invoker then invokes. If more than one method can handle the current request, the invoker throws an exception indicating the problem. If no method can handle the request, the invoker calls HandleUnknownAction() on the controller.

The ASP.NET MVC framework includes one implementation of this base attribute, the AcceptVerbsAttribute.

AcceptVerbsAttribute

This is a concrete implementation of ActionSelectionAttribute which uses the current HTTP request’s http method (aka verb) to determine whether or not a method is the action that should handle the current request.

This allows for having two methods of the same name (different parameters of course) to both be actions, but respond to different HTTP verbs.

For example, we may want two versions of the Edit method, one which renders the edit form, and the other which handles the request when that form is posted.

[AcceptVerbs("GET")]
public ActionResult Edit(string id)
{
  return View();
}

[AcceptVerbs("POST")]
public ActionResult Edit(string id, FormCollection form)
{
  //Save the item and redirect…
}

When a POST request for /home/edit is received, the action invoker creates a list of all methods of the controller that match the “edit” action name. In this case, we would end up with a list of two methods. Afterwards, the invoker looks at all of the ActionSelectionAttribute instances applied to each method and calls the IsValidForRequest() method on each. If each attribute returns true, then the method is considered valid for the current action.

For example, in this case, when we ask the first method if it can handle a POST request, it would respond with false because it only handles GET requests. The second method responds with true because it can handle the POST request and it is the one selected to handle the action.

Helpers

One consequence to keep in mind when using helpers which use our routing API to generate URLs is that the parameters for all of these helpers take in the action name,not the method name. So if I want to render the URL to the following action:

[ActionName("List")]
public ActionResult ListSomething()
{
  //...
}

Use “List” and not “ListSomething” as the action name.

<!-- WRONG! -->
<%= Url.Action("ListSomething") %>

<!-- RIGHT! -->
<%= Url.Action("List") %>

This is one reason you’ve seen the MVC team resistant to including helper methods, such as Url<T>(…), that use an expression to define the URL of an action. The action is not necessarily equivalent to a method on the class with the same name.

Summary

So in the end, an action is a logical concept that represents an event caused by the user (such as clicking a link or posting a form) which is eventually mapped to a method which handles that user event.

It’s convenient to think of an action as a method of the same name, but they are distinct concepts. A lowly method can become an action by the power of its own name (aka name dropping), but in this egalitarian framework, any method, no matter its name, can handle a particular action, by merely using the ActionNameAttribute.

Technorati Tags: aspnetmvc,routing

Found a typo or error? Suggest an edit! If accepted, your contribution is listed automatically here.

Comments

avatar

37 responses

  1. Avatar for Simone Chiaretta
    Simone Chiaretta August 29th, 2008

    Phil, I think the routing rule in the sample should be {controller}/{action}/{id}, no {id}/{action}/{id}
    And about the ActionName attribute. This is a nice way to add many view with the same name, but I really like the correspondence between methods and actions since it allowed the Url<t> helpers that provided compile-time check and intellisense when writing redirect to actions and url to actions.
    So, not sure whether to like this new feature or not :)

  2. Avatar for davidalpert
    davidalpert August 29th, 2008

    I have to agree with Simone, initially i liked the "ActionName" attribute.
    In the end, however, if that means that i cannot find a way to do this:

    foreach (int productID in (List<int>)listOfProductIDs) {
    string route = Html.ResolveRoute<productscontroller>(h => h.ShowProduct(productID);
    }

    then i'd drop the attribute like a hot potato in favor of a lambda-based method to resolve routes in my views.

  3. Avatar for haacked
    haacked August 29th, 2008

    Simone, thanks for the tip. I corrected my post. The thing is, an action has always been a string that just happened to map to a method on a controller. All along, you could have written action filters that change that string, thus destroying the tenuous connection between an action name and method.
    For strongly typed checking, there are other techniques you can use, such as a static class of custom made route values so that all your strings are in one place. etc... Use your imagination, I think you'll figure out other ideas.
    Remember, the request starts off as a string, even if we took away ActionName, that doesn't suddenly make the URL helpers work in all cases.

  4. Avatar for shawn
    shawn August 29th, 2008

    Great post Phil.
    I agree completely with the "action is just a logical construct" concept as it enforces loose coupling from View to Controller.
    I'm relieved to see the ASP.NET MVC architects finally help express this for developers via these new additions in preview 5.
    Frankly, while undeniably tempting, I consider controller lambda expressions being used in a view as a gross violation of the pattern!

  5. Avatar for Justice~!
    Justice~! August 29th, 2008

    "This is one reason you’ve seen the MVC team resistant to including helper methods, such as Url<t>(…), that use an expression to define the URL of an action. The action is not necessarily equivalent to a method on the class with the same name."
    Can't it be resolved nonetheless if the action we're calling is decorated with an attribute, though? I mean, it seems odd to me that if we're breaking down the action in that way we wouldn't be able to substitute the ActionName assigned to that method. At least, I would figure that's the way it should be.

  6. Avatar for Jonathan
    Jonathan August 29th, 2008

    Excellent article Phil.
    "The ASP.NET MVC framework includes one implementation of this base attribute, the AcceptVerbsAttribute."
    Let's not forget about the NonActionAttribute :)

  7. Avatar for Andy
    Andy August 29th, 2008

    Phil,
    I am really keen to use MVC but my hesitation revolves around the native integration with jQuery ?
    i.e. MS Ajax is integrated heavily with ASP.NET but it seems that jQuery is not. This is something that I find really frustrating as native integration should really exist for a lib as popular as jQuery ?
    Is there plans to integrate this natively or ? As currently, the benefits combining MS Ajax and MVC far outweigh the benefits of using jQuery and MVC.
    Thoughts? Love a post or some further indication from you in this regard.
    Cheers,
    Andy

  8. Avatar for CarlH
    CarlH August 29th, 2008

    A agree with Simone, compiletime checking of URLs is a absolute requirement, especially for a huge site.

  9. Avatar for Timothy Khouri
    Timothy Khouri August 29th, 2008

    Phil, I know I bug you a lot about what else I wish to see in MVC... but this feature is just another
    one of those 100% solid and well done items.
    Back in the Preview 4 starter template in the Register action of the AccountController... the whole

    if (Request.HttpMethod != "POST")
    {
    return View();
    }

    thing was really bugging me. It didn't seem like a natural way to handle the scenario
    (oh, btw, that wasn't changed in the Preview 5 template).
    Thanks for the awesome product,
    -Timothy Khouri
    P.S. I'm not just trying to butter you up because I've seen your angry side.

  10. Avatar for Magnus M&#229;rtensson
    Magnus M&#229;rtensson August 30th, 2008

    I love the new ActionName and AcceptedVerbs attributes!
    They really help make a well evolved controller more intuitive to coders! I have a sample at my site where the Index action GETs from one method and POSTs to another!
    Cool stuff for sure!
    Cheers, /Magnus

  11. Avatar for mvcFan
    mvcFan August 31st, 2008

    Although they are distinct concepts, it's practical to have an action as a method of the same name.
    With ActionName, the problem has just moved: You still have to find a name for "ViewSomething".
    I think it's much simpler to adopt a solid naming convention. Instead of using "View", you can still use "Index", "Details", "MyView" or anything else.
    Personally, the 2 methods with the same name ("Edit") is another bad example, as "Edit" can be used to display the form and "Update" to update the database (see weblogs.asp.net/.../...ontroller-action-names.aspx).
    According to me, ActionName was a waste of time.
    I don't know when I will update my application to Preview 5, as it seems that there are a lot of breaking changes (See http://forums.asp.net/1146.aspx). Now I am a bit afraid.

  12. Avatar for haacked
    haacked September 1st, 2008

    @Andy There’s nothing preventing anyone from using JQuery with MVC. I even did a screencast where I demonstrated its use. In P5, one of our motivations for refactoring all the Ajax helpers to be extension methods in their own namespace was to enable people to write their own helpers using JQuery.
    I'd love to do it myself, but I imagine others out there will jump on this opportunity to supply the community with those helpers.

  13. Avatar for Steve Sanderson
    Steve Sanderson September 1st, 2008

    > Although they are distinct concepts, it's practical
    > to have an action as a method of the same name.
    I have to agree with mvcFan. Not yet going so far as to say "ActionName was a waste of time", but I am curious about what the intended use case is here.
    As with anything in software, an extra abstraction layer adds flexibility, but it also adds an extra level of cognitive friction. Why would developers want to have to keep track of which action methods correspond to which "action names"? When placing a link to another action method, doesn't the developer usually want to say *exactly which* actual C# method should be called, not obscuring that intention behind a mapping layer that might in fact cause the link to go to some other method? There is such a thing as too many abstraction layers :)
    The fact that this destroys any possibility of strongly-typed link helpers and redirection helpers, reducing the whole API to a string-based system, is unfortunate too. Is it really worth it? Who was asking for action names? Can you cite compelling use cases?
    I'm sure you'll come along and explain how action names are great and then we'll all be happy. Until then, curious...

  14. Avatar for Justice~!
    Justice~! September 1st, 2008

    I'm still waiting to hear about the possibility of using the [ActionName] as something to aid in compile-time URL generation. Sincerely though the lack of compile-time checking on URLs seems like a very strange design decision, defeating the potential of using MVC on a large site. Am I misreading this?

  15. Avatar for Steve Sanderson
    Steve Sanderson September 1st, 2008

    @Justice: You're right, having generic helpers infer the action name from a detected [ActionName] attribute would be possible, but it would make a mockery of the whole system. You'd then use one action name when you reference it with a string, and a different name when you reference it with a lambda expression.

  16. Avatar for haacked
    haacked September 1st, 2008

    @Steve @Justice The ability to have the strongly typed URL helpers was already an illusion, though a compelling one. There were many other ways that an action might not correspond to a method of the same name. For example, a filter might have tweaked things, a custom route could tweak things, a controller factory could tweak things, a custom action invoker could tweak things.



    An action truly is just a string, as is an URL. Routing and MVC are loosely coupled. We could try really really hard to tightly couple the two and make the action-to-method a one-to-one mapping, but to make that really work, we'd have to remove all those layers of extensibility.


    Hence, we're moving the strongly typed helpers to MvcFutures. If you don't use any of these extensibility layers, they'll sort of work. But we don't want to support them given that at all layers, you can cause a breakage inadverdently via the extensibility layers. The fact that an action really is a logical concept is not something we made up. It's been that way all along. We're just trying to expose that fact so people are aware of it and the consequences of treating it otherwise.

  17. Avatar for Justice~!
    Justice~! September 2nd, 2008

    @Steve - I agree with you but then again I'm not all that keen on the "ActionName" aspect. I respect that my bias may be showing! ;)
    @Haacked - I do agree with you from the coupling standpoint (in fact, I've done some interesting stuff with routing that couldn't use the strongly typed stuff). I think it's probably a case of the MVC team finding its footing because I remember presentations, etc. making a very big deal about strongly typed routing styles.
    As a secondary concern, I noticed Image and ImageButton were moved to Futures as well in p4 (which I assume has to do with the weird REsolveUrl() in MVC vs. in WebForms). The implication seems to be that the Extension methods are moved into futures because they're not going to be further supported. Is that what is happening with Image and ImageButton?

  18. Avatar for haacked
    haacked September 2nd, 2008

    @Justice that is indeed the case. The MvcFutures project is where we put features that we're considering for future versions of ASP.NET MVC.
    Some of the helpers we don't feel add all that much value for the QA cost it would entail. It seems like some of those are trivial, but when you add up a bunch of trivial items, it starts to total alot of work.

  19. Avatar for Aybar Dumlu
    Aybar Dumlu September 11th, 2008

    cool stuff, thank you
    _________________________
    http://www.aybardumlu.com

  20. Avatar for code
    code September 17th, 2008

    this is the kind of stuff i want to read about on this blog

  21. Avatar for celik
    celik September 20th, 2008

    I like AcceptVerbs attribute. Makes your code cleaner instaed of if then else loops.

  22. Avatar for Steve
    Steve September 24th, 2008

    Strongly typed helpers was one of the biggest wins of asp.net mvc.
    If anything, I'd like the checking down on builds.
    I am using this everyday, real life stuff. Now theories, and these strongly typed helpers are extremely valuable.

  23. Avatar for Kaitlyn
    Kaitlyn September 29th, 2008

    Great explanation, thank you for the post

  24. Avatar for Sime
    Sime December 23rd, 2008

    ActionName attribute is a good candidate for REST approach !

  25. Avatar for Nik Radford
    Nik Radford February 5th, 2009

    Hmm, but even using the helper methods, couldn't you still use reflection to discover the actual action name?

  26. Avatar for Tiendq
    Tiendq February 12th, 2009

    I agree with mvcFan too, you don't need to use Edit again for update action and then use HTTP verb to find correct action method.

  27. Avatar for buy liquor online
    buy liquor online December 16th, 2009

    that really was a "coming of age" story!

  28. Avatar for Dave
    Dave January 31st, 2010

    Why doesn't it respect the controller name in the lookup for the action.
    If you have a base controller with common actions you cannot override any of these actions from derived controllers.
    you get the ambiguous action methods error?

  29. Avatar for Brian Kendig
    Brian Kendig March 9th, 2010

    Phil said: "An action truly is just a string." Okay, then I want some function call that'll generate an action string to refer to a specific controller method.
    I don't often tweak action names via filters, custom routes, controller factories, or custom action invokers. I *do* often use links from web pages to other web pages. Having to use "magic strings" for this is not a workable option for large sites; there's no type checking of arguments, and it's very hard to trace down all the places which call a specific method if I change that method's name or arguments.
    It seems right now that strongly-typed link helpers are somewhat broken (in MVC 1, at least); an action I invoke via Html.RenderAction<MyController>(c => c.MyAction()) will have any Html.ActionLink() calls in its view spit out empty hrefs. I was hoping things would improve in MVC 2, but instead it looks like MVC 2 is moving farther from this and closer to magic strings!
    Am I supposed to tough up and tolerate magic strings, or is there a best-of-both-worlds way out there?

  30. Avatar for Property auctions
    Property auctions August 26th, 2010

    I too think that both method and action are the same.Thanks for the info you have made.

  31. Avatar for mvcvm
    mvcvm September 7th, 2011

    The ControllerFactory and ControllerActionInvoker are not quite clever enough to detect the difference in arguments when posting from a strongly typed view:
    "... The current request for action X on controller type Y is ambiguous between the following action methods: X(MyModel) on type Y and X(TheModel) on type TheController ..."

    public class TheController: Controller
    {
    [HttpPost]
    public virtual ActionResult X(TheModel model)
    { ... }
    }
    public class Y: TheController
    {
    [HttpPost]
    public virtual ActionResult X(MyModel model)
    { ... }
    }
    As the signatures differ, neither overriding nor hiding is involved.
    This would be useful for inheriting from any frameworks and then quickly adding special behaviour.

  32. Avatar for Saeed Neamati
    Saeed Neamati January 30th, 2012

    This resolves my issue, for having similar methods, with similar names and signatures, but having different HttpMethod attributes on top of them. One method should server HTTP Get, and another method the HTTP Post (post parameters would be extracted from Request object, rather than letting MVC bind them).
    This scheme was impossible. But by using ActionNameAttribute, I think I'll make it work.
    Thanks

  33. Avatar for Rubens Mariuzzo
    Rubens Mariuzzo January 29th, 2013

    Well explained!

  34. Avatar for Vaibhav Shah
    Vaibhav Shah August 27th, 2013

    Nicely Explained.

  35. Avatar for Avinash
    Avinash February 2nd, 2014

    Having an attribute ActionName("") is similar to writing action with string specified in ActionName attribute. Look at the following example still there is still ambiguity

    [ActionName("OverideAction")]
    public ActionResult MyAction(string s)

    {

    ViewBag.Message = "Only String Parameter:" + s;

    return View();

    }

    [ActionName("OverideAction")]

    public ActionResult MyAction(string s , int i)

    {

    ViewBag.Message = "One String Parameter:" + s + "One integer :" + i.ToString();

    return View();

    }

  36. Avatar for simon
    simon March 8th, 2015

    Looks great, though does this support the use of the Html.ActionLink<>(Expression) extensions method in your views?

  37. Avatar for Tridip Bhattacharjee
    Tridip Bhattacharjee September 17th, 2015

    by attribute routing we can handle this

    [HttpGet]
    [Route("View")]
    // Returns/View
    public ActionResult View()
    {
    // I wouldn't really do this but it proves the concept.
    int id = 7026;
    return View(id);
    }

    [HttpGet]
    [Route("View/{id:int}")]
    // Returns/View/7003
    public ActionResult View(int id)
    {
    //.....
    }

    [HttpGet]
    [Route("View/{id:Guid}")]
    // Returns/View/99300046-0ba4-47db-81bf-ba6e3ac3cf01
    public ActionResult View(Guid id)
    {
    //.....
    }