Debug attributes in .NET

I recently received a GitHub issue for Vogen. It was a proposal to add the DebuggerTypeProxyAttribute. I was familiar with a few of the debugger related attributes, but not this one. So I've thrown together this post with my findings on the various debug related attributes. You never know, they might help you when you're next debugging (debugging someone else's code, obviously!)

DebuggerDisplay #

This is a popular attribute. It tells the debugger what to display in the IDE. Given the following type:

public class Person {
public int Age { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}

... when looking at the type in the IDE (for instance, the Watch window in Visual Studio), you'll see:

But with a DebuggerDisplayAttribute:

[DebuggerDisplay("{FirstName} {LastName} ({Age})")]
public class Person

... you'll see:

🤓 note
Fred Flintstone is actually 41 years old according to Someone On The Internet.

You can also have expressions in these attributes, e.g.

[DebuggerDisplay("{FirstName} {LastName} ({Age + 2})")]

or

[DebuggerDisplay("{FirstName} {LastName} ({Age + System.Math.PI})")]

Multi-line string literals are also allowed:

[DebuggerDisplay(@"FirstName: {FirstName}
LastName:{LastName}
Age: {Age})"
)]

This is one of the most common debugger attributes. I've found it very useful, not just for my types, but for types I don't own. Here's an example:

// pretend that we don't own this type - for instance, it's in a library
public class Foo {
private string Name = "Fred";
private int Age = 41;
}

...then, put this at the top level, e.g. Program.cs

[assembly: DebuggerDisplay("{FooDebuggerDisplay.Describe(this)}", Target = typeof(Foo))]

... and here's the thing that inspects the type when debugging:

class FooDebuggerDisplay
{
public static string Describe(Foo foo) {
return $"Name={Name(foo)}, Age={Bar(foo)}";
}

public static string Name(Foo foo) => (string)GetFieldInfo(foo, "Name").GetValue(foo);

public static int Bar(Foo foo) => (int) GetFieldInfo(foo, "Age").GetValue(foo);

private static FieldInfo? GetFieldInfo(Foo foo, string name) => foo.GetType()
.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic);
}

... and here's the output in Visual Studio:

Unfortunately, using this attribute at the assembly level doesn't work in JetBrains Rider (although it does at the class level):

DebuggerStepThrough #

This attribute tells the debugger to step through the code instead of stepping into it. It's most useful for compiler generated code. To give an example of how it's used, consider the following methods:

    private int GetSomethingTrivial() {
return 0;
}

private int GetSomethingImportant() {
return 42;
}

public int CalculateSomethingImportant() {
return GetSomethingTrivial() +
GetSomethingImportant() +
GetSomethingTrivial();
}

We want to 'step in' to GetSomethingImportant, but we want to skip (step through) calls to GetSomethingTrivial. Seeing as these are in the same expression, we have no choice but to F11 (step into), which will step into both Trivial and Important methods.
But we can tell the debugger to skip the trivial method by marking it with an attribute:

    [DebuggerStepThrough]
private int GetSomethingTrivial() {
return 0;
}

This steps through the calls to the trivial method, even if a breakpoint is set (but only if 'Just My Code' is enabled).

🤓 JetBrains Rider
Rider has a nice feature (Smart Step Into) which lets you pick which method you want to step into when presented with more than one method in an expression

DebuggerTypeProxy #

Applying this attribute to a type, tells the debugger to use that type for debug information. The proxy type is usually a nested type. For performance reasons, debuggers do not instantiate these proxy types unless the user expands the type, or unless there's a DebuggerBrowsable attribute present (more on that in a bit.)

Here's an example:

[DebuggerTypeProxy(typeof(WrapperDebugView))]
public class Wrapper {
private readonly int _value;
private readonly bool _isSet;

public Wrapper() { }

public Wrapper(int value) {
_value = value;
_isSet = true;
}

public static Wrapper New(int value) => new Wrapper(value);

public int Value => _isSet
?
_value
: throw new Exception("not set!");

internal class WrapperDebugView {
private readonly Wrapper _wrapper;

public WrapperDebugView(Wrapper wrapper) => _wrapper = wrapper;

public bool IsSet => _wrapper._isSet;
public string Value => _wrapper._isSet ? _wrapper._value.ToString() : "[not set]";
}
}

... here's two instances:

Wrapper w1 = new Wrapper();
Wrapper w2 = Wrapper.New(123);

... and here's what it looks like in the debugger:

Note the familiar 'Raw view' section; you might've spotted these with the in-built proxies, like the one for Dictionary.

DebuggerBrowsable #

This can be applied to properties and fields and controls if the member is displayed, and how it is displayed. It is one of three values:

The first two are pretty self explanatory. Here's what the third one looks like...

Without the attribute:

public class People {
private List<Person> _items;
public People(List<Person> items) => _items = items;
}

.. when we have two people:

Person p1 = new Person(12, "a");
Person p2 = new Person(13, "b");

People people = new People(new() { p1, p2 });

... and we display it in the debugger:

Here's what it looks like with the attribute:

public class People {
==> [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
private List<Person> _items;

public People(List<Person> items) => _items = items;
}

... and now the same instance in the debugger:

DebuggerHidden #

The docs for this one says that the Common Language Runtime (CLR) attaches no semantics to this attributes. It says that the Visual Studio 2005 debugger does not allow breakpoints on a method marked with this attribute, nor does it stop on such a method.

That is still the case with the Visual Studio 2022 debugger:

However, this attribute has no effect in Rider:

DebuggerNonUserCode #

This attribute is a combination of DebuggerHidden and DebuggerStepThrough and is used for code not specifically created by the user. I can see this type useful in source generated code.

This attribute is only respected by Visual Studio. It has no effect on Rider:

DebuggerVisualizer #

This attribute is typically used at the assembly level. Its constructor takes two types, the visualizer, and the visualizer object source. Both are strings and both represent the fully qualified assembly type names.
The benefit of this is that the debugee doesn't need to reference the assembly containing the visualizer.
A walk-through on creating one can be found here.

DebuggerStepperBoundary #

This attribute represents a debugging boundary where the debugger will usually jump over the method with this attribute. The difference between this and DebuggerStepThrough is that breakpoints can be set and will be hit.

Summary #

We've seen how different debuggers handle these attributes. We've seen how useful they can be when you're debugging. And when you're going through the denial stage of debugging, you need all the help and support you can get!

(bot image created with https://mod-dotnet-bot.net/create-your-bot/)

🙏🙏🙏

Since you've made it this far, sharing this article on your favorite social media network would be highly appreciated 💖! For feedback, please 🦋 ping me on Bluesky! 🦋

Leave a comment

Comments are moderated, so there may be a short delays before you see it.

1 comments on this page

  • Mike Pelton

    commented

    Hi Steve - just wanted to say thanks for this - I didn't know about these attributes and I've been doing this C# stuff a very long time!

Published