Noninvasive DebuggerDisplay Attributes
Development | Nathan Chappell

Noninvasive DebuggerDisplay Attributes

Monday, Feb 6, 2023 • 11 min read
Visual Studio provides a means for developers to modify how the Visual Studio debugger displays types - without modifying your application code.

Intro

Visual Studio offers a number of ways to customize what messages are displayed in the debugger. I found that I liked using the DebuggerDisplayAttribute for this purpose. The main problem with this approach is that it pollutes the source code. My senior was not particularly pleased with that, so I found out that Visual Studio grabs DebuggerDisplay attributes declared at the assembly level from an assembly named autoexp.dll. If you are willing to recompile your own .dll, then you can replace this one with your own, and whatever DebuggerDisplay attributes are present will be used by the Visual Studio debugger.

Requirements

When I shared this tip with some co-workers, the immediate reaction was “override ToString() FTW.” While missing my point, they brought up a good one - this is not the simplest way to improve your debugger’s ability to display information. So I will list the criteria that are probably necessary and sufficient for you to consider this approach:

  1. You have complex classes for which a consice summary would improve your efficiency debugging your program
  2. You do not wish to modify your application’s source code

If either of these are not true, then there are easier ways to accomplish this. Some motivations for (2) could be that the resulting .dll is handed over to clients, and you don’t want such debugging code handed off. You may be thinking “conditional compilation!” Again, you are still modifying the application source code. Another motivation for (2) is somewhat more “philosophical” - what is the “right” display for these classes? You wanted to improve their display because it improves your debug-time efficiency, but who’s to say that something different isn’t more appropriate for another developer on the project? What’s the solution then? It seems like it’s going to get awkward to deal with this situation in a consistent manner, also considering that now these debugger-related-attributes – which ostensibly should be different for different developers – need to be dealt with by source control.

Solution

As explained here, there is a way to relate attributes to types by declaring them at the assembly level in a lonely little file called autoexp.dll. The full path to this file (for me, using Community 2022) is:

  • C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Packages\Debugger\Visualizers\Original\autoexp.dll

In the directory, you will also find a file autoexp.cs, which is just an ordinary (if not a little bare) C# file. As the documentation explains, these are the “default” values used by Visual Studio. If you wish to include your own attributes, you need to recompile autoexp.dll, details of this process will be discussed below. Once you’ve recompiled this file, the Visual Studio debugger will use all the attributes declared in autoexp.dll to modify the display of your types.

Technical Details

First we will present some sample code, and look at how it is displayed in the debugger. We will then show the use of the DebuggerDisplayAttribute and see how it improves the debugging experience. Then we will move the attribute to autoexp.dll. Finally, we will suggest a way to incorporate this technique into your workflow.

Sample Code

Here is our amazing class to work with:

    public class Customer
    {
        public Customer(
            string id,
            string firstName,
            string lastName,
            IReadOnlyCollection<string> comments
        )
        {
            Id = id;
            FirstName = firstName;
            LastName = lastName;
            Comments = comments;
        }

        public string Id { get; }
        public string FirstName { get; }
        public string LastName { get; }
        public IReadOnlyCollection<string> Comments { get; }
    }

The Ugly

Here is what a collection of 10 customers looks like in the debugger.

Ugly

Not very helpful. If we try to get some actual information about our instances, it just gets worse.

Uglier

The Bad

We can make a significant improvement to our debugging experience by adding an attribute like so:

    [DebuggerDisplay("{Id} ({Comments.Count}) {LastName}, {FirstName}")]
    public class Customer
    {
        /* ... */
    }

In the debugger, now we have the following:

Pretty

Informative and ledgible, our debugger is now far more effective at providing useful information quickly.

I’ve labeled this section The Bad because while our result is no longer ugly, we’ve violated one of our requirements: we’ve modified our source code in the name of debug-time convenience. We will fix this issue in the next section.

The Good

To test out this technique, first I recommend that you modify the autoexp.cs file directly, recompile, and check that it works. In a later sub-section we will pose a recommendation for how to incorporate this technique into your workflow in a more reasonable manner.

