Skip to content

Using the AspectObjectDumper

Val Melamed edited this page May 4, 2017 · 1 revision

Usage

The dumper is implemented by the class ObjectTextDumper in the namespace vm.Aspects.Diagnostics.

Here is the first usage example that we are going to improve on further down:

using System;
using System.IO;
using vm.Aspects.Diagnostics;

namespace ObjectDumperSamples
{
    class MyClass
    {
        public bool BoolProperty { get; set; }
        public int IntProperty { get; set; }
        public Guid GuidProperty { get; set; }
        public Uri UriProperty { get; set; }
    }

    class Program
    {
        static void Main()
        {
            int anInt = 5;
            var anObject = new MyClass
            {
                BoolProperty = true,
                IntProperty  = 3,
                GuidProperty = Guid.NewGuid(),
            };

            using (var writer = new StringWriter())
            {
                var dumper = new ObjectTextDumper(writer);

                // dump a primitive value:
                dumper.Dump(anInt);
                // dump complex value:
                dumper.Dump(anObject);
                Console.WriteLine(writer.GetStringBuilder().ToString());
            }

            Console.ReadKey(true);
        }
    }
}

This is the output from this program:

5
MyClass (ObjectDumperSamples.MyClass, ObjectDumperSamples, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null):
  BoolProperty             = True
  GuidProperty             = 6e27359b-c1b1-48c5-bf69-967b7fda886c
  IntProperty              = 3
  UriProperty              = <null>

I prefer the following two simple facades implemented as extension methods to System.Object. Just replace the using statement above with this code snippet:

using (var writer = new StringWriter())
{
    anInt.DumpText(writer);
    anObject.DumpText(writer);
    Console.WriteLine(writer.GetStringBuilder().ToString());
}

Or replace the entire using statement with:

Console.WriteLine(anInt.DumpString());
Console.WriteLine(anObject.DumpString());

If you don't have better ideas for overriding MyClass.ToString(), why not implement it like this:

public override string ToString()
{
    return this.DumpString();
}

And now we can replace the last line in the snippet with:

Console.WriteLine(anObject);

The programmer has control over the dump through attributes associated with the classes, properties and fields of the dumped objects. The attributes can be applied:

  1. Directly on the class and its properties and fields.

  2. Indirectly in a so called buddy class - class referred to in a MetadataTypeAttribute on your class.

  3. By using the parameters of the Dump method associate a type of objects with after-the-fact written buddy-class.

  4. By using the method of the static class ClassMetadataResolver.SetClassDumpData. The signature of the method is self-explanatory:

    public static void SetClassDumpData( Type type, Type metadata = null, DumpAttribute dumpAttribute = null);

If the second and third parameters are null-s their respective values are extracted from the attributes applied to the target type and if not found there - default values are assumed. The ClassMetadataResolver contains a static cache associating types with dumping metadata. This facility is most useful for BCL or third party classes.

Let's go back to the constructor of the ObjectTextDumper class:

public ObjectTextDumper(
    TextWriter writer,
    int indentLevel = 0,
    int indentLength = 2,
    int maxDumpLength = 4*1024*1024,
    BindingFlags propertiesBindingFlags = BindingFlags.Public|BindingFlags.NonPublic|BindingFlags.Instance|BindingFlags.DeclaredOnly,
    BindingFlags fieldsBindingFlags = BindingFlags.Public|BindingFlags.Instance|BindingFlags.DeclaredOnly);

It takes an object of type descending from TextWriter; the initial value for the indent level; the size of the indent - the number of spaces in a single indent, the fourth parameter sets a limit on the dumped text. This would be useful when dumping objects with enormous graphs - you may run out of memory. The default value is 4 million characters (~ 8MB). The last two parameters control which properties and fields should be dumped with respect to their visibility and lifetime modifiers. By default all public and non-public instance properties and all public instance fields are being dumped. From that point on properties and fields are treated equally and for the sake of brevity in this document I will use only the word "property" and unless explicitly stated otherwise, please assume the same applies to fields.

Let's look at the Dump method too:

public object Dump(
    object value,
    Type dumpMetadata = null,
    DumpAttribute dumpAttribute = null);

The method returns the dumper object itself, just in case you want to chain the Dump calls. As you can see here, it takes actually three parameters, where the last two default to null-s:

  1. The first parameter is the object that we want to dump.
  2. The second parameter is supposed to be a Type object representing the dumping metadata for the type of the dumped object.
  3. The third parameter applies a DumpAttribute to the type of the dumped object.

The last two parameters override the dumped object's buddy class and DumpAttribute. The heart of the customization features of the dumper is the attribute class DumpAttribute. For a more detailed description take a look at the documentation comments in the source code or the class documentation generated off of them. The DumAttribute attribute can be applied to class-es, struct-s, properties and fields. When applied to class-es or struct-s, only a few of the parameters are applicable and they affect the dump of all properties and fields from the type, unless overridden by DumAttribute applied to a specific property or field. It is much more common though to apply the attribute to properties and public fields. Let's look at specific scenarios that will demonstrate the customization of the dump behavior.

