Source Generator for DebuggerDisplayAttribute

While Debugging your application, it is often handy to have the DebuggerDisplayAttribute set, enabling you to quickly glance over complex data structures. While these aren’t way too much to type in your self, it would be handy to have the code Auto generated for you.

In this blog post, we will create a Source Generator using the Rosyln Compiler API to generate a DebuggerDisplayAttribute to list values of all public properties in the class. We will begin by creating a custom attribute which our Generator would use to identify the classes, for which to generate the DebuggerDisplayAttribute.

namespace InGen.Types.Attributes
{
    [AttributeUsage(AttributeTargets.Class)]
    public class AutoDebuggerDisplayStringAttribute:Attribute
    {
    }
}

The next step involves creating the Syntax Reciever, which would scan through the syntax nodes and detect the presence of a class declaration, which has our desired attribute.

private class AutoDebuggerDisplayStringSyntaxReciever : ISyntaxContextReceiver
{
    public ClassDeclarationSyntax IdentifiedClass { get; private set; }

    public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
    {
        if (context.Node is ClassDeclarationSyntax classDeclarationSyntax && classDeclarationSyntax.AttributeLists.Any())
        {
            var classDeclarationSemantics = context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax);

            if(classDeclarationSemantics.GetAttributes().Any(x=>x.AttributeClass.ToDisplayString() == "InGen.Types.Attributes.AutoDebuggerDisplayStringAttribute"))
            {
                IdentifiedClass = classDeclarationSyntax;
            }
        }
    }
}

We will now declare our Generator class and register for Syntax notification.

[Generator]
public class AutoDebuggerDisplayStringGenerator : ISourceGenerator
{
    public void Execute(GeneratorExecutionContext context)
    {
        // Discussed below
    }

    public void Initialize(GeneratorInitializationContext context)
    {
        context.RegisterForSyntaxNotifications(() => new AutoDebuggerDisplayStringSyntaxReciever());
    }

    private class AutoDebuggerDisplayStringSyntaxReciever : ISyntaxContextReceiver //where TAttribute:Attribute
    {
        // Defined above
    }

}

Once we register for Syntax notification, during the Compilation process the OnVisitSyntaxNode of the Syntax Reciever would be invoked for each node. This would help us identify our class.

Now it is time to fill in our Execute method to generate the actual code.

public void Execute(GeneratorExecutionContext context)
{
    var syntaxReceiver = (AutoDebuggerDisplayStringSyntaxReciever)context.SyntaxContextReceiver;

    var userClass = syntaxReceiver.IdentifiedClass;
    if (userClass is null)
    {
        return;
    }


    var properties = userClass.DescendantNodes().OfType<PropertyDeclarationSyntax>();
    var code = GetSource(userClass.Identifier.Text, properties);

    context.AddSource(
        $"{userClass.Identifier.Text}.generated",
        SourceText.From(code, Encoding.UTF8)
    );
}

private string GetSource(string className, IEnumerable<PropertyDeclarationSyntax> properties)
{
    var toStringContend = string.Join(",", properties.Select(x => $"{x.Identifier.Text}={{{x.Identifier.Text}}}"));

    var code = $@"

                namespace InGen.Client {{
                [System.Diagnostics.DebuggerDisplay(""{toStringContend}"")]
                    public partial class {className}
                    {{

                    }}
                }}";
    return code;
}

As observed in the Execute methodm once the Syntax Reciever identifies our class, we will iterate over all the Property declarations in the class, and use them to list the properties while generating the DebuggerDisplayAttribute. The code is pretty self explainatory.

Let us look at an example now.

// Input
namespace InGen.Client
{
    [AutoDebuggerDisplayString]
    public partial class AutoToStringDemo
    {
        public string UserName { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
}

// Generated Class
namespace InGen.Client {
[System.Diagnostics.DebuggerDisplay("UserName={UserName},FirstName={FirstName},LastName={LastName}")]
    public partial class AutoToStringDemo
    {

    }
}

This example could be further developed to skip properties which you are not interested by adding a SkipPropertyAttribute which could be added to the properties. But for the sake of the example, I have kept this simple.Keeping coding.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s