Posts Tagged 'reflection'

C# helper to dump any object to a log

Some time back I had the need for code to make a reasonable go at dumping out any object into a debug log. Visual Studio does a good job of inspecting objects, so I figured there was probably a clever trick somewhere to achieve this easily, but I couldn’t find one. In the end, I wrote my own, and learned a bit more about reflection in the process.

My first attempt was useful but a bit fragile. Often it would fail horribly on a new class and I’d have to decorate the class with my custom attributes to make it work.

Since then I’ve refined it a fair amount, and it seems pretty stable although I’m sure it’s neither complete nor perfect. You get better output if you use the custom attributes here and there, but they shouldn’t be required. I use it in lots of my code and works well for me, so I share it here in case it’s more widely useful.


using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
using Common.Logging;

namespace DebugTools
{        
    public interface IDebugDumpMask
    {
        bool GetMask(string MemberName);
    }

    [AttributeUsage(AttributeTargets.Class)]
    public class DumpClassAttribute : System.Attribute
    {

        public DumpClassAttribute()
        {
            IsEnumerable = false;
            ForceUseToString = false;
        }

        /// <summary>
        /// Forces the class to be treated as an enumerable type. Default false.
        /// </summary>
        public bool IsEnumerable { get; set; }
        /// <summary>
        /// Forces a simple string conversion of the object. Default false.
        /// </summary>
        public bool ForceUseToString { get; set; }
    
    }

    // Note: Visibility takes priority over masking
    // If an member is not visible, it will never be included.
    // A visible member can be masked out.

    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
    public class DumpMemberAttribute : System.Attribute
    {
 
        public DumpMemberAttribute()
        {
            IsVisible = true;
            MemberName = null;
            IsEnumerable = false;
            ForceUseToString = false;
            EscapeString = false;
        }

        /// <summary>
        /// Controls whether the member is included in the output or not. Default true for public members, false for private ones.
        /// </summary>
        public bool IsVisible { get; set; }
        /// <summary>
        /// Overrides the automatically derived memeber name
        /// </summary>
        public string MemberName { get; set; }
        /// <summary>
        /// Forced the member to be treated as an enumerable type. Default false.
        /// </summary>
        public bool IsEnumerable { get; set; }
        /// <summary>
        /// Forces a simple string conversion of the member. Default false.
        /// </summary>
        public bool ForceUseToString { get; set; }
        /// <summary>
        /// If true, the string is escaped before outputting.
        /// </summary>
        public bool EscapeString { get; set; }
    }

    public class DebugDumper
    {
        private static int RecursionCount = 0;

        private static DumpClassAttribute GetDebugDumpClassAttribute(Type cls)
        {
            object[] attributes = cls.GetCustomAttributes(typeof(DumpClassAttribute), true);
            foreach (object attr in attributes)
            {
                if (attr is DumpClassAttribute)
                    return (DumpClassAttribute)attr;
            }
            return null;

        }

        private static DumpMemberAttribute GetDebugDumpAttribute(MemberInfo member)
        {
            object[] attributes = member.GetCustomAttributes(typeof(DumpMemberAttribute), true);
            foreach (object attr in attributes)
            {
                if (attr is DumpMemberAttribute)
                    return (DumpMemberAttribute)attr;
            }
            return null;

        }

        private static Dictionary<string, string> Enumerate(object o, string baseName)
        {
            //logger.Trace("Enumerate {0}",baseName);

            RecursionCount++;

            if (RecursionCount > 5)
                Debugger.Break();

            var members = new Dictionary<string, string>();

            var ddc = GetDebugDumpClassAttribute(o.GetType());

            bool ForceEnum = CheckForcedEnumerable(o.GetType());

            if (ForceEnum || (ddc != null && ddc.IsEnumerable))
            {
                ProcessEnumerable(members, o, baseName);
            }
            else if (ddc != null && ddc.ForceUseToString)
            {
                members.Add(baseName, o.ToString());
            }
            else
            {

                BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public;

                FieldInfo[] fi = o.GetType().GetFields(flags);
                PropertyInfo[] pi = o.GetType().GetProperties(flags);

                ProcessFields(o, baseName, members, fi);
                ProcessProperties(o, baseName, members, pi);
            }

            RecursionCount--;

            return members;
        }

        private static bool CheckForcedEnumerable(Type type)
        {
            if (type.FullName.StartsWith("System.Collections.Generic.List"))
                return true;
            if (type.FullName.StartsWith("System.Collections.Generic.Dictionary"))
                return true;
            if (type.IsArray)
                return true;
            return false;
        }