Recompile autoexp.cs

Add the following line to the bottom of the autoexp.cs file we identified earlier (don’t break it into multiple lines unless you want to). I ended up needing to modify some security settings for files in this directory. Check with your sysadmin to find out if this is strictly necessary, or could cause any security issues (the bold and daring will likely just assume Full Control of whatever files get in their way…).

[assembly: DebuggerDisplay(
    "{Id} ({Comments.Count}) {LastName}, {FirstName}",
    Target = typeof(DebuggerDisplayDemoCode.Customer))
]

Important to note is the Target named-parameter. I’ve put in here the fully-qualified “target” type that the DebuggerDisplayAttribute should apply to.

In order to recompile autoexp.cs, we will need access to csc (the csharp compiler), and we will need to reference the assembly containing the definition of DebuggerDisplayDemoCode.Customer. To get access to csc, open up the Developer PowerShell for VS 2022. I find having to use this “special version” of powershell to be a bit annoying – see the appendix for my approach.

Once you have access to csc and are located in the folder containing autoexp.cs, you can run the following command to recompile the file:

csc -target:library `
    -r:C:\Path\To\Application\ApplicationName\bin\Debug\ApplicationName.{dll, exe} `
    autoexp.cs

-r means reference, as in reference this assembly when you compile. Replace Path\To\Application and ApplicationName with whatever is appropriate for you (also choose either .exe or .dll, depending on the output of your build process). NOTE: input csc /? to get a list of options you can pass to csc.

Assuming everything works, you should be able to run your debugger and see that the attribute has been applied.

An Improvement

On the one hand, we’ve cleaned up our code by removing those pesky attributes to some obscure autoexp.dll. Good stuff. Now we would like to not pollute this obscure autoexp.cs file with stuff that came from our local project. Luckily for us, we can have that cake and eat it too - just put the relevant attributes in a .cs file somewhere else, and include it in the source list when you recompile autoexp.cs. Here is a demonstration.

First, create a similar file in the root of the project - by root we mean the directory above where the .csproj file is located. Basically, we don’t want this file to get sucked up into the building of the application, so we need to make sure it isn’t in a place where it would get found (otherwise explicitly exclude it from the project where you must include it).

// ProjectName.autoexp.cs  // recommended file name

// csc -target:library `
//      -r:'C:\Path\To\Application\ApplicationName\bin\Debug\ApplicationName.{dll, exe}' `
//      -out:'C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Packages\Debugger\Visualizers\Original\autoexp.dll' `
//      'C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Packages\Debugger\Visualizers\Original\autoexp.cs'
//      'C:\Path\To\Application\ApplicationName.autoexp.cs'

using System.Diagnostics;

[assembly: DebuggerDisplay(
    "{Id} ({Comments.Count}) {LastName}, {FirstName}",
    Target = typeof(DebuggerDisplayDemoCode.Customer))
]

The commented csc command now includes the full path to autoexp.cs, but now also includes ApplicationName.autoexp.cs, which should be this very file. It’s worth mentioning that while the -out: parameter wasn’t technically necessary before, it is now.

Internal Problems

You may notice that the type we are currently applying our attribute to is public. Suppose that instead, Customer was internal. Then, in principle, our technique should fail – when we try to recompile autoexp.dll, the compiler will see that the type is not to be used by other assemblies, namely autoexp.dll. In fact, when I try to recompile I get the following error:

...\autoexp.cs(144,45): error CS0122: 'Customer' is inaccessible due to its protection level

It turns out that dealing with internal types is not a show-stopper, just a slight complication. .NET offers us a means to create friend assemblies, that is, it allows us to specify that an assembly should expose its internal types to another assembly. It may require a few tries to get just right, I will explain what I did to get it to work.

My project has an AssemblyInfo.cs file. These are obsolete with dotnet-core, but Visual Studio created one for me when I created the application. I’ve decided that’s where I would like my assembly-level attribute exposing the internals, so I add the following attribute declaration to the bottom of the file:

[assembly: InternalsVisibleTo("autoexp")]

