There’s quite some annoyence about how ASP.Net routing handles trailing slashes. I agree, it would have been nice if one could enforce whether or not to use a trailing slash.
I’ve always taken good care of my trailing slashes. Almost every link comes from my custom UrlHelper
or extension methods (for convenience – e.g. Model.Product.GetThumbnailUrl()
). Anyway, that doesn’t mean it stopped bugging me! For one, external links (SEO) are not under your control, I don’t really know how serious this duplicate content issue is, but I’m not willing to take a chance on it. Another unsolved annoyence is when you add content pages. You always have to think twice about trailing slashes in every little link you include.
This week, I thought it was about time to go for a more generic solution. I started by googling for it and almost ended up using the new IIS7 URL Rewrite Module just like Scott suggested in his post. The main reason I didn’t was because I hate managing things in two places. Especially when their so closely related.
And then I thought: Why not let the system do what you ask it to do? How hard can it be to make the system respect you route mappings? If I map a route with url "{category}/"
or "{category}/{product}"
I want to make sure category urls end with a slash and products don’t. That simple.
It seemed to me that tuning ASP.Net MVC’s UrlRoutingModule
was the most abvious thing to do, so here’s what I’ve done:
public class UrlRoutingModule : System.Web.Routing.UrlRoutingModule { private const string SLASH = "/"; public override void PostResolveRequestCache(HttpContextBase context) { var routeData = RouteCollection.GetRouteData(context); var redirecturi = GetRedirectUri(routeData, context.Request.Url); if (redirecturi != null) { context.Response.RedirectPermanently(redirecturi); return; } base.PostResolveRequestCache(context); } private static Uri GetRedirectUri(RouteData routeData, Uri requestedUri) { if (routeData == null) return null; var route = routeData.Route as Route; if (route == null) return null; var requestedSegments = GetSegmentCount(requestedUri.AbsolutePath); var mappedSegements = GetSegmentCount(route.Url); if (requestedSegments == mappedSegements) { var slashRequired = route.Url.EndsWith(SLASH); if (slashRequired && !requestedUri.AbsolutePath.EndsWith(SLASH)) return requestedUri.Append(SLASH); if (!slashRequired && requestedUri.AbsolutePath.EndsWith(SLASH)) return requestedUri.TrimPathEnd(SLASH[0]); } else if (!requestedUri.AbsolutePath.EndsWith(SLASH)) // requestedSegments < mappedSegements { return requestedUri.Append(SLASH); } return null; } private static int GetSegmentCount(string path) { return path.Split(SLASH.ToCharArray(), StringSplitOptions.RemoveEmptyEntries).Length; } }
Note: If your route ends with optional segments, urls that don’t include them always end up with a trailing slash.
Another note: The Append
and TrimPathEnd
are Uri extension methods:
public static class UriExtensions { public static Uri Append(this Uri uri, string value) { return new Uri(GetAbsoluteUriWithoutQuery(uri) + value + uri.Query); } public static Uri TrimPathEnd(this Uri uri, params char[] trimChars) { return new Uri(GetAbsoluteUriWithoutQuery(uri).TrimEnd(trimChars) + uri.Query); } private static string GetAbsoluteUriWithoutQuery(Uri uri) { var ret = uri.AbsoluteUri; if (uri.Query.Length > 0) ret = ret.Substring(0, ret.Length - uri.Query.Length); return ret; } }
One more note:: I’m fully aware that this module alone does not solve the entire trailing slash problem, but minor additional (possibly project-specific) things like a custom UrlHelper (what I did/had to do, I spare you the details) or solutions like this method are easy enough to make it complete.
Image may be NSFW.
Clik here to view.
Clik here to view.
