Quantcast
Channel: typesafe » .Net
Viewing all articles
Browse latest Browse all 10

Solving the Trailing Slash Problem in ASP.Net MVC using a Custom UrlRoutingModule

$
0
0

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.



Viewing all articles
Browse latest Browse all 10

Trending Articles