Manipulating Action Method Parameters

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

During the MVP summit, an attendee asked me for some help with a common scenario common among those building content management systems. He wanted his site to use human friendly URLs.

  http://example.com/pages/a-page-about-nothing/

instead of

  http://example.com/pages/123/

Notice how the first URL is descriptive whereas the second is not. The first URL contains a URL “slug” while the second one contains the ID for the content, typically associated with the ID in the database.

This is easy enough to set up with routing, but there’s a slight twist. He still wanted the action method which would respond to the first URL to have the integer integer ID as the parameter, not the slug. Let’s look at one possible approach to solving this.

Here’s an example of what the route might look like:

routes.MapRoute(
  "Slug", // Route name
  "pages/{slug}", // URL with parameters
  new { controller = "Home", action = "Content" } // Parameter defaults
);

Notice that the route URL contains one parameter for “slug” and no “id” parameter whatsoever. Here’s an example of the controller action that route should map to.

public ActionResult Content(int id)
{
  // Note the argument is an id, not slug
  return View();
}

Note that the action method does not accept a parameter named “slug” but instead expects an integer “id” parameter.

Fortunately, there’s an easy way to do this. Action filters, classes which derive from ActionFilterAttribute, allow hooking into the point in time after the parameters of action method have been bound, but just before the action method has been invoked. This gives us a fine opportunity to muck around with the parameters.

The following is an example of an action filter which converts a slug to an ID (you can imagine a real one would probably look it up in the database, not in a static dictionary like the sample does).

public class SlugToIdAttribute : ActionFilterAttribute
{
  static IDictionary<string, int> Slugs = new Dictionary<string, int>
  {
    {"this-is-a-slug", 100}, 
    {"another-slug", 101}, 
    {"and-another", 102}
  };

  public override void OnActionExecuting(ActionExecutingContext filterContext)
  {
    var slug = filterContext.RouteData.Values["slug"] as string;
    if(slug != null)
    {
      int id;
      Slugs.TryGetValue(slug, out id);
      filterContext.ActionParameters["id"] = id;
    }
    base.OnActionExecuting(filterContext);
  }
}

The filter overrides the OnActionExecuting method which is called just before the action method is called. The filter than grabs the slug from the route data, and looks up the corresponding id. Now all we need to do is make sure the id is passed into the action method.

Fortunately the filter context passed into this method allows us to peek into the parameters that will get passed into the action method via the ActionParameters property. Not only that, it allows us to change them!

In this case, I’m grabbing the slug from the route data, and looking up the associated id, and adding a parameter named “id” to the action parameters with the correct id value.

All I need to do now is apply this filter to the action method and when the action method is called, this id will be passed into the method.

This works whether the argument to the action method is a simple primitive type as in this example or whether it’s a complex type. I’ve included a sample project that demonstrates changing parameters to action methods via an action filter.

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

Comments

avatar

