random .NET and web development musings

I try to use natural IDs in my URIs wherever possible, like:

http://www.mydomain.com/some/resource/description

However sometimes this is not always practical, and for many of the non-public facing applications I work on it is simply unnecessary. In these cases I tend to use the GUID ID of the requested resource, like:

http://www.mydomain.com/resources/95801FAD-DA29-434F-B4EA-175C76266BB7

These 36 character GUIDs are rather ugly, here is a solution to shorten them down to 22 chars, which looks like:

http://www.mydomain.com/resources/DV0Ft9JPqkGV2Xne0Q64XA

not perfect, but much better.

N.B. My next step to shorten them further is to use a custom GUID algorithm with no machine-specific part to it, which should remove a significant number of bits. In the meantime, however…

First, we’ll introduce a ShortGuid struct based on the class described here.

public struct ShortGuid
{
	private readonly Guid guid;

	public ShortGuid(Guid guid)
	{
		this.guid = guid;
	}

	public static bool TryParse(string guid, out ShortGuid shortGuid)
	{
		Guid parsed;
		try
		{
			parsed = new Guid(Convert.FromBase64String(guid.Replace("_", "/").Replace("-", "+") + "=="));
		}
		catch
		{
			try
			{
				parsed = new Guid(guid);
			}
			catch
			{
				shortGuid = new ShortGuid();
				return false;
			}
		}

		shortGuid = new ShortGuid(parsed);
		return true;
	}

	public override string ToString()
	{
		return Convert.ToBase64String(guid.ToByteArray())
			.Substring(0, 22)
			.Replace("/", "_")
			.Replace("+", "-");
	}

	public Guid ToGuid()
	{
		return this.guid;
	}

	public static implicit operator string(ShortGuid guid)
	{
		return guid.ToString();
	}

	public static implicit operator Guid(ShortGuid shortGuid)
	{
		return shortGuid.guid;
	}
}

All pretty easy stuff. The difficulty comes when trying to get MVC to use and recognise it.

The easiest solution is to use the ShortGuid class on your resources, however the whole “short guid” concept is purely for HTTP. It certainly has no place in your domain, and has questionable presence in your resources (view models).

The next easiest solution is to use a custom route which replaces any GUID route values just before rendering, here is the code:

public class ShortGuidReplacingRoute : Route
{
	public ShortGuidReplacingRoute(string url, IRouteHandler routeHandler) : base(url, routeHandler)
	{
	}

	public ShortGuidReplacingRoute(string url, RouteValueDictionary defaults, IRouteHandler routeHandler) : base(url, defaults, routeHandler)
	{
	}

	public ShortGuidReplacingRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, IRouteHandler routeHandler) : base(url, defaults, constraints, routeHandler)
	{
	}

	public ShortGuidReplacingRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, RouteValueDictionary dataTokens, IRouteHandler routeHandler) : base(url, defaults, constraints, dataTokens, routeHandler)
	{
	}

	public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
	{
		var dictionary = new RouteValueDictionary();

		foreach(var kvp in values)
		{
			if (kvp.Value.GetType() == typeof (Guid))
				dictionary.Add(kvp.Key, new ShortGuid((Guid)kvp.Value));
			else
				dictionary.Add(kvp.Key, kvp.Value);
		}

		return base.GetVirtualPath(requestContext, dictionary);
	}
}

You then need to use this class when registering your routes, I made this convenient extension method to make life easier:

public static class RouteExtensions
{
	public static Route MapGuidReplacingRoute(this RouteCollection routes, string name, string url, object defaults)
	{
		Route route = new ShortGuidReplacingRoute(url, new MvcRouteHandler());
		route.Defaults = new RouteValueDictionary(defaults);
		route.Constraints = new RouteValueDictionary();

		routes.Add(name, route);
		return route;
	}
}

which you can use almost as normal, like so:

routes.MapGuidReplacingRoute(
	"Default",
	"{controller}/{action}/{id}",
	new { controller = "Home", action = "Index" }
	);

Then all you need is a ShortGuid model binder:

public class ShortGuidModelBinder : IModelBinder
{
	public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
	{
		if(bindingContext.ValueProvider.ContainsKey(bindingContext.ModelName))
		{
			ShortGuid shortGuid;
			var valid = ShortGuid.TryParse(bindingContext.ValueProvider[bindingContext.ModelName].AttemptedValue, out shortGuid);

			if (valid)
			{
				if(bindingContext.ModelType == typeof(Guid))
					return shortGuid.ToGuid();

				if (bindingContext.ModelType == typeof(ShortGuid))
					return shortGuid;
			}
		}

		return Guid.Empty;
	}
}

Which you register in the normal way:

ModelBinders.Binders[typeof (Guid)] = new ShortGuidModelBinder();

Job done :)

1 COMMENT
Petrus Theron
July 8, 2011
ad

This is awesome! Please update this for MVC3 (mostly breakages with IValueProvider).

Post a comment