Now I recompile everything and see that it works. Note that we weren’t able to completely avoid altering the source-code in this case – we had to add that attribute somewhere. It’s worth mentioning that this particular attribute can be configured through the .csproj file, depending on your build-process. Here is an example, note that I’ve also included a Compile@Remove – in this project the file ApplicationName.autoexp.cs was in a location where it would be found automatically.

<ItemGroup>
    <Compile Remove="ApplicationName.autoexp.cs" />
    <InternalsVisibleTo Include="autoexp" />
</ItemGroup>

NOTE

If you need to expose your internals this way, make sure you specify the -out: parameter to csc. The reason why, explained here, is that “the compiler has not yet generated the name for the assembly”. This seems pretty reasonable… The compiler sees the assembly has an attribute exposing its internals, but to who? How should it know that they are exposed to the assembly it’s currently building?

Better Practices

I’m still not sure what the “best practices” are for using this capability, but here is roughly what we intend to do in our project.

  1. Utilize the “default/ignored” pattern for source control (see appendix)
  2. Include the compilation command in the default autoexp.cs file

In the end, the file in the root of our project looks roughly like this:

// DEFAULT FILE!  This file should not be used or altered.
// Copy this file to ProjectName.autoexp.cs and make changes - that file will be ignored by source control.

// After making desired changes, run the following compilation command:

// csc -target:library `
//      -r:'C:\Path\To\Application\ApplicationName\bin\Debug\ApplicationName.{dll, exe}' `
//      -out:'C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Packages\Debugger\Visualizers\Original\autoexp.dll' `
//      'C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Packages\Debugger\Visualizers\Original\autoexp.cs'
//      'C:\Path\To\Application\ApplicationName.autoexp.cs'

// using System.Diagnostics;

// Here are some examples of how to use this file and the DebuggerDisplayAttribute

// [assembly: DebuggerDisplay(
//     "{Id} ({Comments.Count}) {LastName}, {FirstName}",
//     Target = typeof(DebuggerDisplayDemoCode.Customer))
// ]

Conclusion

With Visual Studio, it’s possible, under a variety of circumstances, to enable developers to personalize their debugging experience without polluting the codebase.

I’d make a remark that what we’ve explored here is typically the type of thing that makes me not like using IDEs like Visual Studio. Compiling some random file magically changes the behavior of core-features. While this “feature” is documented fairly well, it doesn’t seem like it’s advertised very well. How many other random little .dlls can be modified with great and useful effect for Visual Studio? The world may never know, and most likely won’t. But, all complaining aside, I actually rather like this approach. You don’t need 100 years of experience configuring Visual Studio to understand it - you just need to know that some .dll is relevant, and how to deal with .dlls.

Appendix

  1. Generating Sample Customers
  2. A Few Tips for using Powershell
  3. Default/Ignore pattern in git

Generating Sample Customers

This is off-topic, but here is the Util class used to generate some customers. There are a couple lines worth highlighting:

  • GetAllFullNames() computes a cartesian product of all first and last names.
  • I stole .OrderBy(_ => rnd.Next()).Take(n) from stackoverflow. Note that while this is a perfectly elegant way to express “get a random sample of size n without repetition,” it is not optimal and not necessarily appropriate in all cases.
  • The result of GenComments are unimportant, I just wanted an IReadOnlyCollection with varying sizes as a member.
    internal static class CustomerUtil
    {
        public static IEnumerable<Customer> GenerateCustomers(int n) => GetAllFullNames()
            .OrderBy(_ => rnd.Next())
            .Take(n)
            .Select(randomName => new Customer(GenId(), randomName.FirstName, randomName.LastName, GenComments()));

        
        private static readonly Random rnd = new Random();
    
        private static readonly string[] firstNames = new[]
        {
            "James",
            "Robert",
            "John",
            "Michael",
            "David",
            "Mary",
            "Patricia",
            "Jennifer",
            "Linda",
            "Elizabeth",
        };

        private static readonly string[] lastNames = new[]
        {
            "Smith",
            "Johnson",
            "Williams",
            "Brown",
            "Jones",
            "Garcia",
            "Miller",
            "Davis",
            "Rodriguez",
            "Martinez",
        };

        private static IEnumerable<(string FirstName, string LastName)> GetAllFullNames() => firstNames.Join(lastNames, x => 1, x => 1, (f, l) => (f, l));

        private static string GenId() => $"{rnd.Next(2000).ToString("D04")}-{rnd.Next(2000,9999),4}";

        private static IReadOnlyCollection<string> GenComments() => Enumerable.Range(0, rnd.Next(2, 5)).Select(i => $"Comment {i}.").ToArray();

        
    }

