When designing your URL scheme you might consider trying to make hackable URLs. By this I mean to say that your URLs should be able to support user manipulation. A URL such as Product/Details/2
is fine for displaying a product, but it doesn't show any information for Google to slurp up when ranking your site; it also doesn't provide any additional navigational abilities to your viewing customer, it just shows a product. Now take the following URL: Catalog/Computers/Laptops/Toshiba/Satellite-650
. With this sort of URL, you are providing your customers with the ability to remove the specific laptop Satellite-650
and display all of the laptops that Toshiba makes. You could further remove the Toshiba
from the URL and see a list of all the Laptops
that the site offers. And you could remove Laptops
and see all the computers, and so on.
One thing to keep in mind when creating these types of URLs is that you do have to do additional work. Each of these URLs become additional routes with corresponding actions in a controller, which ultimately map to a view that displays the appropriate data. If you provide hackable routes for one area of your site, your customers are inevitably going to try to hack other areas of your site's URLs. Be consistent in the implementation of hackable URLs on your site.
Also, you may have noticed in the preceding URL examples that we had some data separated by slashes, while other data was separated by dashes. The general idea is that data separated by slashes can be removed to view additional results. Data separated by dashes can't be modified for additional results.
In order to demonstrate how hackable URLs work, we first need to create an application, which is capable of showing data for each state of the URL. In this case, we will create a product catalog that allows you to navigate from a category to a subcategory, to manufacturers, to products, to a product—and back again.
I will describe the tasks for setting up the routing in detail, but I will point to the other sections of code only as needed. To see the full example code, feel free to take a look at the source code included with the book.
Global.asax.cs
and take a look at the RegisterRoutes
method. We will start by creating a new route. This first route will handle displaying all of the categories in our product catalog. As that is a simple task, we will end up with a simple route—this route will be called Categories
. We will start all of these product catalog routes with the word Catalog
to keep this set of routes separate from any other routes we might create for this application. Also, all of these routes will map to the Home Controller regardless of what is stated in the URL. The last thing for us to be concerned with is the action we want to use for this route. We will specify the ShowAllCategories
action (which we will need to create). As you can see, this is a parameterless route.Global.asax.cs:
routes.MapRoute("Categories", "Catalog", new {controller = "Home", action = "ShowAllCategories"});
ShowAllCategories
. This action will make a call into the ProductCatalogFactory
to get the appropriate instance of a view-specific model object called AllCategoriesModel
. The ProductCatalogFactory
makes a call into a non-specific Repository
class that generates instances of various classes as we need them using NBuilder. The action then returns the properly hydrated instance of the AllCategoriesModel
to the view.Global.asax.cs:
public ActionResult AllCategories() { AllCategoriesModel model = ProductCatalogFactory.AllCategoriesModelFactory(); return View(model); }
Models/ViewModels/AllCategoriesModel.cs:
public class AllCategoriesModel { public List<Category> Categories { get; set; } }
Models/Services/ProductCatalogFactory.cs:
public static AllCategoriesModel AllCategoriesModelFactory() { AllCategoriesModel result = new AllCategoriesModel(); result.Categories = new Repository().GetCategories(); return result; }
Models/Repositories/Repository.cs:
public List<Category> GetCategories() { return Builder<Category> .CreateListOfSize(10) .Build() .ToList(); }
Models/Domain/Category.cs:
public class Category { public Guid CategoryId { get; set; } public string Name { get; set; } }
Views/Home/AllCategories.aspx:
... <h2>Catalog</h2> <fieldset> <legend>Categories</legend> <% foreach(Category category in Model.Categories) { %> <div class="display-label"> <%:Html.Label(category.Name) %> </div> <div class="display-field"> <%:Html.ActionLink(category.Name,"AllSubCategoriesForCategory", new {categoryName = category.Name}) %> </div> <% } %> </fieldset> ...
Global.asax:
routes.MapRoute("Category", "Catalog/{categoryName}", new {controller = "Home", action = "AllSubCategoriesForCategory"});
Global.asax:
routes.MapRoute("SubCategory", "Catalog/{categoryName}/{subcategoryName}", new { controller = "Home", action = "AllManufacturersForSubCategory" });
Global.asax:
routes.MapRoute("Manufacturer", "Catalog/{categoryName}/{subcategoryName}/{manufacturerName}", new { controller = "Home", action = "AllProductsForManufacturer" });
Global.asax:
routes.MapRoute("Product", "Catalog/{categoryName}/" + "{subcategoryName}/{manufacturerName}/" + "{productName}", new { controller = "Home", action = "ProductDetails" });
When a request comes in, it is matched to routes in the route dictionary. The collection of routes are interrogated from the top down, or from the first item added to the list. The thing to keep in mind here is that the order is very important. If you have a very general route specification at the top of the list that matches every request, then none of your other routes will be acted on. You usually want to have the more specific routes at the top of the list and the less specific routes towards the bottom.
You could have done this in a slightly different manner. Had you been willing to put all of your display logic into one view, you could have also consolidated all of your routes into one specification. This would ultimately give you less code to manage but it would also yield less overall flexibility. In this case, we can use different views for each route, which may be good or bad depending on what you are building.