1. Suppress dumping of a property:

[Dump(false)] public string Unimportant { get; set; } 

Alternatively:

[Dump(Skip=ShouldDump.Skip)]
public string Unimportant { get; set; }

2. Control the order of dumping:

class MyClass
{    
    [Dump(2)]                               // or [Dump(Order=2)]
    public bool BoolProperty { get; set; }
    [Dump(1)]
    public int IntProperty { get; set; }
    [Dump(-1)]
    public Guid GuidProperty { get; set; }
    [Dump(0)]
    public Uri UriProperty { get; set; }
}

This change to the type of the dumped object produces the following output:

MyClass (ObjectDumperSamples.MyClass, ObjectDumperSamples, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null):    
  UriProperty              = <null>
  IntProperty              = 3
  BoolProperty             = True
  GuidProperty             = d8de41d8-14f3-4cf0-a1b6-fb18396be0e6

These are the recursive ordering rules:

  1. First are dumped the properties of the base class (if any) with positive or non-specified order. Properties with the same order number are dumped in alphabetical order.
  2. Next are dumped the current class properties with positive or unspecified order.
  3. Follow the properties with positive or unspecified order of the derived class (if any.)
  4. Then are dumped the properties with negative order in the opposite order: the derived, the current, the base class'.
  5. Finally are dumped all properties from all classes with dump order int.MinValue. This pushes some properties really to the end of the dump, for example the stack in an Exception object.

So if we inherit from MyClass in MyClassDescendant:

class MyClassDescendant : MyClass
{
    [Dump(0)]
    public string StringProperty { get; set; }
}

a dump of an object of type MyClassDescendant will look like this:

MyClassDescendant (ObjectDumperSamples.MyClassDescendant, ObjectDumperSamples, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null):
  UriProperty = <null>
  IntProperty              = 3
  BoolProperty             = True
  StringProperty           = StringProperty
  GuidProperty             = fe0e72b2-196f-424a-a011-7f8119c04ead

3. Dump some of the properties only if they are not null:

[Dump(0, DumpNullValues=ShouldDump.Skip)]
public Uri UriProperty { get; set; }

With this modification the last dump becomes:

MyClassDescendant (ObjectDumperSamples.MyClassDescendant, ObjectDumperSamples, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null):
  IntProperty              = 3
  BoolProperty             = True
  StringProperty           = StringProperty
  GuidProperty             = e6559163-6b53-4c9a-aaf5-8bf620d9155a

Note that the property DumpNullValues can be applied also on a class level. Then any property with value null will be skipped in the dump output.

4. Mask the values of some (e.g. PII) properties:

[Dump(Mask=true)]
public string SSN { get; set }

If we add this property with its attribute to MyClass the dump will look something like this:

MyClass (ObjectDumperSamples.MyClass, ObjectDumperSamples, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null):
  IntProperty              = 3
  BoolProperty             = True
  GuidProperty             = 1b862d91-ccb1-4b92-977d-d4f723c4d39d
  SSN                      = ******

If SSN is null the output would be:

MyClass (ObjectDumperSamples.MyClass, ObjectDumperSamples, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null):
  IntProperty              = 3
  BoolProperty             = True
  GuidProperty             = 64d2112c-b8d9-4a67-922e-87d02e5da3ca
  SSN                      = <null>

If you want different string for mask, use the DumpAttribute's property MaskValue:

[Dump(Mask=true, MaskValue="------")]
public string SSN { get; set }

MyClass (ObjectDumperSamples.MyClass, ObjectDumperSamples, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null):
  IntProperty              = 3
  BoolProperty             = True
  GuidProperty             = 4e7fe09e-81b7-4527-b4b6-32e42ade950c
  SSN                      = ------

Clearly, these features were introduced with logging in mind.

5. Control the length of the output with the property MaxLength.

When applied to strings it specifies to dump only the first MaxLength characters. By default the dumper will dump the entire string. Let's add this property:

[Dump(MaxLength=25)]
public string Description { get; set; }

and assign a long value to it:

var anObject = new MyClass
{
    ...
    Description = "This is one very very very very very long description",
};

The output would be:

MyClass (ObjectDumperSamples.MyClass, ObjectDumperSamples, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null):
  IntProperty              = 3
  BoolProperty             = True
  Description              = This is one very very ver...
  GuidProperty             = 07d23136-1ae5-40d8-bebc-8ff2a4fefb44

The property can be applied to sequences of objects too - arrays, lists, etc. By default the dumper outputs no more than the first 10 elements of the sequence. If you want to dump them all (at your own risk) specify a negative value, e.g. -1.

