Using Reflection and Attributes for better Tracing and Logging data rendering

In most projects there is always a requirement for tracing and/or writing debugging information along with writing out log information. When dealing with objects it is usually up to the developer to override the ToString() method to ensure meaningful information is presented. However this does not always happen. Reflection presents an alternative solution to this problem, and one that i recently adopted on a project.

Rendering an Object using Reflection

The power of reflection, in this scenario, is that it allows one to easily navigate through an object’s properties and determine the corresponding name value pair. This then allows one to construct a string that can be easily rendered.

Rather than run through small code snippets I have included is a helper class that will support passing in either an object, or an object collection, and creates a string representation of the object. The complete listing can be found at the bottom of this blog.

The key to making this work is the ability of reflection to iterate through an objects properties and determine the associated value.

 Type objectType = item.GetType();
foreach (PropertyInfo propertyInfo in objectType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
    if (propertyInfo.CanRead)
    {
        string value = ProcessProperty(propertyInfo.PropertyType, propertyInfo.Name, propertyInfo.GetValue(item, null));

        // Code goes here
     }
}

Once the value has been determined, the type is inspected, and converted to a string. You should also notice that if an object returns a meaningful value from the ToString() call this this value is returned.

 string valueString = item.ToString();
Type objectType = item.GetType();
if (!objectType.IsValueType && valueString == objectType.FullName)
{    
    valueString = GetObjectValues(item, multipleLine);
}

In terms of using the code it is as easy as calling the ParameterCollectionToString or ParameterObjectToString method, within the debugging, logging, or tracing call.

Dealing with Sensitive Information

One issue that will arise in adopting such a generic solution, rather than one specific to each object (the ToString() solution) is how we hide Sensitive or Personally Identifiable Information. Take banking or medical applications for instance. Any tracing or logging solution should never disclose information such as bank account numbers or sensitive medical information. This is where attributes can help.

In the trace helper class shown above, for each property, a lookup is performed to see if an attribute, called SensitiveInformationAttribute, exists.

 SensitiveInformationAttribute piiAttribute = null;
foreach (Attribute attribute in member.GetCustomAttributes(typeof(SensitiveInformationAttribute), false))
{
    piiAttribute = attribute as SensitiveInformationAttribute;
    if (piiAttribute != null)
    {
        break;
    }
}
return piiAttribute;

This attribute provides a mechanism to hide an property value, and optionally provide a default string to display.

All one then has to do is attribute the object properties to mark them as sensitive information providing an optional display value; as in the code sample below.

 public class PersonDetails
{
    public string FirstName { get; set; }
    public string MiddleInitial { get; set; }
    public string LastName { get; set; }
    public string EmailAddress { get; set; }
 
    [SensitiveInformationAttribute]
    public string AccountNumber { get; set; }
 
    [SensitiveInformationAttribute]
    public string CreditInstitution { get; set; }
 
    public DateTime DateofBirth { get; set; }
 
    [SensitiveInformationAttribute("***-**-****")]
    public string IdentificationNumber { get; set; }
 
    public int? Age { get; set; }
}

The actual attribute implementation is actually quite simple. It does nothing more than define a few properties that affects the behaviour of the object rendering. It is the rendering that does the work in inspecting the attribute values. A full listing can be found below.

The provided implementation is rather simple. One could elaborate on this and provide a means to hide data using a pattern; such as only show last 4 characters, as if often done with credit card numbers.

Usage Example

So how does all this help?

When writing debugging information, say for a PersonDetails instance called person, one could possibly use:

 Debug.WriteLine(person);

The issue with doing this is the output merely renders the object type name. Not much use.

Using the trace helper if one was to replace this with the following you will get a much more meaningful representation:

 Debug.WriteLine(TraceHelper.ParameterObjectToString(person));

Using DebugView one can see the output from both statements. As you can see the trace helper version renders a highly useful value with all sensitive information hidden.

image_6_0EE67C27

For Tracing or Logging one would merely have to use these parameter methods before rendering the data.

Thus hopefully I have demonstrated and provided some useful code, that uses Reflection and Attribute programming, to provide a useful extensions to a Tracing or Logging solution.

The beauty of this approach is that it allows one to use the ToString() approach to rendering objects in a meaningful manner, and provides a generic solution for when not implemented. In addition is provide a mechanism for hiding sensitive (or PII) data.

A final note on performance. The intention of this code is to use in Tracing, Logging, and Debugging. As such one should always be able to turn off this code path for performance reasons.

Code Listings

The complete listing for the Trace Helper class is as follows:

 namespace MicrosoftConsulting.SensitiveInformationTracing.Diagnostics
{
    using System;
    using System.Globalization;
    using System.Reflection;
    using System.Text;

    /// <summary>
    /// This static class is used to assist tracing within the solution.
    /// </summary>
    public static class TraceHelper
    {
        /// <summary>
        /// Null display value.
        /// </summary>
        private const string TracingResourcesNullValue = @"null";

        /// <summary>
        /// Quote for displaying.
        /// </summary>
        private const string TracingResourcesQuote = "\"";

