How to Debug C# 9 Source Code Generators

Build and debug a source code generator with C# 9 and Visual Studio.

I’ve been working on a follow up to my previous post on a different take on MVVM (Thinking Out Loud: Mvvm Navigation for XAML Frameworks such as Xamarin.Forms, UWP/WinUI, WPF and Uno) and I’ve got the code to a point that I’m mostly happy with it. However, there’s a couple of things that are just tedious for developers to have to do and something that’s perfect for source code generation. More about that in a later post; in this post I want to cover how you can start to generate code and how you can debug the source code generation process.

Before we get started, for a more detailed set of instructions on how to get started with code generation, the dotnet team has a great post on this from earlier this year – Introducing C# Source Generators.

Let’s jump in and create a simple code generator (remember the point of this post isn’t to talk in detail about code generation, it’s to show you how to debug the generation process).

We’ll start with just a vanilla .NET standard 2.0 class library, GenerationSample – this is the assembly where we’ll be doing the code generation. I’ve specifically picked a .NET Standard 2.0 class library to point out that even though the code generation is a feature of the C# 9 feature set, you can still use it to inject code into existing libraries. We’ll come back to this library in a minute, once we’ve created our code generator.

Next up, we need to create a new project that will house our code generator. Again, I’ll create a .NET Standard 2.0 library, called Generators. Creating a generator only requires us to reference the appropriate NuGet packages and then create a class that implements ISourceGenerator (and has the Generator attribute).

Here’s the updated csproj file with both the NuGet package references we need, as well as LangVersion set to preview

<Project Sdk="Microsoft.NET.Sdk">
	<PropertyGroup>
		<TargetFramework>netstandard2.0</TargetFramework>
		<LangVersion>preview</LangVersion>
	</PropertyGroup>
	<ItemGroup>
		<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0-3.final" PrivateAssets="all" />
		<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.0" PrivateAssets="all" />
	</ItemGroup>
</Project>

To do the code generator we’ll create a class called DebuggableGenerator that implements the ISourceGenerator interface. Note: If you copy the code from the introductory post from April 2020 by the dotnet team, you’ll see a bunch of errors relating to the ISourceGenerator interface.

These errors can easily be fixed by clicking on the dropdown next to the error and selecting Implement Interface, and then removing the old methods – it looks like the ISourceGenerator interface changed since the post back in April. Our code generator should look similar to the following code at this point.

using Microsoft.CodeAnalysis;
using System;
using System.Diagnostics;

namespace Generators
{
    [Generator]
    public class DebuggableGenerator : ISourceGenerator
    {
        public void Execute(GeneratorExecutionContext context)
        {
            Debug.WriteLine("Execute code generator");
        }
        public void Initialize(GeneratorInitializationContext context)
        {
            Debug.WriteLine("Initalize code generator");
        }
    }
}

The last thing to do, to complete the setup, is to add a reference to the Generators library into the GenerationSample library. The csproj should now look similar to the following.

<Project Sdk="Microsoft.NET.Sdk">
	<PropertyGroup>
		<TargetFramework>netstandard2.0</TargetFramework>
		<LangVersion>preview</LangVersion>
	</PropertyGroup>
	<ItemGroup>
		<ProjectReference 
			Include="..\Generators\Generators.csproj"
			OutputItemType="Analyzer"
			ReferenceOutputAssembly="false" />
	</ItemGroup>
</Project>

A couple of things to note: We’ve set the LangVersion to preview and added a couple of additional attributes to the ProjectReference – these are required to link the code generator into the compilation process.

At this point, you’re good to start implementing your code generation. However, as pointed out by the dotnet team in the FAQ section of their post, there’s currently no built in support for debugging. You can set a breakpoint in Visual Studio but it won’t be hit. You can write Debug.WriteLine or Console.WriteLine statements but they won’t appear in the Output tool window (not even if you set the verbosity to Diagnostic).

Luckily, there’s quite a simple hack to give you a mostly-fully featured debugging experience. At the start of the Initialize methods (you can think of this as the entry point for code generation) add code to launch the debugger.

using Microsoft.CodeAnalysis;
using System.Diagnostics;

namespace Generators
{
    [Generator]
    public class DebuggableGenerator : ISourceGenerator
    {
        public void Execute(GeneratorExecutionContext context)
        {
            Debug.WriteLine("Execute code generator");
        }
        public void Initialize(GeneratorInitializationContext context)
        {
#if DEBUG
            if (!Debugger.IsAttached)
            {
                Debugger.Launch();
            }
#endif 
            Debug.WriteLine("Initalize code generator");
        }
    }
}

To ensure this code doesn’t accidentally end up in my Release code I’ve wrapped it using conditional compilation. The code also tests to see if the Debugger is already attached, otherwise you’ll find that it will continually prompt to launch a new debugging session.

Now if we force a rebuild of our GenerationSample library, we’ll see a prompt asking to specify where the code should be debugged.

From this dialog you can either select the existing instance of Visual Studio, or open a new instance. My preference it to open up a new instance of Visual Studio – it feels a bit to much like inception to debug the code generation in the same instance of Visual Studio but this comes down to whatever works for you.

Once the debugger is attached, you can step through the code, view variables, set breakpoints etc. Unfortunately it appears that Edit and Continue doesn’t work, so it does mean that in order to make changes you need to stop debugging, make changes and then rebuild the project in order to trigger the code generator to run.

And there you have it -the ability to debug your code generation library using Visual Studio.