As detailed in my earlier post about exception signatures I and my colleagues take exceptions from our production servers seriously. In addition to grouping them by signatures (which helps a lot and makes triage much more pleasant) and logging them in our internal bug tracking software we also try to add relevant debug information whenever we throw an exception.
The BCL team in their infinite wisdom added the Exception.Data property. This property is simply an IDictionary which allows storing key and value pairs of any type. By default this is an empty collection which means you don’t have to worry about it being null.
Typical usage
public void LogonUser(string username, string password, string domain) { try { DataStore.LogOnUser(username, password, domain); } catch (Exception exc) { var ae = new ApplicationException("Underlying logon failed, see InnerException", exc); ae.Data["username"] = username; ae.Data["domain"] = domain; throw ae; } }
When the exception reaches our internal trac site it might look something like the image below (in our case the data dictionary will actually be added as a comment but you get the point).
This has proven to be is incredibly useful for debugging purposes but it is a bit tedious to actually write the code. Storing the exception in a variable just to get access to the data property just didn’t feel right to me and writing the same boilerplate .Data[“xyz”] = xyz was just boring.
Step 1 – The AddData extension method.
I started off by creating an extension method for System.Exception called AddData. AddData looks a bit like this (very simplified, see the end of the post for the real deal)
public static Exception AddData(this Exception exception, string key, object value) { exception.Data.Add(key, value);
// whohoo, chaining! return exception; }
This allowed me to save a few keystrokes over the first method and I would end up with something like this instead
catch(...) { throw new ApplicationException("Underlying logon failed, see InnerException", exc) .AddData("username", username) .AddData("domain", domain); }
It might not look like much but it sure helps when you want to add debug data to an already existing exception, it saves you from having to store the reference and so on but it still doesn’t solve the problem with having to write the argument names twice. This isn’t only a nuisance when writing the code; it’s very easy for the key and value to get out of sync if you’re doing refactoring since refactoring tools will only change the variable name, not the key.
My ideal solution would be something like this
catch(...) { throw new ApplicationException("Underlying logon failed, see InnerException", exc) .AddData(username) .AddData(domain); }
And have the AddData method automatically infer the proper key name but since that’s not possible I had to find another way.
Step 2 – Taking a cue from ASP.NET MVC
We continued using the AddData extension method above for a couple of weeks before it dawned on me that I could use anonymous types to skip the parameter name duplication. Anonymous types have the great ability of being able to “infer” property names when initialized.
var x = new { username = username, password = password } // Is the same as var x = new { username, password }
With this in mind I wrote an AddData extension method which accepts a single object and then uses reflection to iterate over all properties and adds their property names and values into the exception data dictionary. This allowed me to rewrite my code yet again.
catch(...) { throw new ApplicationException("Underlying data store logon failed, see InnerException", exc) .AddData(new { username, password }); }
Neat, isn’t it? This is the exact same technique that ASP.NET MVC uses for route-declarations, attributes in html helpers and more.
Reflection? Isn’t that horribly slow
Not like in the olden days. It comes with a cost but you shouldn’t be too worried since you’re probably not throwing exception often enough for it to matter anyway (and if you are, then you have bigger problems).
Teh codez
using System; using System.ComponentModel; using System.Diagnostics; namespace freakcode.Extensions { ////// Extension methods related to instances of public static class ExceptionExtensions { ///System.Exception and inherited objects. ////// Adds the supplied debug data to the exceptions data dictionary and returns /// the exception allowing chaining. /// ///The exception type, you should not need to specify this explicitly /// The exception. /// The key of the debug value to be inserted into the exceptions data dictionary. /// The value to be inserted into the exceptions data dictionary. ///key is null ///An element with the same key already exists in the Data dictionary public static T AddData<T>(this T exception, string key, object value) where T : Exception { if (exception == null) throw new ArgumentNullException("exception"); if (key == null) throw new ArgumentNullException("key"); /* Key or value is not serializable (or key is null). The default internal structure which * implements the IDictionary is going to throw an exception in Add() so instead of * throwing another exception while preparing to throw the first one we silently ignore the * error. Unless we're building in debug mode that is, then we'll fail. */ if (value != null && !value.GetType().IsSerializable) { Debug.Fail("Attempt to add non-serializable value to exception data"); } else { exception.Data.Add(key, value); } return exception; } ////// Adds the each property name and value from the supplied object to the exceptions data dictionary and returns /// the exception allowing chaining. /// ///The exception type, you should not need to specify this explicitly /// The exception. /// An object from where properties will be read and added to the exception debug data collection. ///key is null ///An element with the same key already exists in the Data dictionary public static T AddData<T>(this T exception, object values) where T : Exception { if (values == null) { // Some really nasty things can happen if you start throwing exceptions in the middle // of throwing exceptions so unless we're in debug more we'll just silently ignore it. Debug.Fail("Argument 'values' was null!"); } else { foreach (PropertyDescriptor descriptor in TypeDescriptor.GetProperties(values)) exception.AddData(descriptor.Name, descriptor.GetValue(values)); } return exception; } } }