Skip to main content

Good Practices

Content​

I am trying here to add good practices , as I see from the community and from my experience. The examples are from the https://github.com/ignatandrei/RSCG_WaitAndOptions project

If you are using a Roslyn Source Code Generator​

See files generated​

To see the files generated , add the following to the csproj file

<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GX</CompilerGeneratedFilesOutputPath>
</PropertyGroup>

Then go to the obj/GX folder and inspect the files generated

Example - click to expand

If you have downloaded https://github.com/ignatandrei/RSCG_WaitAndOptions , see src\Console_Wait\Console_Wait.csproj file

also, after compiling, you can see the files generated in the obj/GX folder from the src\Console_Wait folder

See if implements IIncrementalGenerator​

In simple terms, works better and faster with VS if RSCG implements IIncrementalGenerator instead of ISourceGenerator

The old way to create a RSCG was with ISourceGenerator interface. The new way is with IIncrementalGenerator interface.

The difference is that the IIncrementalGenerator interface has a method called "Initialize" that is called only once per modification of the code - it is not called if the code is not modified.

Also the source generators have been deprecated in favor of the new incremental generators.

See https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md

If you are creating a Roslyn Source Code Generator project​

It is a nuget package​

Follow best practices for NuGet packages. See https://learn.microsoft.com/en-us/nuget/create-packages/package-authoring-best-practices

Read the documentation​

The documentation is at https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.cookbook.md

For easy debugging, add IsRoslynComponent​

You can debug easy the component if you add the following code to the Roslyn csproj file

<PropertyGroup>
<IsRoslynComponent>true</IsRoslynComponent>
</PropertyGroup>

Then add a simple console ( or any other project) and reference the Roslyn project. ( Pay attention to ReferenceOutputAssembly property ,could be false or true)

<ItemGroup>
<ProjectReference Include="..\RSCG_Wait\RSCG_Wait.csproj" OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>

Then in the Roslyn project you can add a breakpoint and debug the code .

Example - click to expand

If you have downloaded https://github.com/ignatandrei/RSCG_WaitAndOptions , see src\RSCG_Wait\RSCG_Wait.csproj file

For continuous debugging​

If you want continuous debugging, a la dotnet watch run , you should delete the bin and obj files of the target project and run dotnet build again

Details

Example - click to expand This is the powershell that I use for this with the name rscg_build.ps1 . I run it in the src\Console_Wait folder from https://github.com/ignatandrei/RSCG_WaitAndOptions , i.e in the target project folder.

while($true)
{

cls
Write-Host "delete obj and bin"
gci obj -recurse | foreach{ri $_.FullName -recurse -force }
gci bin -recurse | foreach{ri $_.FullName -recurse -force }
dotnet clean
dotnet restore
dotnet build /p:EmitCompilerGeneratedFiles=true --disable-build-servers --force
dotnet run
Read-Host -Prompt "Press Enter to continue"

}

Aim for ReferenceOutputAssembly="false"​

Generally speaking , the work of a Roslyn generator is to generate code. So, you do not need to reference the assembly generated when publishing the build.

So, you should aim for ReferenceOutputAssembly="false" in the csproj file that reference the Roslyn generator.

Example - click to expand

Do not use all classes​

Add an attribute to be used on the classes that you want to add functionality to.

For example, in the project