A Few Tips for using Powershell

I’m no powershell expert, but I can give two tips that will be immediately useful to someone after reading this article.

  1. LaunchDev command
  2. Running the comment-commands quickly

LaunchDev

Getting access to the tools available to the “Developer Powershell” is something that became so useful to me that I added the following command to my profile (located at C:\Users\UserName\Documents\PowerShell\profile.ps1):

function LaunchDev {
    & 'C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\Launch-VsDevShell.ps1'
}

The file pointed to here is the powershell script that will get run internally anyways, so by invoking it ourselves we can get access to all those tools without leaving our current prompt. Want to know more? You know what to do (go read the script source).

Running CSC Quickly (in VSCode)

Consider again the commented compilation command:

// csc -target:library `
//      -r:'C:\Path\To\Application\ApplicationName\bin\Debug\ApplicationName.{dll, exe}' `
//      -out:'C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Packages\Debugger\Visualizers\Original\autoexp.dll' `
//      'C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Packages\Debugger\Visualizers\Original\autoexp.cs'
//      'C:\Path\To\Application\ApplicationName.autoexp.cs'

First, The command is broken up into multiple lines to improve readability, but also to improve maintainability. We can easily see what assemblies and sources are being used, and can add to or remove from these lists quickly. Next, note that each line ends with a backtick (`). In powershell, this will act as a line continuation, so all lines get concatenated before executing the command. Finally, this comment is designed to be executed quickly in VSCode.

First, select the text. The following keystrokes can be executed without forcing your hands to leave home: gi

Keystroke Command Purpose
Ctrl-/ Toggle Line Comment Un-comment the command
Ctrl-c Copy Copy to clipboard (for terminal)
Ctrl-z Undo Re-comment the command
Ctrl-` View: Toggle Terminal Prepare to execute
Ctrl-v Paste Input command
Enter (Execute Command) Run the command

Here’s a little gif to demonstrate. After the cursor is positioned, the rest of the commands are executed directly from the keyboard (I don’t want to say that it’s fast, but it’s fast enough for me).

Run command without leaving home

Default/Ignore pattern in git

Consider the following problem:

  • I have a file that I want to have checked into git, but I want subsequent changes ignored.

This is particularly useful for files related to configuration, where there may be specific values that contributors to a project may need to fill in for themselves, but these changes aren’t relevant to the project as a whole and shouldn’t be tracked by git. Before you waste two hours on stack-overflow trying to sort this out (like someone I know did), let’s just read what git has to say about it: (emphasis mine)

Users often try to use the assume-unchanged and skip-worktree bits to tell Git to ignore changes to files that are tracked. This does not work as expected, since Git may still check working tree files against the index when performing certain operations. In general, Git does not provide a way to ignore changes to tracked files, so alternate solutions are recommended.

For example, if the file you want to change is some sort of config file, the repository can include a sample config file that can then be copied into the ignored name and modified. The repository can even include a script to treat the sample file as a template, modifying and copying it automatically.

I recommend you read the first emphasized sentence one more time if it’s not perfectly clear: if git starts tracking a file, it cannot be ignored.

To illustrate the technique, suppose we added our ProjectName.autoexp.default.cs file to the root of our project. Next, add the target filename to .gitignore (by target filename I mean the filename without .default):

# .gitignore

ApplicationName.autoexp.cs

Now other developers on the project can copy ProjectName.autoexp.default.cs to ProjectName.autoexp.cs and make any desired changes, and these changes will not be tracked by git.