Simple Variable Dump






4.89/5 (12 votes)
This class dumps any .NET object properties to your output window.
Introduction
This is a quick variable dump.
It shows types and values of all properties and works its way recursively avoiding variables that have already been dumped.
Background
This is based on this blog.
Using the Code
A simple line to call the static
method...
Utils.Dump(requestObject);
...will display the output (as an example):
1 PublicProperties (Middleware.Contracts.ReturnPeriodicMeterDataRequestRequestMessage)
2 | ExtensionData (System.Runtime.Serialization.ExtensionDataObject)
3 | Header (Middleware.Contracts.HeaderType)
4 | | ExtensionData (System.Runtime.Serialization.ExtensionDataObject)
5 | | MessageId = "0847c4b1-7f1c-4cf2-a011-09626d181234" (System.String)
6 | | CorrelationId = "1" (System.String)
7 | | Segment = 1739 (System.Int32)
8 | | SegmentCount = 1753 (System.Int32)
9 | | LastSegment = null (System.Nullable`1[System.Boolean])
10 | MeterData (Middleware.Contracts.MeterDataType)
11 | | ExtensionData (System.Runtime.Serialization.ExtensionDataObject)
12 | | MeterSerialNumber = "00000000123450083" (System.String)
13 | | RegisterList (System.Collections.Generic.List`1[Middleware.Contracts.RegisterType])
14 | | | Capacity = 4 (System.Int32)
15 | | | Count = 1 (System.Int32)
16 | | RegisterList[0] (Middleware.Contracts.RegisterType)
17 | | | ExtensionData (System.Runtime.Serialization.ExtensionDataObject)
18 | | | RegisterId = "2" (System.String)
19 | | | Unit = "Wh" (System.String)
20 | | | MeterReadingList (System.Collections.Generic.List`1[Middleware.Contracts.MeterReadingType])
21 | | | | Capacity = 32 (System.Int32)
22 | | | | Count = 20 (System.Int32)
23 | | | MeterReadingList[0] (Middleware.Contracts.MeterReadingType)
24 | | | | ExtensionData (System.Runtime.Serialization.ExtensionDataObject)
25 | | | | Reading = 17540 (System.Double)
26 | | | | ReadingDate = 21/12/2011 04:00:00 (System.DateTime)
27 | | | | Status = "2000" (System.String)
28 | | | MeterReadingList[1] (Middleware.Contracts.MeterReadingType)
29 | | | | ExtensionData (System.Runtime.Serialization.ExtensionDataObject)
30 | | | | Reading = 17540 (System.Double)
31 | | | | ReadingDate = 21/12/2011 05:00:00 (System.DateTime)
32 | | | | Status = "2000" (System.String)
...
118 | | | MeterReadingList[19] (Middleware.Contracts.MeterReadingType)
119 | | | | ExtensionData (System.Runtime.Serialization.ExtensionDataObject)
120 | | | | Reading = 17540 (System.Double)
121 | | | | ReadingDate = 21/12/2011 23:00:00 (System.DateTime)
122 | | | | Status = "2000" (System.String)
You can also dump private
members if you prefer:
Utils.Dump(yourObject, Utils.DumpStyle.PrivateFields);
In this mode, we dump all fields rather than public
properties.
Complete Code
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Text;
class Utils
{
public enum DumpStyle
{
PublicProperties,
PrivateFields
}
public static void Dump(object obj)
{
Dump(obj, DumpStyle.PublicProperties, null);
}
public static void Dump(object obj, DumpStyle dumpStyle)
{
var varDumper1 = new VarDumper();
varDumper1.Dump(obj, dumpStyle, null);
}
public static void Dump(object obj, DumpStyle dumpStyle, Action<string> action)
{
if (action == null) action = (s) => Trace.WriteLine(s);
var varDumper1 = new VarDumper();
varDumper1.Dump(obj, dumpStyle, action);
}
public static IEnumerable<string> HexDump(byte[] bytes, int bytesPerLine)
{
if (bytes == null)
{
yield return "<null>";
yield break;
}
int bytesLength = bytes.Length;
char[] HexChars = "0123456789ABCDEF".ToCharArray();
int firstHexColumn =
8 // 8 characters for the address
+ 3; // 3 spaces
int firstCharColumn = firstHexColumn
+ bytesPerLine * 3 // - 2 digit for the hexadecimal value and 1 space
+ (bytesPerLine - 1) / 8 // - 1 extra space every 8 characters from the 9th
+ 2; // 2 spaces
int lineLength = firstCharColumn
+ bytesPerLine; // - characters to show the ASCII value
char[] line = (new String(' ', lineLength)).ToCharArray();
int expectedLines = (bytesLength + bytesPerLine - 1) / bytesPerLine;
for (int i = 0; i < bytesLength; i += bytesPerLine)
{
line[0] = HexChars[(i >> 28) & 0xF];
line[1] = HexChars[(i >> 24) & 0xF];
line[2] = HexChars[(i >> 20) & 0xF];
line[3] = HexChars[(i >> 16) & 0xF];
line[4] = HexChars[(i >> 12) & 0xF];
line[5] = HexChars[(i >> 8) & 0xF];
line[6] = HexChars[(i >> 4) & 0xF];
line[7] = HexChars[(i >> 0) & 0xF];
int hexColumn = firstHexColumn;
int charColumn = firstCharColumn;
for (int j = 0; j < bytesPerLine; j++)
{
if (j > 0 && (j & 7) == 0) hexColumn++;
if (i + j >= bytesLength)
{
line[hexColumn] = ' ';
line[hexColumn + 1] = ' ';
line[charColumn] = ' ';
}
else
{
byte b = bytes[i + j];
line[hexColumn] = HexChars[(b >> 4) & 0xF];
line[hexColumn + 1] = HexChars[b & 0xF];
line[charColumn] = (b < 32 || b >= 128 ? '.' : (char)b);
}
hexColumn += 3;
charColumn++;
}
yield return new String(line);
}
}
private class VarDumper
{
int maxRecursion = 10;
private DumpStyle mDumpStyle;
private int mLineCounter;
private Dictionary<Object, int>
seenObjects = new Dictionary<Object, int>();
private static HashSet<Type> SimpleTypes = new HashSet<Type>() {
typeof(UInt16 ), typeof(UInt32), typeof(UInt64),
typeof(Int16 ), typeof(Int32), typeof(Int64),
typeof(Byte), typeof(DateTime),
typeof(Single), typeof(Double), typeof(Decimal),
typeof(Boolean),
typeof(Char), typeof(String),
typeof(string)};
internal void Dump<T>(T obj, DumpStyle dumpStyle, Action<string> writeLine)
{
mDumpStyle = dumpStyle;
DumpObjectMember(0, "", dumpStyle.ToString(), typeof(T), obj, writeLine);
}
private string IndentedLine(string format, params object[] args)
{
mLineCounter++;
string line;
if (args != null && args.Length > 0) line = string.Format(format, args);
else line = format;
line = mLineCounter.ToString("000") + " " + line;
return line;
}
private bool DumpObjectMembers(int recursion, string indent, object obj, Action<string> writeLine)
{
if (obj == null) return false;
// Protect the method against endless recursion
if (seenObjects.ContainsKey(obj))
{
writeLine(IndentedLine("{0}<already dumped on line {1}>", indent, seenObjects[obj]));
return false;
}
else
{
// Determine object type
Type t = obj.GetType();
if (SimpleTypes.Contains(t) || obj is Type)
{
return false;
}
if (obj is Byte[])
{
Byte[] arr = obj as Byte[];
int lineNo = 0;
foreach (var line in Utils.HexDump(arr, 16))
{
if (lineNo > 100)
{
writeLine(IndentedLine("{0}...({1} bytes)", indent, arr.Length));
break;
}
writeLine(IndentedLine("{0}{1}", indent, line));
lineNo++;
}
return true;
}
if (recursion >= maxRecursion)
{
writeLine(IndentedLine("{0}<maximum recursion of {1} reached>", indent, maxRecursion));
}
seenObjects.Add(obj, mLineCounter);
bool classDisplayed = true;
int propertiesFound = 0;
while (t != null)
{
switch (mDumpStyle)
{
case DumpStyle.PublicProperties:
// Get array with properties for this object
PropertyInfo[] properties = t.GetProperties
(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly);
foreach (PropertyInfo property in properties)
{
string l = CheckClassIsDisplayed(ref classDisplayed, indent, t);
if (l != null) writeLine(l);
Exception ex = null;
object value = null;
try
{
if (property.GetIndexParameters().Length > 0) continue;
// Get the property value
propertiesFound++;
value = property.GetValue(obj, null);
}
catch (Exception ex0)
{
// You can't yield in a catch block
ex = ex0;
}
if (ex == null)
{
// Create indenting string to put
// in front of properties of a deeper level
// We'll need this when we display the property name and value
DumpObjectMember
(recursion, indent, property.Name, property.PropertyType, value, writeLine);
}
if (ex != null)
writeLine(IndentedLine(@"{0}{1} = {2} ({3})", indent, property.Name, ex.Message, ex.GetType()));
}
break;
case DumpStyle.PrivateFields:
// Get array with properties for this object
FieldInfo[] fields = t.GetFields(BindingFlags.Instance |
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly);
foreach (FieldInfo field in fields)
{
string l = CheckClassIsDisplayed(ref classDisplayed, indent, t);
if (l != null) writeLine(l);
// Get the property value
object value = field.GetValue(obj);
// Create indenting string to put
// in front of properties of a deeper level
// We'll need this when we display the property name and value
propertiesFound++;
DumpObjectMember
(recursion, indent, field.Name, field.FieldType, value, writeLine);
}
break;
}
t = t.BaseType;
classDisplayed = false;
}
if (propertiesFound == 0)
{
seenObjects.Remove(obj);
}
}
return true;
}
private string CheckClassIsDisplayed(ref bool classDisplayed, string indent, Type t)
{
if (classDisplayed == false)
{
classDisplayed = true;
return IndentedLine("{0}(inherited from {1})", indent, t);
}
else return null;
}
private void DumpObjectMember(int recursion,
string indent, string propertyName, Type propertyType, object value,
Action<string> writeLine)
{
if (value == null)
{
writeLine(IndentedLine("{0}{1} = null ({2})",
indent, propertyName, FriendlyTypeName(propertyType)));
}
else
{
string displayValue = value.ToString();
// If the value is a string, add quotation marks
if (value is string) displayValue = String.Concat("\"",
displayValue.Replace("\"", "\"\""), '"');
string valueType = value.GetType().ToString();
if (displayValue.Contains("\r\n"))
{
writeLine(IndentedLine("{0}{1} = ({2})",
indent, propertyName, FriendlyTypeName(value.GetType())));
int lineNo = 0;
string[] lines = displayValue.Split('\r');
foreach (var line in lines)
{
if (lineNo > 100)
{
writeLine(IndentedLine("{0} ... ({1} lines)", indent, lines.Length));
break;
}
displayValue = line.TrimStart('\n');
if (lineNo >= (lines.Length - 1) && displayValue.Length == 0) break;
if (displayValue.Length >= 200) displayValue =
string.Format("{0}... (length: {1})",
displayValue.Substring(0, 200), displayValue.Length);
writeLine(IndentedLine("{0} | : {1}", indent, displayValue));
lineNo++;
}
}
else
{
if (displayValue.Length >= 200) displayValue =
string.Format("{0}... (length: {1})",
displayValue.Substring(0, 200), displayValue.Length);
if (displayValue == valueType)
{
// The display value is identical to the type (no need to add it)
writeLine(IndentedLine("{0}{1} ({2})",
indent, propertyName, FriendlyTypeName(value.GetType())));
}
else
{
// Add property name and value to return string
writeLine(IndentedLine("{0}{1} = {2} ({3})",
indent, propertyName, displayValue, FriendlyTypeName(value.GetType())));
}
}
if (DumpObjectMembers(recursion + 1, indent + " | ", value, writeLine)
&& value is IEnumerable && !(value is string) && !(value is Byte[]))
{
// 2009-07-29: added support for collections
// The value is a collection (eg. it's an arraylist or generic list)
// so loop through its elements and dump their properties
int elementCount = 0;
Type elementType = typeof(object);
Exception ex = null;
IEnumerator enumerator = (value as IEnumerable).GetEnumerator();
bool next = false;
object element = null;
while (true)
{
// You can't yield in a try catch this is why
// I am breaking the loop with an explicit enumerator
try
{
next = enumerator.MoveNext();
if (next) element = enumerator.Current;
}
catch (Exception ex0)
{
// You can't yield in a catch block
ex = ex0;
}
if (!next || ex != null) break;
// Display the collection element name and type
DumpObjectMember(recursion + 1,
indent,
string.Format("{0}[{1}]", propertyName, elementCount),
elementType, element, writeLine);
elementCount++;
}
if (ex != null) writeLine(IndentedLine("{0} * An error occured while enumerating {1} : {2}",
indent, propertyName, ex.Message));
}
}
}
private object FriendlyTypeName(Type type)
{
if (!type.IsGenericType)
{
if (type.IsPrimitive || type.Namespace == "System")
{
return type.Name;
}
else
{
return type.FullName.Replace('+', ' ');
}
}
var name = type.Name;
var index = name.IndexOf("`");
name = name.Substring(0, index);
var args = type.GetGenericArguments();
if (type.GetGenericTypeDefinition() ==
typeof(Nullable<>) && args[0].IsPrimitive)
{
return args[0].Name + '?';
}
else
{
var builder = new System.Text.StringBuilder();
builder.Append(type.Namespace);
builder.Append('.');
builder.Append(name);
builder.Append('<');
var first = true;
foreach (var arg in args)
{
if (first) first = false;
else builder.Append(',');
builder.Append(FriendlyTypeName(arg));
}
builder.Append('>');
return builder.ToString();
}
}
}
public static object DumpToString(object obj, DumpStyle dumpStyle)
{
var varDumper1 = new VarDumper();
StringBuilder result = new StringBuilder();
varDumper1.Dump(obj, dumpStyle, (s) => result.Append(s));
return result.ToString();
}
}
Points of Interest
Nothing fancy. It dumps the public
properties of the object. The main difference with Ruud version is that I keep a HashSet
of dumped objects. This way you can dump trees that point to themselves or parents and it won't show much duplication.
I added the line counter to point to objects that have already been dumped, in practice it is also useful to locate when objects start and stop.
History
- 2013-06-26 (PG) Improve array rendering (now shows at higher level) and do not rely so much on yield
- 2012-06-01 (PG) Dump byte arrays, display generics types with C# syntax.
- 2012-05-22 (PG) Display exceptions and inherited members a bit better. Also line numbers for cycling references.
- 2012-05-18 (PG) Moved the
StringBuilder
and most of the code to a class - 2009-07-28 First version from Ruud.Tech (no license attached)