public void Initialize(IncrementalGeneratorInitializationContext context)
{

var classToImplement = context.SyntaxProvider
.ForAttributeWithMetadataName("RSCG_IFormattableCommon.AddIFormattableAttribute",
CouldBeClass,
GetClassInfo)
.Collect();
;
//more code

I do prefer putting this attribute in a separate nuget package and reference it from the main program.

General attributes / code​

Problem - make internal or have another assembly referenced ( or the opposite) https://andrewlock.net/creating-a-source-generator-part-8-solving-the-source-generator-marker-attribute-problem-part2/

Add reference to another package when need just for compilation​

See the documentation at https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.cookbook.md#use-functionality-from-nuget-packages

For files generated​

Add .g.cs extension​

Add a .g.cs generated suffix so some tools consider the file to be generated

Example - click to expand

The following code is from the project https://github.com/ignatandrei/RSCG_WaitAndOptions

private void GenerateData(SourceProductionContext context /*other arguments*/)
{
context.AddSource("WaitGeneratorStart.g", $$"""
//generated code here
""");
}

Add auto-generated comment​

As a header of the file generated, add the following comment

//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version: ...
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
Details

Example - click to expand In the project https://github.com/ignatandrei/RSCG_WaitAndOptions , this the header

static string Header()
{
var version = ThisAssembly.Info.Version;
var name = ThisAssembly.Info.Title;
var header = $$"""
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool {{name}}.
// Runtime Version: {{version}}
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
""";
return header;
}

Add a version to the files generated​

[global::System.CodeDom.Compiler.GeneratedCode("GeneratorName", "1.0.0.0")]

You could use the version from the generator in order to know what version of the generator was used to generate the code.

Example - click to expand

You could use AssemblyInfo , as I have done myself into the project https://github.com/ignatandrei/RSCG_WaitAndOptions

In the csproj file , add the following

<ItemGroup>
<PackageReference Include="ThisAssembly.AssemblyInfo" Version="1.4.3" OutputItemType="Analyzer"
ReferenceOutputAssembly="false">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

And use when generating the code

var version=ThisAssembly.Info.Version;
var name = ThisAssembly.Info.Title;
var data = $$"""
namespace RSCG_Wait;
[global::System.CodeDom.Compiler.GeneratedCode("{{name}}", "{{version}}")]
public partial class OptionsFromBuild{

}
""";

Make the full path of the file generated shorter​

Because of the 260 characters limit for the path, you should the path of the files generated shorter. See more at https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=powershell

Example - click to expand

The files generated for the project src\Console_Wait\Console_Wait.csproj

are in the folder

obj\GX\RSCG_Wait\RSCG_Wait.WaitGenerator

i.e obj\GX\Name of the Nuget\Name of the class name that implements IIncrementalGenerator

mark the code as non - code coverage​

Add the following attribute to the generated code

[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
Example - click to expand

In the mentioned project src\RSCG_Wait\RSCG_Wait.csproj

var data = $$"""
{{Header()}}
namespace RSCG_Wait;
[global::System.CodeDom.Compiler.GeneratedCode("{{name}}", "{{version}}")]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
partial class OptionsFromBuild
{
""";

Add comments for method / classes / properties generated​

Add comments to the generated code in order to help the user understand the code generated

Or if not , add

//pragma warning disable CS1591

to the generated code

Example - click to expand

In the mentioned project src\RSCG_Wait\RSCG_Wait.csproj

context.AddSource("WaitGeneratorStart.g", $$"""
{{Header()}}
namespace RSCG_Wait;
//pragma warning disable CS1591
[global::System.CodeDom.Compiler.GeneratedCode("{{name}}", "{{version}}")]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
partial class MyGeneratedCode

If you're fond of nullable reference types​

Add

#nullable enable

at the start of the file and

#nullable restore

at the end of the file

Details

Example - click to expand In the mentioned project src\RSCG_Wait\RSCG_Wait.csproj

context.AddSource("WaitGeneratorStart.g", $$"""
{{Header()}}
#nullable enable
namespace RSCG_Wait;
//pragma warning disable CS1591
[global::System.CodeDom.Compiler.GeneratedCode("{{name}}", "{{version}}")]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
partial class MyGeneratedCode
{
public static string DateStart => "{{DateTime.Now.ToString()}}";
public static int SecondsToWait={{secondsToWait}};
}
#nullable restore
""");

For deploy​

NuGet package​

The best way to deploy the Roslyn generator is to use a NuGet package.

So you should follow documentation at https://learn.microsoft.com/en-us/nuget/create-packages/package-authoring-best-practices

Read also https://www.meziantou.net/ensuring-best-practices-for-nuget-packages.htm

Details

Example - click to expand In the mentioned project src\RSCG_Wait\RSCG_Wait.csproj

<PropertyGroup>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GX</CompilerGeneratedFilesOutputPath>
</PropertyGroup>

<ItemGroup>
<None Include="../../readme.md" Pack="true" PackagePath="\" />
<None Include="../../docs/imgs/nuget.png" Pack="true" PackagePath="\" />
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
<None Include="../../readme.txt" pack="true" PackagePath="." />
</ItemGroup>
<PropertyGroup>
<Version>2024.2.23.1940</Version>
<Authors>Andrei Ignat</Authors>
<Description>This package wait for a time and put all global options into a cs file</Description>
<Title>RSCG Wait and Options</Title>
<PackageId>RSCG_WaitAndOptions</PackageId>
<PackageTags>C#;.NET;Roslyn;RSCG;Roslyn Source Code Generator;</PackageTags>
<PackageReadmeFile>readme.md</PackageReadmeFile>
<PackageIcon>nuget.png</PackageIcon>
<RepositoryUrl>https://github.com/ignatandrei/RSCG_WaitAndOptions</RepositoryUrl>
<PackageProjectUrl>https://github.com/ignatandrei/RSCG_WaitAndOptions</PackageProjectUrl>
<RepositoryType>GIT</RepositoryType>
<Copyright>MIT</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<IncludeSymbols>true</IncludeSymbols>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<Deterministic>true</Deterministic>
<DebugType>embedded</DebugType>

</PropertyGroup>
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>

Ensure in nuget​

The generator should be packed in analyzer folder in nuget package

image

Details

Example - click to expand In the mentioned project src\RSCG_Wait\RSCG_Wait.csproj

<ItemGroup>
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>

Performance​

For performance, see the following links:

https://andrewlock.net/creating-a-source-generator-part-9-avoiding-performance-pitfalls-in-incremental-generators/

https://www.thinktecture.com/net/roslyn-source-generators-high-level-api-forattributewithmetadataname/

https://papafe.dev/posts/source-generators-tips/