C# How to Write a Source Generator Part 2/5: Setting Up

Kafka Wanna Fly
5 min readDec 15, 2022

--

Our solution will have 3 parts: Attribute declaration, source generator code and unit test.

Initialize Projects

I will use Visual Studio Community 2022 in this story. Different IDE may have different buttons and layouts but the code behavior should be the same.

Create a Class Library in Visual Studio 2022
Create Class Library in Visual Studio 2022

To begin with, Create a Project > Choose Class Library. Do this twice, their name should be:

  • BindableProps: This project will contain attribute classes. They act as signals for generators to focus on a certain piece of code. After packing up the library, user (dev) will use those classes for their business.
  • BindablePropsSG: We will write Source Generator in this project. The code will run silently in background and not visible to user.

Finally, create a xUnit Test Project. We will need it for testing and debugging later on.

The solution structure would look like this:

Solution Structure
Solution Structure with 3 Projects

Linking Projects and Declare Dependencies

Right click to each project > Edit Project File. Change their to content to this:

<!-- BindableProps.csproj -->

<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<LangVersion>10.0</LangVersion>
<ImplicitUsings>true</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\BindablePropsSG\BindablePropsSG.csproj"
PrivateAssets="contentfiles;build"
SetTargetFramework="TargetFramework=netstandard2.0"
ReferenceOutputAssembly="false" />
</ItemGroup>

<ItemGroup Label="Package">
<!-- Include source generator as an analyzer -->
<!-- It will be packed with BindableProps -->
<!-- And when people import the library,
their IDE will run the generator correctly-->
<None Include="..\BindablePropsSG\bin\$(Configuration)\netstandard2.0\BindablePropsSG.dll"
PackagePath="analyzers\dotnet\cs"
Pack="true"
Visible="false" />
</ItemGroup>

<!-- Enable trimming support on .NET 6 -->
<PropertyGroup Condition="'$(TargetFramework)' == 'net6.0'">
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
<IsTrimmable>true</IsTrimmable>
</PropertyGroup>
</Project>
<!-- BindablePropsSG.csproj -->

<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>10.0</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IncludeBuildOutput>false</IncludeBuildOutput>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.4.0" />
</ItemGroup>
</Project>
<!-- UnitTest.csproj -->
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.2.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.XUnit" Version="1.1.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing.XUnit" Version="1.1.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeRefactoring.Testing.XUnit" Version="1.1.1" />
<PackageReference Include="FluentAssertions" Version="6.8.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\BindablePropsSG\BindablePropsSG.csproj" />
<ProjectReference Include="..\BindableProps\BindableProps.csproj" />
</ItemGroup>

<ItemGroup>
<!-- We will create this folder -->
<!-- It holds some sample code in a txt file -->
<None Update="TestData\**\*.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>

First Run To Make Sure We Correctly Set Up

In BindableProps project, create a class, BindableProp.cs:

namespace BindableProps
{
// We want to apply this attribute only to a field
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class BindableProp : Attribute
{
// Since we want to generate a BindableProperty for UI component.
// Each prop below represent a parameter of BindableProperty.Create() function.
// Other information, such as data-type, can be infered from the field declaration which this attribute attached to.

public int DefaultBindingMode { get; set; }

public string ValidateValueDelegate { get; set; }

public string PropertyChangedDelegate { get; set; }

public string PropertyChangingDelegate { get; set; }

public string CoerceValueDelegate { get; set; }

public string CreateDefaultValueDelegate { get; set; }


public BindableProp()
{

}
}
}

In BindablePropsSG project, create a folder called Generators. We will put all generators class inside this place. Create a class, BindablePropSG.cs:

namespace BindablePropsSG.Generators
{
// The [Generator] attribute cause the compiler to consider this class as a source generator
// There are others generator interface out there
// IIncrementalGenerator is chosen because of performance reason
// With this guy, our generator would not run over and over again every time dev types some random things
// It will invoke our generator when modification happens in our interested pieces of code
[Generator]
public class BindablePropSG : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
Console.WriteLine("Happiness is an allegory. Unhappiness a story.");
}
}
}

In UnitTest project, create a file inside this folder structure, TestData/Shared/NoUseAtAll.txt:

namespace UnitTest.TestData.BindablePropTest
{
public partial class MyClass
{
string title;

string description = "To the moon";
}
}

Create a new class at the root directory of UnitTest project, TestHelper.cs:

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System.Text.RegularExpressions;

namespace UnitTest
{
internal class TestHelper
{
// Whenever dev writes code, the compiler read and send the source code to generators and analyzers
// The analyzers may feedback with syntax errors or warnings
// The source generator may produce additional code
// What we're doing here is simulating that process
public static string? GetGeneratedOutput(string sourceCode, IIncrementalGenerator generator)
{
var syntaxTree = CSharpSyntaxTree.ParseText(sourceCode);
var references = AppDomain.CurrentDomain.GetAssemblies()
.Where(assembly => !assembly.IsDynamic)
.Select(assembly => MetadataReference
.CreateFromFile(assembly.Location))
.Cast<MetadataReference>();

var compilation = CSharpCompilation.Create("SourceGeneratorTests",
new[] { syntaxTree },
references,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

CSharpGeneratorDriver.Create(generator)
.RunGeneratorsAndUpdateCompilation(compilation,
out var outputCompilation,
out var diagnostics);


diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error)
.Should().BeEmpty();

return outputCompilation.SyntaxTrees.Skip(1).LastOrDefault()?.ToString();
}

public static string RemoveAllWhiteSpace(string content)
{
return Regex.Replace(content, @"\s+", string.Empty);
}
}
}

Create a another class at the root directory of UnitTest project, BindablePropTest.cs:

namespace UnitTest
{
public class BindablePropTest
{
// Notice the class name
// We will call the BindablePropSG from this place
IIncrementalGenerator generator = new BindablePropSG();

[Fact]
public void ShouldDoNothing()
{
var sourceCode = File.ReadAllText(
Path.Combine(
Directory.GetCurrentDirectory(),
"TestData",
"Shared",
"NoUseAtAll.txt"
)
);

var generatedCode = TestHelper.GetGeneratedOutput(sourceCode, generator);

generatedCode.Should().BeNullOrEmpty();
}
}
}

After all this long, we can right click to the function name and click Run Test or put a break point to Debug Test.

Run or Debug test
Run or Debug a Test Case

If things run fine, we can begin to code!

--

--