        /// <summary>
        /// Seperator for displaying.
        /// </summary>
        private const string TracingResourcesParamSeparator = @" | ";

        /// <summary>
        /// Unknown Property String Value.
        /// </summary>
        private const string TracingResourcesUnknownValue = "Unknown Property Value";

        /// <summary>
        /// Add details of a collection of parameters to the supplied log entry.
        /// </summary>
        /// <param name="parameters">Parameters to be described in the log entry.</param>
        /// <param name="logEntry">Log entry to add parameter information to.</param>
        public static string ParameterCollectionToString(object[] parameters)
        {
            // Make sure we have a parameter array which is safe to pass to Array.ConvertAll
            if (parameters == null)
            {
                parameters = new object[] { null };
            }

            // Get a string representation of each parameter that we have been passed
            string[] paramStrings;
            paramStrings = Array.ConvertAll<object, string>(parameters, ParameterObjectToString);

            // Add details of each parameter to log entry
            string allParamStrings = string.Join(TracingResourcesParamSeparator, paramStrings);

            return allParamStrings;
        }

        /// <summary>
        /// Convert a parameter object to a string for display in the trace.
        /// </summary>
        /// <param name="parameter">Parameter object to convert.</param>
        /// <returns>A string describing the parameter object.</returns>
        public static string ParameterObjectToString(object parameter)
        {
            string paramDesc = string.Empty;

            if (parameter == null)
            {
                paramDesc = TracingResourcesNullValue;
            }
            else
            {
                // Surround string values with quotes
                if (parameter.GetType() == typeof(string))
                {
                    paramDesc = String.Concat(TracingResourcesQuote, (string)parameter, TracingResourcesQuote);
                }
                else
                {
                    paramDesc = TraceHelper.GetObjectString(parameter);
                }
            }

            return paramDesc;
        }

        /// <summary>
        /// Gets a string representation of an object and items values.
        /// </summary>
        /// <param name="item">Object Item.</param>
        /// <returns>String Value.</returns>
        public static string GetObjectString(object item)
        {
            return GetObjectString(item, false);
        }

        /// <summary>
        /// Gets a string representation of an object and items values.
        /// </summary>
        /// <param name="item">Object Item.</param>
        /// <param name="multipleLine">Indicates is the output should be on a multiple lines.</param>
        /// <returns>String Value.</returns>
        public static string GetObjectString(object item, bool multipleLine)
        {
            // first call ToString and if returns type call GetValues
            string valueString = item.ToString();

            Type objectType = item.GetType();
            if (!objectType.IsValueType && valueString == objectType.FullName)
            {
                valueString = GetObjectValues(item, multipleLine);
            }

            return valueString;
        }

