Background
Like many developers, when I write code that catches Exceptions, I often use the Data property to add context and rethrow it to be handled at a higher level. For example in a library routine that uses ADO.net, I might have something like:
catch ( System.Exception err )
{
err.Data [ "CommandText" ] = cmd.CommandText ;
throw ;
}
This has been working as expected, but recently I had an application blow up and log a message like:
System.ArgumentException: Argument passed in is not serializable.
Parameter name: value
at System.Collections.ListDictionaryInternal.set_Item(Object key, Object value)
I scratched my head for a while, knowing that the code in question doesn't use Dictionaries directly, but does use Exception.Data . So I experimented. Try this:
System.Exception whoops = new System.Exception ( "Whoops" ) ;
whoops.Data [ "Panel" ] = new System.Windows.Forms.Panel() ;
This goes against the documentation, which clearly states "the value component of the pair can be any type of object" .
https://msdn.microsoft.com/en-us/library/system.exception.data(v=vs.110).aspx
Solution
My first thought was to write a custom class that I could use to wrap something before adding it to the Data collection, but because the value would really only be used for logging, the class wouldn't need to do much except have a ToString. So a custom class seemed like overkill. Then I thought about System.Tuple -- is it Serializable? Yes, it is! Try this:
System.Exception whoops = new System.Exception ( "Whoops" ) ;
whoops.Data [ "Panel" ] = new System.Tuple<System.Object> ( new System.Windows.Forms.Panel() ) ;
And what does the System.Tuple.ToString do? It simply makes a string of its contents -- which is exactly what I need for logging.
Using the code
My solution uses a System.Tuple<System.Type,System.Object> so I get the type of the value as well as the value, because I like to log the type, particularly for ADO.net Command Parameters.
I implemented my solution as an Extension Method on IDictionary:
public static System.Tuple<System.Type,System.Object>
AddWithType
(
this System.Collections.IDictionary Dictionary
,
System.Object Key
,
System.Object Value
)
{
System.Tuple<System.Type,System.Object> result ;
if ( Dictionary == null )
{
throw ( new System.ArgumentNullException ( "Dictionary" , "You must provide a Dictionary" ) ) ;
}
if ( Key == null )
{
throw ( new System.ArgumentNullException ( "Key" , "You must provide a Key" ) ) ;
}
if ( Value == null )
{
result = new System.Tuple<System.Type,System.Object> ( null , "<null>" ) ;
}
else
{
result = new System.Tuple<System.Type,System.Object> ( Value.GetType() , Value ) ;
}
Dictionary [ Key ] = result ;
return ( result ) ;
}
The method can then be used like this:
catch ( System.Exception err )
{
err.Data.AddWithType ( "CommandText" , cmd.CommandText ) ;
throw ;
}
What you do with the Tuple once you catch the Exception higher up the application is beyond the scope of this Tip.
In closing
If you write code that indiscriminately sticks values in the Data collections of Exceptions, you may need to ensure that you are protected against non-serializable objects. Simply wrapping the object in a Tuple is probably enough in most cases.
In case you're wondering, the object that caused the problem was a custom Exception from a third-party API I'm using. In some specific cases, involving the retrying of operations after catching an Exception, I store the Exception that caused the retry in the Data collection of another Exception (no, this is not a case where it should be the InnerException).