Google analytics script

Latest jQuery CDN with code tiggling.

Tuesday, 14 December 2010

Custom action method selector attributes in Asp.net MVC

Some of the least customized but very useful extension points in Asp.net MVC are action method selector attributes. We use them all the time without actually knowing, but we may write our own as well and make seemingly complex things trivial.

A real world example

Take for instance Twitter website. When you first visit their site, you are presented with the anonymous visitor home page that provides search capabilities, trends etc.

But when you're logged in you see a completely different page. Web address is still the same, but content is completely different. You see your home twitter page where you can send tweets and read your stream.

Using custom action method selector attributes, you can easily differentiate between such requests without using multifaceted controller actions.

The real world scenario seems like a simple if branch in your controller action code but sometimes you have more than just two options. I once had four:

  1. Anonymous GET request
  2. Authenticated GET request
  3. Anonymous POST request
  4. Authenticated POST request
Doing it all in a single controller action would create a complex multifaceted action that is hard to comprehend and maintain as well. This brings me to default action method selector attributes that we all use: HttpGetAttribute and HttpPostAttribute. I'm sure you all know exactly what they do. When you decorate a certain action method with an HttpPostAttribute you know it will get executed on a POST request only. This next example is a common usage scenario:
   1:  public ActionResult Products()
   2:  {
   3:      // probably get a list of products
   4:  }
   5:   
   6:  [HttpPost]
   7:  public ActionResult Products(Product data)
   8:  {
   9:      // probably save a new product
  10:  }

With my previous scenario of four different facets (GET, POST, Anonymous and Authenticated) one could use AuthorizeAttribute but let me tell you that it wouldn't do the trick. That particular action attribute is an authorization filter, but not an action method selector. Hence you'll get a runtime exception of having too many matching action methods for a certain request.

What are action method selector attributes?

They provide means for the Asp.net MVC framework to distinguish which of the equally named action methods should be executed for the current request. Only after it determines such a method it then starts running other action filters like AuthorizeAttribute that are defined on it.

AjaxAttribute action method selector attribute

I've already pointed out two possible action method selector attributes:

  • the ones already provided by the framework to distinguish between various request types and
  • a possible custom action method selector attribute that distinguishes between anonymous and authenticated requests
But on my current project I needed a different one. I had to provide a selector between normal (as in non-Ajax) vs. Ajax requests. These are four different action methods that use this custom action method selector attribute:
   1:  /// <summary>
   2:  /// Displays a user locations view.
   3:  /// </summary>
   4:  /// <returns>Returns a view with user locations editor.</returns>
   5:  [Ajax(false)]
   6:  [HttpGet]
   7:  public ActionResult Locations()
   8:  {
   9:      return View(this.service.GetActiveUsers());
  10:  }
  11:   
  12:  /// <summary>
  13:  /// Gets particular user's locations.
  14:  /// </summary>
  15:  /// <param name="identifier">User identifier.</param>
  16:  /// <returns>Returns JSON with user locations.</returns>
  17:  [Ajax]
  18:  [HttpGet]
  19:  public ActionResult Locations(int id)
  20:  {
  21:      return Json(this.service.GetUserLocations(is), JsonRequestBehavior.AllowGet);
  22:  }
  23:   
  24:  /// <summary>
  25:  /// Adds a location to a user.
  26:  /// </summary>
  27:  /// <param name="user"><see cref="User"/> object instance.</param>
  28:  /// <param name="location"><see cref="Location"/> object instance.</param>
  29:  /// <returns>Returns JSON with newly added location.</returns>
  30:  [Ajax]
  31:  [HttpPost]
  32:  [ActionName("Locations")]
  33:  public ActionResult AddLocation(User user, Location location)
  34:  {
  35:      return Json(this.service.AddUserLocation(user, location));
  36:  }
  37:   
  38:  /// <summary>
  39:  /// Removes a user location.
  40:  /// </summary>
  41:  /// <param name="user"><see cref="User"/> object instance.</param>
  42:  /// <param name="location"><see cref="Location"/> object instance.</param>
  43:  /// <returns>Returns JSON with the removed location.</returns>
  44:  [Ajax]
  45:  [HttpDelete]
  46:  [ActionName("Locations")]
  47:  public ActionResult RemoveLocation(User user, Location location)
  48:  {
  49:      return Json(this.service.RemoveUserLocation(user, location));
  50:  }

As you can see I've abused capabilities of Asp.net MVC action method selector attributes to use the same action method name for displaying, adding and deleting user locations. So I have all in all four different action methods that are related to the same request URL. I could of course use different names for each, but I wanted to make it closer to RESTful architecture. All of these actions have very simple non-branched code that presents a smaller surface for bugs to appear.

And this is the code of the AjaxAttribute action method selector attribute:

   1:  /// <summary>
   2:  /// Represents an attribute that is used to restrict an action method so that the method handles only Ajax requests.
   3:  /// </summary>
   4:  [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
   5:  public sealed class AjaxAttribute : ActionMethodSelectorAttribute
   6:  {
   7:      /// <summary>
   8:      /// Gets or sets a value indicating whether request must be an Ajax request or not.
   9:      /// </summary>
  10:      /// <value><c>true</c> if request must be an Ajax request; otherwise, <c>false</c>.</value>
  11:      public bool MustBe { get; private set; }
  12:   
  13:      /// <summary>
  14:      /// Initializes a new instance of the <see cref="AjaxAttribute"/> class.
  15:      /// </summary>
  16:      public AjaxAttribute()
  17:          : this(true)
  18:      {
  19:          // does nothing
  20:      }
  21:   
  22:      /// <summary>
  23:      /// Initializes a new instance of the <see cref="AjaxAttribute"/> class.
  24:      /// </summary>
  25:      /// <param name="mustBe">if set to <c>true</c> then request must be an Ajax request.</param>
  26:      public AjaxAttribute(bool mustBe)
  27:      {
  28:          this.MustBe = mustBe;
  29:      }
  30:   
  31:      /// <summary>
  32:      /// Determines whether the action method selection is valid for the specified controller context.
  33:      /// </summary>
  34:      /// <param name="controllerContext">The controller context.</param>
  35:      /// <param name="methodInfo">Information about the action method.</param>
  36:      /// <returns>
  37:      /// true if the action method selection is valid for the specified controller context; otherwise, false.
  38:      /// </returns>
  39:      public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
  40:      {
  41:          if (controllerContext == null)
  42:          {
  43:              throw new ArgumentNullException("controllerContext");
  44:          }
  45:   
  46:          return controllerContext.HttpContext.Request.IsAjaxRequest() == this.MustBe;
  47:      }
  48:  }

Important usage information

You may run into a situation when you will have multiple matching action methods (either by their method name or defined ActionNameAttribute on them with a matching name) you will probably have to set action method selector attributes on all of them. Basically it's a good practice to decorate them all so a single method provides all the information one may need to know whether they're dealing with the correct action method or not. Check this simplified execution flow diagram of the ControllerActionInvoker class that actually selects action method that should be executed:

Got any questions?

This is it. Think of action method selector attributes when writing branched controller actions based on request metadata/specifics. These are possibly very good candidates for custom action method selector attributes. Use them and make your life easier. If you have any additional questions simply add a comment.

2 comments:

  1. Nice. I like the RESTful presentation to the consumer.

    ReplyDelete
  2. It's not exactly how Roy Fielding suggested it to be, but it's definitely closer.

    ReplyDelete