Manipulating Action Method Parameters

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.

What others have said

Requesting Gravatar... Vladan Strigo Feb 22, 2010 12:05 AM
# re: Manipulating Action Method Parameters
Hi Phil,

How about generating this kind of url with slugs? via Html.ActionLink or something?

How to make that part work?



Thanks,
Vladan

Requesting Gravatar... Koistya `Navin Feb 22, 2010 12:44 AM
# re: Manipulating Action Method Parameters
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.
Requesting Gravatar... Rik Hemsley Feb 22, 2010 1:23 AM
# re: Manipulating Action Method Parameters
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?
Requesting Gravatar... ali62b Feb 22, 2010 2:13 AM
# re: Manipulating Action Method Parameters
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-page-about-nothing/
Requesting Gravatar... Daniel Liuzi Feb 22, 2010 3:01 AM
# re: Manipulating Action Method Parameters
I you don't want to hit the database twice, can you pass the whole object rather than just the ID?
Requesting Gravatar... Daniel Liuzzi Feb 22, 2010 3:04 AM
# re: Manipulating Action Method Parameters
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.
Requesting Gravatar... Eduardo Mendes those Feb 22, 2010 7:09 AM
# re: Manipulating Action Method Parameters
Usally you use the ID or one String as parameters.
Nice solution "for both".
Requesting Gravatar... Zach Curtis Feb 22, 2010 7:37 AM
# re: Manipulating Action Method Parameters
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.

Requesting Gravatar... Simon Labrecque Feb 22, 2010 9:38 AM
# re: Manipulating Action Method Parameters
I do something like that in my application, but with a ModelBinder. When should one use one over the other?
Requesting Gravatar... haacked Feb 22, 2010 9:42 AM
# re: Manipulating Action Method Parameters
@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.
Requesting Gravatar... BjartN Feb 22, 2010 9:56 AM
# re: Manipulating Action Method Parameters
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" }
);
Requesting Gravatar... Cymen Feb 22, 2010 9:59 PM
# re: Manipulating Action Method Parameters
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).
Requesting Gravatar... Phillip Feb 23, 2010 1:19 AM
# re: Manipulating Action Method Parameters
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.
Requesting Gravatar... Ian Mckay Feb 23, 2010 5:24 AM
# re: Manipulating Action Method Parameters
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.
Requesting Gravatar... Gleb Feb 23, 2010 5:57 AM
# re: Manipulating Action Method Parameters
Thanks for a great post, Phil! Just what I was looking for. Any ideas how to make this attribute reusable?
Requesting Gravatar... Alexander Feb 24, 2010 1:26 AM
# re: Manipulating Action Method Parameters
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.
Requesting Gravatar... Asif Ashraf Feb 26, 2010 1:59 AM
# Local parameter values dynamically
/*
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. }

}

Requesting Gravatar... Pita.O Feb 26, 2010 7:54 AM
# re: Manipulating Action Method Parameters
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.

Requesting Gravatar... Lee C. Feb 26, 2010 8:19 AM
# re: Manipulating Action Method Parameters
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
Requesting Gravatar... rtpHarry Feb 26, 2010 10:40 AM
# re: Manipulating Action Method Parameters
For those of you that are wondering how to do this but are still working with WebForms you can read my tutorial here.
Requesting Gravatar... slipy Feb 28, 2010 4:52 AM
# re: Manipulating Action Method Parameters
I agree with Alexander, that implementation is easier.
Requesting Gravatar... Lenen Mar 02, 2010 6:09 AM
# re: Manipulating Action Method Parameters
rtpHarry, thanks for that link. It was a long read, but it made a lot of thing more clear.
Requesting Gravatar... SEOINKÖLN Mar 09, 2010 2:50 AM
# re: Manipulating Action Method Parameters
super!!!
Requesting Gravatar... AGRITURISMO Mar 09, 2010 2:58 AM
# re: Manipulating Action Method Parameters
weiter so
Requesting Gravatar... Bedrijf Met Schulden May 29, 2010 7:55 AM
# re: Manipulating Action Method Parameters
thanks for the sample project (Y) Now I can test with manipulating action method parameters.
Requesting Gravatar... andrew Jun 12, 2010 3:19 AM
# re: Manipulating Action Method Parameters
I think url rewrite( www.example.com/title) without any numbers etc will be the best from seo position!
escorts toronto
Requesting Gravatar... Randy Stewart Jul 08, 2010 1:10 PM
# re: Manipulating Action Method Parameters
People who don't factor in the importance of high quality rewritten urls drive me crazy! Thanks for covering this!

What do you have to say?

(will show your gravatar)
Please add 5 and 1 and type the answer here: