DebuggerDisplay resolve to string at runtime
Asked Answered
M

4

20

Is there a way to access the string shown by DebuggerDisplayAttribute at runtime?

For our business objects i try to get automatic debugger information on exception handling. the actual object that was used while the exception was caught should be serialized to text to enhance the exception message. Since some attributes have other business objects as type, this could get really long if used recursively. Therefore i'd like to serialize to just the information that is already defined in DebuggerDisplay attributes of the class. The ToString() implementation of the classes can differ and are not usable for this task.

So is it possible to get the string that is shown in the debugger at runtime?

Mossgrown answered 15/4, 2011 at 10:26 Comment(0)
E
5

I don't think so (at least not without some effort on your part) - I've just done a bit of digging around and found this an article about Debugger Display Best Practices. It's not directly related, but it does highlight one thing:

Each property {expression hole} must be evaluated individually and done so once for every instance of this type in every debugger display window.

I expect that it's using the debugger to do the evaluation once the code has been broken into (kind of similar to how you would use the immediate window to evaluate a statement when you're at a breakpoint).

The long and short of it is that the resulting debugger display value for an object is not available to you at runtime, unless you're willing to parse each of the expression holes and use reflection to evaluate them yourself.

The article suggests that the most efficient way to provide debugger output is to have a private method do a String.Format over all the properties you want to display. You might want to consider making this a public method (maybe on an interface) and use this to retrieve your exception information from.

Ethelda answered 15/4, 2011 at 10:48 Comment(0)
M
2

Probably there is some way to extract that information, but wouldn't it be easier to redefine those classes with a property like this:

[DebuggerDisplay("{InfoProperty}")]
class X {
    public string InfoProperty {
        get { return "Debug and display info here"; }
    }
}

Then you include that InfoProperty in your error messages / logs instead of digging the way the data for display is reconstructed by Visual Studio.

Of course I am assuming that you can modify the business object classes, which might not be the case...

Musselman answered 15/4, 2011 at 10:32 Comment(1)
That would be my last exit, but that would mean that a hundred of classes need to be redesigned just for that.Mossgrown
B
0

Technically, sure, it's possible - you could access the DebuggerDisplayAttribute at runtime with Reflection and write some code that parses the string and again uses Reflection to get the values. This won't work if you've got anything but properties and fields inside those curly braces, though.

In any case, I strongly suggest you heed Mike or Paolo's advice -if there are hundred of classes you need to change - then find a way to change them automatically - either with something like Resharper's Structural Search and Replace, or a Regular Expression - it shouldn't take too long.

Boudicca answered 15/4, 2011 at 18:11 Comment(5)
DebuggerDisplay also supports method calls, like [DebuggerDisplay("{ToString()}")]Lowbrow
@Lowbrow I didn't know this syntax and I was searching if I can implement a ToString with same syntax as My DebuggerDisplayAttribute. But reverse also works. Thank.Muskrat
why would you put DebuggerDisplay at all in this case?Kannry
Calling ToString() is default behavior of the debugger. However, as a side note, generally speaking it is better to avoid overriding ToString(). Overriding this method creates code which is hardly maintainable because it is very hard to find where this specific implementation of ToString() is called. Therefore you will (almost) never know if you break something when you modify your ToString implementation. For that reason DebuggerDisplay is way better alternative if you just want to show something in debugger.Kannry
The advice supplied by Mike and Paolo assumes that you have control over the classes for which you want to display the information. What if DebuggerDisplayAttribute is the definition of what you want to display, on classes that you do not control? It seems shortsighted on Microsoft's part not to make this information available at runtime, as it diminishes the value of using their attribute at all, as well as that of the classes that they have developed that use the attribute.Mechanical
M
0

This static class provides a method to display the string shown by DebuggerDisplayAttribute at runtime. It utilizes https://github.com/codingseb/ExpressionEvaluator to evaluate the expressions. It also uses two extension methods, shown below the class.

using System;
using System.Diagnostics;
using System.Text;
using CodingSeb.ExpressionEvaluator;

namespace CentralFunctionality.UtilityTypes;