41 responses

  1. Avatar for Vladan Strigo
    Vladan Strigo February 21st, 2010

    Hi Phil,
    How about generating this kind of url with slugs? via Html.ActionLink or something?
    How to make that part work?

    Thanks,
    Vladan

  2. Avatar for Koistya `Navin
    Koistya `Navin February 21st, 2010

    I prefer not to use this approach but instead create methods in a service / data layer which work with titles, names etc. - non IDs values. This way there won't be any problems with generating links later on and it's pretty simple. Splitting parameters in ActionFilters might be confusing for those folks who look at the code first time.

  3. Avatar for Rik Hemsley
    Rik Hemsley February 21st, 2010

    Why is it necessary or desirable to pass this integer ID to the Content method? Surely if the slug uniquely identifies the entity, and is, for example, indexed in the database, the Content method will be able to pull the entity from the database just as swiftly and easily as it would by id? Isn't this just performing two queries where one would do?

  4. Avatar for ali62b
    ali62b February 21st, 2010

    As an alternate solution to this issue one can use both id an slug in url and just ignore slug at action method (its just for humans!).
    example.com/pages/123/a-pag...

  5. Avatar for Daniel Liuzzi
    Daniel Liuzzi February 21st, 2010

    I you don't want to hit the database twice, can you pass the whole object rather than just the ID?

  6. Avatar for Daniel Liuzzi
    Daniel Liuzzi February 21st, 2010

    Duh! Didn't see the "This works whether the argument to the action method is a simple primitive type as in this example or whether it’s a complex type." bit. I guess next time I'll read the whole article before posting. Cool article BTW.

  7. Avatar for Eduardo Mendes those
    Eduardo Mendes those February 21st, 2010

    Usally you use the ID or one String as parameters.
    Nice solution "for both".

  8. Avatar for Zach Curtis
    Zach Curtis February 21st, 2010

    Phil, I really like this approach. At first I was hesitant because the article titles/slugs have to be unique, but then I remembered creates a unique slug based on the article title you enter Joomla. Adding that functionality with this solution is an elegant way to handle SEF Url's. I am curious what the performance hit is however.

    One solution I used before was to just pass the slug directly to the action method and look the article up that way.

  9. Avatar for Simon Labrecque
    Simon Labrecque February 21st, 2010

    I do something like that in my application, but with a ModelBinder. When should one use one over the other?

  10. Avatar for haacked
    haacked February 21st, 2010

    @Rik perhaps there is no data access to look up the ID for the slug, but instead it's looked up in a cached dictionary. Maybe there are other situations where you really do pass in the ID to get the content, and you simply want to normalize on that method.
    The point of this blog post was to show how you can manipulate parameters to action methods, not to demonstrate the right way to build a CMS. I greatly simplified the example to make this point and it doesn't bear a big resemblance to the original problem.

  11. Avatar for BjartN
    BjartN February 21st, 2010

    Isn't this exactly the problem route constraints are created to solve ?
    routes.MapRoute(
    "Id",
    "pages/{id}",
    new { controller = "Home", action = "GetById" },
    new { id = new IntegerIdContraint()}
    );
    routes.MapRoute(
    "Slug",
    "pages/{slug}",
    new { controller = "Home", action = "GetBySlug" }
    );

  12. Avatar for Cymen
    Cymen February 22nd, 2010

    If there are a relatively few number of pages (say under a couple hundred), is there anything wrong with publishing a route for each direct page with the "object default" holding the numeric identifiers that correspond with the slug (and other helpful slug-specific data)? That is the approach I've tried and it seems to work well and avoids having to do any kind of lookup during runtime.
    So I do something like this:

    routes.MapRoute(
    "Database.Home.1",
    "Home",
    new {
    controller = "Database",
    action = "Load",
    sectionID = 1,
    sectionName = "Home",
    routeName = "Home",
    routeID = 1,
    pageID = 1
    }
    );
    routes.MapRoute(
    "Database.Contact.2",
    "Home/Contact",
    new {
    controller = "Database",
    action = "Load",
    sectionID = 1,
    sectionName = "Home",
    routeName = "Home/Contact",
    routeID = 2,
    pageID = 2
    }
    );

    Of course, this is populated from database values in a loop so the above is just an example. The feature I particularly like is using "object defaults" to store the mappings. I did wonder if there were any performance issues with stuffing the routing table with hundreds of URLs (although the small project I'm doing this on ~50).
    My thought is that this is a reasonable approach for non-category URLs (for category URLs, lean towards using numeric identifiers embedded in the URL).

  13. Avatar for Phillip
    Phillip February 22nd, 2010

    No one in here is working on a site with 100k content pages where having a properly indexed friendly URL column on the page table is going to cause any performance concerns hitting the database looking for a friendly URL instead of an ID.
    Instead of passing the ID around, use the "slug" as you call it.
    There's no need for "two database hits" looking for the value for the "slug" then using the ID to get the content.

  14. Avatar for Ian Mckay
    Ian Mckay February 22nd, 2010

    Nice post. I have done something similar for a concept CMS I am attempting to create. I needed to convert the slug into an id but I wanted a mixture of managed and unmanaged pages. So I created a custom route and handler class that does the lookup in the database and passes down the id and controller. If it doesn't find the page in the database it returns null and the default route will be matched.

  15. Avatar for Gleb
    Gleb February 22nd, 2010

    Thanks for a great post, Phil! Just what I was looking for. Any ideas how to make this attribute reusable?

  16. Avatar for Alexander
    Alexander February 23rd, 2010

    I believe it's much more easy to implement this url
    http://site.com/some-text-32/
    this url contains id at the end, and text so it's easy to implement using ASP.NET MVC and it's good for Search engines.

  17. Avatar for Asif Ashraf
    Asif Ashraf February 25th, 2010

    /*
    Hi,
    This question may sound off topic but Please anybody help me in this thing.
    Look at this method
    */
    [LogParameters("City", "Country")]
    public void DoTheHellNow(int Id, string City, string Country)
    {
    var matches = new Dictionary<string, string>();
    /*
    Note the attribute(loggable params) and note that only City and Country parameter should be added to the dictionary. I want to fill this dictionary by using "reflection". Match patamers names with those names in attribute i.e City and Country, But NOT "Id" because Id is not loggable(according to the rule defined by the attribute).
    */
    //Now take those loggable parameters and fill dictionary so that
    //Dictionary first entry should be:
    //[ Key = "City", Value ="New york" ] { The value newyork will be //sent by the caller of the method. }
    //Dictionary second entry should be:
    //[ Key = "Country", Value ="USA" ] { The value USA will be sent by //the caller of the method. }
    }

  18. Avatar for Pita.O
    Pita.O February 25th, 2010

    I am going with @BjartT: on this one. The use of Attributes for concerns that are not truly cross-cutting can be an issue here albeit a minor one.
    The major issue is around simplicity and ownership of a concern. I would suggest that how one data element in a domain maps to another is a business problem. I would do a slug-catching action method and pass the slug into a lower layer that resolves it to an appropriate id. A simple data access method overload would just clean this up and preserve the flexibility I could otherwise loose in the View.

  19. Avatar for Lee C.
    Lee C. February 25th, 2010

    The IIS.Net URL Rewrite Module seems perfectly suited to this task. Set up your simple rewrite RegEx rewrite rule and programattically set the form action.
    Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
    If Not String.IsNullOrEmpty(Request.ServerVariables("HTTP_X_ORIGINAL_URL")) Then
    form1.Action = Request.ServerVariables("HTTP_X_ORIGINAL_URL")
    End If
    End Sub

  20. Avatar for rtpHarry
    rtpHarry February 25th, 2010

    For those of you that are wondering how to do this but are still working with WebForms you can read my tutorial here.

  21. Avatar for slipy
    slipy February 27th, 2010

    I agree with Alexander, that implementation is easier.

  22. Avatar for Lenen
    Lenen March 1st, 2010

    rtpHarry, thanks for that link. It was a long read, but it made a lot of thing more clear.

  23. Avatar for SEOINK&#214;LN
    SEOINK&#214;LN March 8th, 2010

    super!!!

  24. Avatar for AGRITURISMO
    AGRITURISMO March 8th, 2010

    weiter so

  25. Avatar for Bedrijf Met Schulden
    Bedrijf Met Schulden May 28th, 2010

    thanks for the sample project (Y) Now I can test with manipulating action method parameters.

  26. Avatar for andrew
    andrew June 11th, 2010

    I think url rewrite( www.example.com/title) without any numbers etc will be the best from seo position!
    escorts toronto

  27. Avatar for Randy Stewart
    Randy Stewart July 8th, 2010

    People who don't factor in the importance of high quality rewritten urls drive me crazy! Thanks for covering this!

  28. Avatar for dheya
    dheya August 5th, 2010

    Valuable information I found, the information that you provided is excellent post and a good job. Keep on posting and thanks for sharing this article.

  29. Avatar for popotube
    popotube August 12th, 2010

    For those of you that are wondering how to do this but are still working with WebForms you can read my tutorial here. Yeah admin...

  30. Avatar for vpills
    vpills August 12th, 2010

    People who don't factor in the importance of high quality rewritten urls drive me crazy! Thanks for covering this!
    Thanks admin.

  31. Avatar for shyel
    shyel August 17th, 2010

    Pretty good post. I just stumbled upon your blog and wanted to say that I have really enjoyed reading your article

  32. Avatar for &#231;i&#231;ek sepeti
    &#231;i&#231;ek sepeti August 18th, 2010

    For those of you that are wondering how to do this but are still working with WebForms you can read my tutorial here.
    Astala Vista...

  33. Avatar for Arvid de fotograaf
    Arvid de fotograaf September 19th, 2010

    Slugs are always better than just the ID. As visitor of a website I'm looking in the URL instead of the Title tag what kind of site I am reading.
    This is a great helpful post, thanks for your tips.

  34. Avatar for jones' story
    jones' story September 23rd, 2010

    I prefer not to use this approach but instead create methods in a service / data layer which work with titles, names etc. - non IDs values. This way there won't be any problems with generating links later on and it's pretty simple. Splitting parameters in ActionFilters might be confusing for those folks who look at the code first time.

  35. Avatar for martabe
    martabe September 28th, 2010

    Recommended Post. I just stumbled upon your blog and wanted to say that I have really enjoyed reading your article

  36. Avatar for wynat
    wynat September 28th, 2010

    Valuable blog article, it was quite interesting nice work. I want to thank you for this informative read, I really appreciate sharing your post. This is my great pleasure to visit your website and to enjoy your excellent post here.

  37. Avatar for Alisha
    Alisha January 16th, 2011

    What actually these action method parameters?? i was a little bit confused with this at routing point.Please help me.

  38. Avatar for Jan Nemec
    Jan Nemec April 3rd, 2011

    Hello,
    I am new in ASP.NET MVC world. Can anyone show me how to look for a slug into a database instead static dictionary as in the example?
    Thanks for help.

  39. Avatar for fin findimi
    fin findimi June 7th, 2012

    wtwahi wa mugaku desu.computer ,nande mo wakaranai, surfing daki daisuki

  40. Avatar for Er Jyot Bhavnagarwala
    Er Jyot Bhavnagarwala August 31st, 2013

    Hi,

    I have implemented your approach. Its very good.... Its is live at http://www.ashika.com

    Here i have used the word: "buy" instead of "pages"

    But my problem is, What if I remove "pages" from the below code

    routes.MapRoute(
    "Slug", // Route name
    "pages/{slug}", // URL with parameters
    new { controller = "Home", action = "Content" } // Parameter defaults
    );

    The problem is other default urls will not work. Also Html.ActionLink will not work..... So I must have to use the word "buy" or anything else according to SEO..

    Can you please suggest??

    Please reply me fast...

  41. Avatar for aakash jalodkar
    aakash jalodkar May 13th, 2015

    hii
    this is very nice artical
    i tried it in my project and it work like magic but when i run it from iis it throws 404 error
    do you have any idea regarding this issue?