So, first of all very special thanks to @StephenMuecke for giving the hint for slugs and also the url he suggested.
I would like to post my approach which is a mix of that url and several other articles.
My goal was to be able to have the user enter a url like:
/product/123
and when the page loads to show in the address bar something like:
/product/my-awsome-product-name-123
I checked several web sites that have this behaviour and it seems that a 301 Moved Permanently
response is used in all i checked. Even SO as shown in my question uses 301
to add the title of the question. I thought that there would be a different approach that would not need the second round trip....
So the total solution i used in this case was:
I created a SlugRouteHandler
class which looks like:
public class SlugRouteHandler : MvcRouteHandler
{
protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
{
var url = requestContext.HttpContext.Request.Path.TrimStart('/');
if (!string.IsNullOrEmpty(url))
{
var slug = (string)requestContext.RouteData.Values["slug"];
int id;
//i care to transform only the urls that have a plain product id. If anything else is in the url i do not mind, it looks ok....
if (Int32.TryParse(slug, out id))
{
//get the product from the db to get the description
var product = dc.Products.Where(x => x.ID == id).FirstOrDefault();
//if the product exists then proceed with the transformation.
//if it does not exist then we could addd proper handling for 404 response here.
if (product != null)
{
//get the description of the product
//SEOFriendly is an extension i have to remove special characters, replace spaces with dashes, turn capital case to lower and a whole bunch of transformations the SEO audit has requested
var description = String.Concat(product.name, "-", id).SEOFriendly();
//transform the url
var newUrl = String.Concat("/product/",description);
return new RedirectHandler(newUrl);
}
}
}
return base.GetHttpHandler(requestContext);
}
}
From the above i need to also create a RedirectHandler
class to handle the redirections. This is actually a direct copy from here
public class RedirectHandler : IHttpHandler
{
private string newUrl;
public RedirectHandler(string newUrl)
{
this.newUrl = newUrl;
}
public bool IsReusable
{
get { return true; }
}
public void ProcessRequest(HttpContext httpContext)
{
httpContext.Response.Status = "301 Moved Permanently";
httpContext.Response.StatusCode = 301;
httpContext.Response.AppendHeader("Location", newUrl);
return;
}
}
With this 2 classes i can transform product ids to SEO friendly urls.
In order to use these i need to modify my route to use the SlugRouteHandler
class, which leads to :
Call SlugRouteHandler
class from the route
routes.MapRoute(
name: "Specific Product",
url: "product/{slug}",
defaults: new { controller = "Product", action = "Index" }
).RouteHandler = new SlugRouteHandler();
Here comes the use of the link @StephenMuecke mentioned in his comment.
We need to find a way to map the new SEO friendly url to our actual controller. My controller accepts an integer id but the url will provide a string.
We need to create an Action filter to handle the new param passed before calling the controller
public class SlugToIdAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var slug = filterContext.RouteData.Values["slug"] as string;
if (slug != null)
{
//my transformed url will always end in '-1234' so i split the param on '-' and get the last portion of it. That is my id.
//if an id is not supplied, meaning the param is not ending in a number i will just continue and let something else handle the error
int id;
Int32.TryParse(slug.Split('-').Last(), out id);
if (id != 0)
{
//the controller expects an id and here we will provide it
filterContext.ActionParameters["id"] = id;
}
}
base.OnActionExecuting(filterContext);
}
}
Now what happens is that the controller will be able to accept a non numeric id which ends in a number and provide its view without modifying the content of the controller. We will only need to add the filter attribute on the controller as shown in the next step.
I really do not care if the product name is actually the product name. You could try fetching the following urls:
\product\123
\product\product-name-123
\product\another-product-123
\product\john-doe-123
and you would still get the product with id 123
, though the urls are different.
Next step is to let the controller know that it has to use a special filer
[SlugToId]
public ActionResult Index(int id)
{
}