muonlab » 2010 » March

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 :)

There are plenty of discussions on why you should do this, which I’m not going to cover here.

What I am interested in discussing is a solution to the endless overriding of Equals and GetHashCode in each of your entities. Not only does this pollute the object with noise, but its a nightmare to maintain.

I use GUIDs for my Id columns, which is very convenient as you don’t have to goto the database for clues as to what the next unique ID may be.

Normally, when overriding GetHashCode you have to compare all your entity’s properties and when comparing for equality, you have to do the same because transient objects don’t yet have an ID.

This got me thinking, why even bother letting NH assign the ID? If you create the ID in your entities’ constructor GetHashCode and Equals become very easily overridden and can be abstracted to a base class, like so:

public abstract class Entity
{
	public virtual Guid Id { get; protected set; }

	protected Entity()
	{
		this.Id = Guid.NewGuid();
	}

	public override int GetHashCode()
	{
		return this.Id.GetHashCode();
	}

	public override bool Equals(object obj)
	{
		if (ReferenceEquals(this, obj))
			return true;

		var entity = obj as Entity;
		if (ReferenceEquals(null, entity))
			return false;

		return entity.Id.Equals(this.Id);
	}
}

For this to work, you must set your NHibernate ID Generator mapping to “Assigned”, like so:

<id name="Id" type="System.Guid">
    <column name="Id" />
    <generator class="assigned" />
</id>

For all you optimizers out there, you can still use alternate GUID algorithms like Comb :)

Thoughts?

I’m fed up of writing:

<%= Html.Encode(bla) %>

throughout my views. Not only is it messy, but ASP.NET’s default behaviour of “be as insecure as possible” means you have to remember to do this everywhere. In addition to this, it simply uses:

System.Web.HttpUtility.HtmlEncode()

underneath, which isn’t particularly good at preventing XSS.

OpenRasta (a brilliant alternative to MVC which you should be using) has an excellent solution to this problem, by using a custom CSharCodeProvider to help with view compilation.

Below is a simplified version of the code OpenRasta uses, demonstrating how you can get automatic HTML encoding of all code expressions in your views. This also works for WebForms.

It uses an IoC service locator to request an arbitrary IHtmlEncoder. This allows you to use whatever encoding library you like, such as Microsoft AntiXss.

public class AutoHtmlEncodingCSharpCodeProvider : CSharpCodeProvider
{
	public AutoHtmlEncodingCSharpCodeProvider()
	{
	}

	public AutoHtmlEncodingCSharpCodeProvider(IDictionary<string, string> providerOptions) : base(providerOptions)
	{
	}

	public override void GenerateCodeFromStatement(CodeStatement statement, TextWriter writer, CodeGeneratorOptions options)
	{
		var codeExpressionStatement = statement as CodeExpressionStatement;
		if (codeExpressionStatement != null)
		{
			var methodInvokeExpression = codeExpressionStatement.Expression as CodeMethodInvokeExpression;
			if (methodInvokeExpression != null)
			{
				if (methodInvokeExpression.Method.MethodName == "Write" && methodInvokeExpression.Parameters.Count == 1)
				{
					var parameter = methodInvokeExpression.Parameters[0] as CodeSnippetExpression;

					if ((parameter != null) && (!string.IsNullOrEmpty(parameter.Value)))
						parameter.Value = "global::" + GetType().FullName + ".PreProcessObject(this, " + parameter.Value + ")";
				}
			}
		}

		base.GenerateCodeFromStatement(statement, writer, options);
	}

	public static string PreProcessObject(object source, object value)
	{
		if(value is Raw)
			return ((Raw)value).Value;

		var encoder = ServiceLocator.Current.TryGetInstance<IHtmlEncoder>();
		if (encoder != null)
			return encoder.HtmlAttributeEncode(value.ToString());

		return HttpUtility.HtmlAttributeEncode(value.ToString());
	}
}

public class Raw
{
	public string Value { get; set; }

	public static explicit operator Raw(string text)
	{
		return new Raw { Value = text };
	}

	public static implicit operator string(Raw output)
	{
		return output.Value;
	}
}

You need to register this in your Web.config like so:

<system.codedom>
	<compilers>
		<compiler language="c#;cs;csharp" extension=".cs" warningLevel="4" type="MyAssembly.AutoHtmlEncodingCSharpCodeProvider, MyAssembly">
			<providerOption name="CompilerVersion" value="v3.5" />
			<providerOption name="WarnAsError" value="false" />
		</compiler>
	</compilers>
</system.codedom>

Then in your pages, you can do this:

<p><%= "<script>alert('i'm encoded, so i wont popup');</script>" %></p>
<p><%= (Raw)"<strong>i'm bold because im escaped with (Raw)!</strong>" %></p>

Which will be rendered as:

<script>alert(‘i’m encoded, so i wont popup’);</script>
i’m bold because im escaped with (Raw)!

There you go!

Because this code has been inspired by/copied from/a modification of code from OpenRasta, according to its license I must reproduce the copyright notice. If you also wish to use this code, you must do the same.

 Permission is hereby granted, free of charge, to any person obtaining
 a copy of this software and associated documentation files (the
 "Software"), to deal in the Software without restriction, including
 without limitation the rights to use, copy, modify, merge, publish,
 distribute, sublicense, and/or sell copies of the Software, and to
 permit persons to whom the Software is furnished to do so, subject to
 the following conditions:
 
 The above copyright notice and this permission notice shall be
 included in all copies or substantial portions of the Software.
 
 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.