        /// <summary>
        /// Gets a string representation of an object and items values.
        /// </summary>
        /// <param name="item">Object Item.</param>
        /// <param name="multipleLine">Indicates is the output should be on a multiple lines.</param>
        /// <returns>String Value.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Tracing must not thrown an exception.")]
        private static string GetObjectValues(object item, bool multipleLine)
        {
            // check object not null
            if (item == null)
            {
                return string.Empty;
            }

            // get the configuration type
            Type objectType = item.GetType();

            StringBuilder printValue = new StringBuilder();
            AddToStringBuilder(printValue, objectType.Name, multipleLine);

            // look through all the properties for strings (public only) - ensure that the property has a GetProperty
            foreach (PropertyInfo propertyInfo in objectType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
            {
                SensitiveInformationAttribute attribute = GetPiiAttribute(propertyInfo);
                if (attribute != null)
                {
                    if (!attribute.Hidden)
                    {
                        string value = GetPiiString(attribute.DisplayOverride, propertyInfo.Name);
                        AddToStringBuilder(printValue, value, multipleLine);
                    }
                }
                else
                {
                    // If the property has a get method, write the property value.
                    if (propertyInfo.CanRead)
                    {
                        string value = null;
                        try
                        {
                            value = ProcessProperty(propertyInfo.PropertyType, propertyInfo.Name, propertyInfo.GetValue(item, null));
                        }
                        catch (Exception)
                        {
                            value = TracingResourcesUnknownValue;
                        }

                        AddToStringBuilder(printValue, value, multipleLine);
                    }
                }
            }

            // look through all the fields for strings (public only)
            foreach (FieldInfo fieldInfo in objectType.GetFields(BindingFlags.Public | BindingFlags.Instance))
            {
                SensitiveInformationAttribute attribute = GetPiiAttribute(fieldInfo);
                if (attribute != null)
                {
                    if (!attribute.Hidden)
                    {
                        string value = GetPiiString(attribute.DisplayOverride, fieldInfo.Name);
                        AddToStringBuilder(printValue, value, multipleLine);
                    }
                }
                else
                {
                    string value = null;
                    try
                    {
                        value = ProcessProperty(fieldInfo.FieldType, fieldInfo.Name, fieldInfo.GetValue(item));
                    }
                    catch (Exception)
                    {
                        value = TracingResourcesUnknownValue;
                    }

                    AddToStringBuilder(printValue, value, multipleLine);
                }
            }

            return printValue.ToString();
        }

        /// <summary>
        /// Gets the PersonallyIdentifiableInformation Attribute.
        /// </summary>
        /// <param name="member">Member Information.</param>
        /// <returns>Personally Identifiable Information Attribute.</returns>
        private static SensitiveInformationAttribute GetPiiAttribute(MemberInfo member)
        {
            SensitiveInformationAttribute piiAttribute = null;

            foreach (Attribute attribute in member.GetCustomAttributes(typeof(SensitiveInformationAttribute), false))
            {
                piiAttribute = attribute as SensitiveInformationAttribute;
                if (piiAttribute != null)
                {
                    break;
                }
            }

            return piiAttribute;
        }

        /// <summary>
        /// Gets the string from the Attribute name.
        /// </summary>
        /// <param name="displayOverride">The display override.</param>
        /// <param name="memberName">The member name.</param>
        /// <returns>String value to display.</returns>
        private static string GetPiiString(string displayOverride, string memberName)
        {
            return string.Format(CultureInfo.CurrentCulture, "{0} = {1}", memberName, (displayOverride == null) ? string.Empty : displayOverride.ToString());
        }

        /// <summary>
        /// Appends the given string to the given builder.
        /// </summary>
        /// <param name="builder">String Builder.</param>
        /// <param name="value">String Value.</param>
        /// <param name="multipleLine">Multiple line indicator.</param>
        private static void AddToStringBuilder(StringBuilder builder, string value, bool multipleLine)
        {
            if (value != null)
            {
                if (multipleLine)
                {
                    builder.AppendLine(value);
                }
                else
                {
                    builder.Append(string.Concat(value, "; "));
                }
            }
        }

        /// <summary>
        /// Returns a string from an object Property/Field.
        /// </summary>
        /// <param name="propertyType">Property/Field type.</param>
        /// <param name="propertyName">Property/Field name.</param>
        /// <param name="propertyValue">Property/Field value.</param>
        /// <returns>String of the Property/Field.</returns>
        private static string ProcessProperty(Type propertyType, string propertyName, object propertyValue)
        {
            string value = null;

            if (propertyValue != null)
            {
                if (propertyType == typeof(string))
                {
                    // see if underlying type is a string and persist the value
                    // get the value and ensure not null
                    string objectValue = propertyValue as string;
                    if (!string.IsNullOrEmpty(objectValue))
                    {
                        value = string.Format(CultureInfo.CurrentCulture, "{0} = {2}{1}{2}", propertyName, objectValue, TracingResourcesQuote);
                    }
                }
                else if (propertyType.IsEnum)
                {
                    // look for enum types and persist the value
                    value = string.Format(CultureInfo.CurrentCulture, "Enum {0} = {1}", propertyName, Enum.GetName(propertyType, propertyValue));
                }
                else if (propertyType.IsValueType)
                {
                    // look for other value type
                    value = string.Format(CultureInfo.CurrentCulture, "{0} = {1}", propertyName, (propertyValue == null) ? string.Empty : propertyValue.ToString());
                }
                else
                {
                    // reference type so return the type name
                    value = string.Format(CultureInfo.CurrentCulture, "{0} Type = {1}", propertyName, (propertyType.Name == null) ? string.Empty : propertyType.Name);
                }
            }
            else
            {
                value = value = string.Format(CultureInfo.CurrentCulture, "{0} = {1}", propertyName, TracingResourcesNullValue);
            }

            return value;
        }
    }
}

As mentioned this is dependant on the Attribute class:

 namespace MicrosoftConsulting.SensitiveInformationTracing.Diagnostics
{
    using System;

    /// <summary>
    /// This attribute class is used to hide PII informaiton when converting a class to a string.
    /// </summary>
    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
    public sealed class SensitiveInformationAttribute : Attribute
    {
        /// <summary>
        /// Default string top display when not hidden.
        /// </summary>
        private const string DefaultStringValue = "PII";

        /// <summary>
        /// String value to display when not hidden.
        /// </summary>
        private string displayOverride;

        /// <summary>
        /// Initializes a new instance of the PersonallyIdentifiableInformationAttribute class.
        /// </summary>
        public SensitiveInformationAttribute()
        {
            this.Hidden = true;
        }

        /// <summary>
        /// Initializes a new instance of the PersonallyIdentifiableInformationAttribute class.
        /// </summary>
        /// <param name="displayOverride">Value to display in ToString.</param>
        public SensitiveInformationAttribute(string displayOverride)
        {
            this.Hidden = false;
            this.displayOverride = displayOverride;
        }

        /// <summary>
        /// Gets the string override value when marked as PII.
        /// </summary>
        public string DisplayOverride
        {
            get
            {
                return string.IsNullOrEmpty(this.displayOverride) ? DefaultStringValue : this.displayOverride;
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether the value should be totally hidden.
        /// </summary>
        public bool Hidden { get; set; }
    }
}

Written by Carl Nolan