6. Customize the format of the property name by using the property LabelFormat.

By default the property label is dumped using the format string "{0,-24} = ", where the property’s name is passed as parameter 0. Let's reuse the previous example:

[Dump(MaxLength=25, LabelFormat="{0,-12} (Truncated)")]
public string Description { get; set; }

The property will be dumped like this:

Description (Truncated)  = This is one very very ver...

7. Customize the dumped value with ValueFormat.

The default value of this property is "{0}". I.e. the default format of the value is used. Let's add another property with custom format for the value, e.g.

[Dump(ValueFormat="{0:o}")]
public DateTime CreatedAt { get; set; }

The dump is:

MyClass (ObjectDumperSamples.MyClass, ObjectDumperSamples, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null):
  IntProperty              = 3
  BoolProperty             = True
  CreatedAt                = 2013-08-25T21:25:54.7103441-04:00
  GuidProperty             = 04dee1cf-1027-444b-87c8-639504461abf

This property recognizes a special format string ToString(), which inserts the result of the property's method ToString() into the output stream.

8. Customize the dumped value with DumpClass and DumpMethod.

These two properties allow you to plug in you own custom dumping for certain properties or fields of your object. They define where to find your method which will be invoked in order to produce the text representation of the property to which it is applied. The attribute's property DumpClass is required if the method is implemented in a class different from the property's class. In this case the method must be public, static, must return string, and must take a single argument of the type of the property or base class of the property's type. The DumpMethod property specifies the name of the method. If it is omitted the object dumper will assume default dump method name Dump.

If the DumpClass property is not specified but the DumpMethod is, the object dumper will assume that the class containing the method is the class of the property and will try to find either an instance method with the specified name that returns string and takes no parameters or a static method which returns String and has a single parameter of the type of the property. In other words these two attributes are equivalent:

[Dump(DumpMethod="ToString")]
[Dump(ValueFormat="ToString()")]

9. RecurceDump

This property controls whether to go into the complex property or just dump the type of it. When applied to a class the property affects dumping of all complex and sequence properties of the class. The type of this parameter is enum ShouldDump which has three values:

  1. Default - use the default dump behavior.
  2. Dump - indent and dump recursively the properties of the associated object.
  3. Skip - do not dump the associated object, just output its type or its "default property".

10. DefaultProperty

If the dump of a property of complex type is suppressed with RecurceDump=ShouldDump.Skip besides of its type you can also dump one of its properties which you may consider characteristic or identifying for this type. For the purpose use the DefaultProperty property with string value the name of the property, e.g.

[Dump(RecurceDump=ShouldDump.Skip, DefaultProperty="Key")]
public ComplexType Associate { get; }

11. If the chain of associated objects is too deep, you can cut it short with the property MaxDepth on a class level:

[Dump(MaxDepth=3)]
class MyClassDescendant : MyClass
{
    ...

Here is the dump that I got for this scenario:

MyClassDescendant (ObjectDumperSamples.MyClassDescendant, ObjectDumperSamples, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null):
  IntProperty              = 3
  BoolProperty             = True
  CreatedAt                = 2013-09-10T20:43:48.9853660-04:00
  StringProperty           = StringProperty
  Description              = This is one very very ver...
  Associate                = ComplexType (ObjectDumperSamples.ComplexType, ObjectDumperSamples, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null):
    Key                      = IL.TX.2013-09-10:20:23:34.85930
    UniqueId                 = d481f7fe-2094-4e0c-b753-f99188db18eb
    Other                    = ComplexType (ObjectDumperSamples.ComplexType, ObjectDumperSamples, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null):
      Key                      = IL.TX.2013-09-10:20:23:34.85931
      UniqueId                 = c914a5a2-201c-4f48-8159-0e75a2809031
      Other                    = ComplexType (ObjectDumperSamples.ComplexType, ObjectDumperSamples, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null):
        Key                      = IL.TX.2013-09-10:20:23:34.85932
        UniqueId                 = 9c4d4937-a1c0-4b2f-b719-daadd12c29c5
        Other                    = ...object dump reached the maximum depth level. Use the DumpAttribute.MaxDepth to increase the depth level if needed.
  GuidProperty             = bdbda70b-95c8-4a62-a8df-dcdf4f8e2912

12. Enumerating sequences (classes implementing IEnumerable)

Generally the dump recurces only into arrays and sequences from the FCL. The reason is that recursing into custom sequences may have unintended side effects. However the author of a custom sequence object may override this by using the Enumerate=ShouldDump.Dump property on a class level applied DumpAttribute:

[Dump(Enumerate=ShouldDump.Dump)]
class MyCollection : IEnumerable<Item>
{
 . . .
}

Note that all public types are well documented with documentation comments that show up in intellisense.