I think the title is clear enough to understand the subject of this post :).
How to implement Vanity URL on ASP.NET MVC!
(Surprised, huh?)
The type of vanityURL implemented is that one used by twitter and facebook, something like:
www.yourwebsite.com/mazzimo
will show the profile of the user "mazzimo"
www.yourwebsite.com/mazzimo/messagges
will show mazzimo's messages
...and so on. Vanity URL's are awesome, but not so easy to manage: the vanity URLs must not override the existing urls.
This is our example: This is the controller "UsersController" that has inside all the "pages" about the user:
public class UsersController : Controller
{
public ActionResult Index(string name)
{
}
public ActionResult Messages(string name)
{
}
}
Now let's look inside the "App_Start/RouteConfig.cs" file. It will look like this:
public static void RegisterRoutes(RouteCollection routes)
{
//Default route
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
This function initializes the routing rules that allows MVC to call the right controller depending on the incoming url. The default settings is "{controller_class}/{method_name}/{id_parameter}".
leaving this default setting as it is, to reach the methods on our example controller for the user "mazzimo" we'll need to call the urls "www.yourwebsite.com/Users?name=mazzimo" and "www.yourwebsite.com/Users/Messages/?name=mazzimo"...not really pretty, isn't it?
To let the Urls working as we want we need to follow 3 steps:
- Change the default rule to let it be ignored in case no controller matches with that one indicated on the url;
- Add another rule that will call the "UsersController" class taking the first part of the RawUrl as the "name" parameter (this will be placed after the default rule, so it will be executed only if the default rule is ignored);
- (Not Mandatory) Change this new rule to let it be ignored if there is no user that matches that particular username;
Step 1: Change the default rule
We need to call a different overload of the "MapRoute" function that uses the "costraints", objects that allows us to put additional checks on the values passed through the url. We'll use the costraint to check if the controller name indicated in the url actually exists. Usually we can indicate a regular expression as costraint but in this case we will create a custom class: This class must implement the interface "IRouteConstraint":
public class ControllerConstraint : IRouteConstraint
{
static List<string> ControllerNames = GetControllerNames();
private static List<string> GetControllerNames()
{
List<string> result = new List<string>();
foreach(Type t in System.Reflection.Assembly.GetExecutingAssembly().GetTypes())
{
if( typeof(IController).IsAssignableFrom(t) && t.Name.EndsWith("Controller"))
{
result.Add(t.Name.Substring(0, t.Name.Length - 10).ToLower());
}
}
return result;
}
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
return ControllerNames.Contains(values[parameterName].ToString().ToLower());
}
}
We will pass an instance of this class when we call the "MapRoute" method inside the "RouteConfig.cs" file:
//Default route
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
//add this line
constraints: new { controller = new ControllerConstraint() }
);
Step 2: add the "vanity URL" rule
We need to call the "MapRoute" method again but AFTER the default rule:
public static void RegisterRoutes(RouteCollection routes)
{
//Default route
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
constraints: new { controller = new ControllerConstraint() }
);
//Additional route
routes.MapRoute(
name: "VanityUrl",
url: "{name}/{action}/{id}",
defaults: new { controller = typeof(UsersController).Name.Replace("Controller", ""), action = "Index", id = UrlParameter.Optional }
);
Now all already works: request will be routed to our controller "Users" only if the default rule is ignored.
Step 3: Username check
Want to be more precise? all you need to do is changing the second rule adding another class as costraint.
Is pretty much similar from what we have seen before for the controllers, the only difference is that instead of taking the name of the controllers through reflection, we will make a query to check if the input value matches with an actual username.
public class UserNameConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
//do the query
//if values[parameterName].ToString().ToLower() matches an username
//return True
//else
//return False
}
}
The second rule will look like this:
//Additional route
routes.MapRoute(
name: "VanityUrl",
url: "{name}/{action}/{id}",
defaults: new { controller = typeof(UsersController).Name.Replace("Controller", ""), action = "Index", id = UrlParameter.Optional }
constraints: new { name = new UserNameConstraint() }
);