        private static void ProcessProperties(object o, string baseName, Dictionary<string, string> members, PropertyInfo[] pi)
        {
            DumpMemberAttribute dd;
            IDebugDumpMask masker = o as IDebugDumpMask;
            bool mask;

            foreach (PropertyInfo prop in pi)
            {
                // Default is to show properties always
                dd = GetDebugDumpAttribute(prop) ?? new DumpMemberAttribute() { MemberName = prop.Name, IsVisible = true };
                mask = masker == null ? true : masker.GetMask(prop.Name);

                if (dd.IsVisible && mask)
                {
                    int IndexerCount = prop.GetIndexParameters().Count();
                    if (IndexerCount == 0 || (dd != null && dd.IsEnumerable))
                        try
                        {
                            ProcessMember(members, dd, prop.Name, prop.GetValue(o, null), baseName);
                        }
                        catch (TargetInvocationException)
                        {
                        }

                    else
                        Debug.Assert(false, "Can't dump an indexed property!");

                }
            }
        }

        private static void ProcessFields(object o, string baseName, Dictionary<string, string> members, FieldInfo[] fi)
        {
            DumpMemberAttribute dd;
            IDebugDumpMask masker = o as IDebugDumpMask;
            bool mask;

            foreach (FieldInfo field in fi)
            {
                // The attribute might be null, so we need to get some defaults if it is
                dd = GetDebugDumpAttribute(field) ?? new DumpMemberAttribute() { MemberName = field.Name, IsVisible = field.IsPublic };
                mask = masker == null ? true : masker.GetMask(field.Name);

                if (dd.IsVisible && mask)
                {
                    try
                    {
                        ProcessMember(members, dd, field.Name, field.GetValue(o), baseName);
                    }
                    catch (TargetInvocationException)
                    {
                    }

                }
            }
        }

        private static void ConcatSubMembers(Dictionary<string, string> members, Dictionary<string, string> subMembers)
        {
            foreach (KeyValuePair<string, string> item in subMembers)
            {
                members.Add(item.Key, item.Value);
            }
        }

        private static void ProcessMember(Dictionary<string, string> members, DumpMemberAttribute attribute, string memberName, object value, string baseName)
        {
            //logger.Trace("ProcessMember {0} : {1}", baseName, memberName);

            string name = string.Format("{0} : {1}", baseName, attribute.MemberName ?? memberName);

            if (value == null)
            {
                members.Add(name, "<null>");
            }
            else
            {
                Type type = value.GetType();
                if (type.IsArray || attribute.IsEnumerable)
                {
                    ProcessEnumerable(members, value, name);
                }
                else if (type.IsValueType || type == typeof(System.String) || attribute.ForceUseToString)
                {
                    members.Add(name, attribute.EscapeString ? EscapeString(value.ToString()) : value.ToString());
                }
                else if (type.IsClass)
                {
                    var subMembers = Enumerate(value, name);
                    ConcatSubMembers(members, subMembers);
                }
                else if (type.IsInterface)
                {
                    members.Add(name, type.ToString());
                }
            }
        }

        private static void ProcessEnumerable(Dictionary<string, string> members, object value, string name)
        {
            IEnumerable e = value as IEnumerable;
            value.GetType();
            int count = 0;

            foreach (object o in e.Cast<object>())
            {
                var member = string.Format("[{0}]", count);
                var dd = new DumpMemberAttribute() { IsVisible = true, MemberName = member };
                ProcessMember(members, dd, member, o, name);
                count++;
            }

        }

        public static List<string> Dump(object o, string baseName)
        {
            RecursionCount = 0;

            Dictionary<string, string> members = Enumerate(o, baseName);

            int maxLength = members.Keys.Select(x => x.Length).Max();

            return members.Select(x => (string.Format("{0} = {1}", x.Key.PadRight(maxLength + 1), x.Value))).ToList();

        }

        /// <summary>
        /// Take a string and create a version with all wierd characters escaped
        /// </summary>
        /// <param name="input">The string to escape</param>
        /// <returns>The escaped string</returns>
        public static string EscapeString(string input)
        {
            StringBuilder result = new StringBuilder();
            string wellknown = "\r\n\t\0\\";
            string wellknownmap = @"rnt0\";
            int ix;

            foreach (char c in input)
            {

                ix = wellknown.IndexOf(c);
                if (ix >= 0)
                {
                    result.Append(@"\" + wellknownmap[ix]);
                }
                else if (char.IsControl(c))
                {
                    result.AppendFormat(@"\x{0:x4}", (int)c);
                }
                else
                {
                    result.Append(c);
                }
            }

            return result.ToString();
        }
    }
}

To use it, just call Dump(), passing the object to dump and a base name to use in the output.
Apply the DumpMemberAttribute to members to change their behaviour when being processed.
Apply the DumpClassAttribute to classes to do the same.
Objects can also implement the IDebugDumpMask interface in order to customise the visibility of their members using more complex logic, if required.

Enjoy!


Top artists this week from Last.fm

My Twitter feed