public static class DisplayDebugView
{
    /// <summary>
    /// Reproduces the output specified in <see cref="DebuggerDisplayAttribute"/>.  Strings are displayed without quotes, regardless of the use of ",nq", consistent with their display in Rider but not Visual Studio.
    /// </summary>
    /// <param name="obj">The object to get the debugger display for.</param>
    /// <returns>The string produced by the DebuggerDisplay value.</returns>
    public static string GetExpression( object obj )
    {
        if( obj.GetType( ).GetCustomAttributes( typeof(DebuggerDisplayAttribute), true ).HasOne( out var untypedAttribute ) )
        {
            DebuggerDisplayAttribute attribute = untypedAttribute as DebuggerDisplayAttribute;
            if( String.IsNullOrWhiteSpace( attribute.Value ) )
                return attribute.Value;

            var evaluator = new ExpressionEvaluator( obj );
            StringBuilder sb = new StringBuilder( );
            int cursorInOriginal = 0;
            while( attribute.Value.IndexOf( '{', cursorInOriginal ) >= 0 )
            {
                int openBrace = attribute.Value.IndexOf( '{', cursorInOriginal );
                sb.Append( attribute.Value.Substring( cursorInOriginal, openBrace - cursorInOriginal ) );
                
                int closeBrace = attribute.Value.IndexOf( '}', openBrace );
                string expression = attribute.Value.Substring( openBrace + 1, closeBrace - (openBrace + 1) );
                if( expression.EndsWith( ",nq" ) ) expression = expression.Substring( 0, expression.Length - 3 );
                try
                {
                    object expr = evaluator.Evaluate( expression );
                    if( expr is not string exprString )
                        exprString = GetExpression( expr );
                    sb.Append( exprString );
                }
                catch (Exception ex)
                {
                    
                }

                cursorInOriginal = closeBrace + 1;
            }

            sb.Append( attribute.Value.Substring( cursorInOriginal ) );
            return sb.ToString( );
        }

        return obj.ToString();
    }

    /// <summary>
    /// Displays the type name and debugger display expression in a manner similar to the display in Rider's debugger.
    /// </summary>
    /// <param name="obj">The object for which to display a line of information.</param>
    /// <returns>The object description.</returns>
    public static string GetAll( object obj )
    {
        return $"{{{obj.GetType().Namespace}.{obj.GetType().PrettyName()}}} {GetExpression( obj )}";
    }
}

The custom extension methods used are as follows:

    public static bool HasOne<TSource>( this IEnumerable<TSource> source, out TSource value, Func<TSource,bool> predicate = null )
    {
        try
        {
            value = predicate != null ? source.Single( predicate ) 
                                      : source.Single( );
            return true;
        }
        catch ( InvalidOperationException ex )
        {
            value = default(TSource);
            return false;
        }
    }

    public static string PrettyName( this Type type )
    {
        // From https://mcmap.net/q/372676/-c-quot-pretty-quot-type-name-function
        // That source had other, more complex options that might handle more complex type names.
        if ( type.GetGenericArguments( ).Length == 0 )
        {
            return type.Name;
        }

        try
        {
            var genericArguments = type.GetGenericArguments( );
            var typeDefeninition = type.Name;
            var unmangledName    = typeDefeninition.Substring( 0, typeDefeninition.IndexOf( "`" ) );
            return unmangledName + "<" + String.Join( ",", genericArguments.Select( PrettyName ) ) + ">";
        }
        catch
        {
            return type.Name;
        }
    }

I would suspect there are things that could be done in DebuggerDisplayAttribute that might work in Visual Studio and/or Rider that could break this, but this should support the functionality currently documented at https://learn.microsoft.com/en-us/visualstudio/debugger/using-the-debuggerdisplay-attribute?view=vs-2022, in the same manner that Rider currently supports it.

Mechanical answered 10/3 at 6:2 Comment(1)
This has one limitation: ExpressionEvaluator will only access public members, so any property or field used in the debug display that is not public will not be shown. If this limitation is a problem, then ExpressionEvaluator could be tweaked to access non-public properties, or you could create a dynamic wrapper that exposes all fields and properties as if they were public.Mechanical

© 2022 - 2024 — McMap. All rights reserved.