Source Generator : AutoToString

In this post, we will look into an application of Source Generator. Quite often, during debugging, you would wish if you had overridden the ToString() method of the class, so that you could understand the state of the instance. These are especially useful when you are dealing with a collection of the type. The ToString() would provide you a glimpse of the state of the object instance, without having to investigate each property of each instance of the collection.

So what is Source Code Generators ? Source code generation is a form of meta programming, allowing you to generate source codes for your code base, without having to write it manually. It is quite useful for mudane repeatitive tasks like the ToString() overrides and implementation of INotifyPropertyChanged. The Source Code Generators makes use of Roselyn Compiler API to understand the existing code and generate new code based on it.

If you are beginging with Source Code Generators, Microsoft have an excellant blog entry on C# Source Generators .

Scenario under consideration

Back to our problem, we have following class.

public partial class Foo
{
    public int Property1 { get; set; } = 1;
    public int Property2 { get; set; } = 2;
    public int Property3 { get; set; } = 3;
}

We would like to auto generate the ToString() override such that it would return a string representing values of all the Properties. For example,

public string ToString()=>$"Property1={Property1},Property2={Property2},Property3={Property3}";

We could always write it ourselves, but it is mudane boiler plate code, not to mention possiblity of us not remembering to update the method each time we add or remove properties to the class.

For the same reason, we could automate the method generation.

AutoToString Attribute

As things would stand, we would not be interested in generating the ToString() override for every class and hence we should devise some way to mark the class for which we need to generate the method. For this purpose, we will declare and use an Attribute which would help us identifying the classes for which the method needs to be generated.

Let us first declare the attribute.

[AttributeUsage(AttributeTargets.Class)]
public class AutoToStringAttribute:Attribute
{
}

We will keep the attribute simple for the sake of this example, but it is always worth to note that this could be expanded to include more meta information – for examples, names of properties which needs to be considered when creating the string in ToString().

AutoToStringGenerator

We will now begin with our Generator. Each of the Code Generator needs to implement the ISourceGenerator interface.

[Generator]
public class AutoToStringGenerator : ISourceGenerator
{
    public void Execute(GeneratorExecutionContext context)
    {
    }

    public void Initialize(GeneratorInitializationContext context)
    {
    }
}

Do note that we have decorated our Generator with the mandatory GeneratorAttribute as well.

Remember that one of the constraints of our example is that we only need to generate the code for classes for which we have the AutoToStringAttribute. So we need some kind of hook for processing and Inspecting different Syntax Nodes in our code for identifying classes with match our condition.

This is achieved with the an instance of ISyntaxReceiver. The ISyntaxReceiver interface comprise of a single method would be invoked for every syntax node in our code base.

private class AutoToStringSyntaxReciever : ISyntaxReceiver
{
    public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
    {
    }
}

As one would expect, we will inspect each of the syntax nodes in the OnVisitSyntaxNode method and verify if it is a class with required Attribute.

private class AutoToStringSyntaxReciever : ISyntaxReceiver
{
    public ClassDeclarationSyntax IdentifiedClass { get; private set; }
    public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
    {
        if (syntaxNode is ClassDeclarationSyntax classDeclaration)
        {
            var attributes = classDeclaration.DescendantNodes().OfType<AttributeSyntax>();
            if (attributes.Any())
            {
                var autoToStringAttribute = attributes.FirstOrDefault(x => ExtractName(x.Name) == "AutoToString");
                if (autoToStringAttribute != null) IdentifiedClass = classDeclaration;
            }
        }
    }

    private static string ExtractName(TypeSyntax type)
    {
        while (type != null)
        {
            switch (type)
            {
                case IdentifierNameSyntax ins:
                    return ins.Identifier.Text;

                case QualifiedNameSyntax qns:
                    type = qns.Right;
                    break;

                default:
                    return null;
            }
        }

        return null;
    }
}

Notice that we have a property IdentifiedClass which would be set if we find a class which meets our condition. As you can observe in the code above, for each class declaration found (ClassDeclarationSyntax), we iterate over its Descendant Nodes to check if we have the specified Attribute.

The Magic

Time for going back to our Generator code and integrating the SyntaxReciever which we wrote now. The first step would require use to register for Syntax notification. This is done in the ISourceGenerator.Initialize method

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

Now that we have our notification registered, the last step is of course code generation. This is done in the ISourceGenerator.Execute method.

public void Execute(GeneratorExecutionContext context)
{
    var syntaxReceiver = (AutoToStringSyntaxReciever)context.SyntaxReceiver;
    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 Client {{
                    public partial class {className}
                    {{
                        public string ToString()=>$""{toStringContend}"";
                    }}
                }}";
    return code;
}

We will detech the properties in the Class recognized by our SyntaxReciever, similiar to our approach in detecting the Attributes earlier by iterating the Descendant Nodes, this time, however, filtering the PropertyDeclarationSyntax. We will then generate the code for our ToString() method with the help of the properties found.

The final step is as expected, to add the generated source to the Compilation.

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

Client Class

To test our newly created Source Code Generator, we will create decorat our Foo class with the AutoToStringAttribute.

[AutoToString]
public partial class Foo
{
    public int Property1 { get; set; } = 1;
    public int Property2 { get; set; } = 2;
    public int Property3 { get; set; } = 3;
}

If you check the generated source code now, then we will can observe we have the following code generated.

namespace Client
{
    public partial class Foo
    {
        public string ToString()=>$"Property1={Property1},Property2={Property2},Property3={Property3}";
    }
}

Note that we have used partial class for our client class. This is because unlike Fody or PostSharp, Code Generators doesn’t do IL weaving and and hence is incapable of modifying existing code. For this reason, we make use of partial classes to create new add-ons to existing classes in separate disk files.

Since the code generators are executed during Compile phase, the generated could would be updated each time you add or remove a property from the class, without you having to do anything special. It also supports Intellisense and all other IDE feature, which you would normally associated with manually written code.

As seen, Source Code generators have huge potentials and we will continue to explore them in some of the future posts. Until then, do check out the sample code discussed here in my Github repo.

Advertisement

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 )

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