Swacblooms🦋

Making the Moves
Menu
  • Home
  • Motivation
  • Education
  • Programming
  • About
  • Contact
  • Privacy Policy
Home
Programming
Incremental Generators In C#
Programming

Incremental Generators In C#

Samson Amaugo April 22, 2024

Hola 👋, In the enchanting realm of generators, there was a distinguished clan known as the Source Generators. Source generators enable developers to analyse the syntax tree of a C# source code by hooking into the compilation process of the targeted C# project and generating some code, based on some defined logic.

I will focus on the newly improved and performant source generator in C# called the Incremental generator. What distinguishes it from the original generator is its support for caching in its execution pipelines. So when the generator sees a node that it has handled before in its pipeline, instead of re-running the same logic, it just returns the same output for that node or input. However, considerations have to be made to make the generator cache-friendly in some cases.

In this article, I will explain how I built a source generator that analyses my code base for classes marked with a custom attribute and a property with a Guid type named Id. Upon these classes, it generates extra classes that can perform CRUD operations on the discovered classes or types. The gif below demonstrates this.

In the gif below, a CRUD(Create, Read, Update and Delete) class called PersonService is generated for the Person’s class.

Creating The Generator

So I created, a class library project targeting the netstandard2.0 platform, and added the NuGet below:

dotnet add package Microsoft.CodeAnalysis.CSharp --version 4.9.2

Next, I created a class implementing the IIncrementalGenerator interface with a Generator attribute, enabling the class to be seen as a generator.

namespace CRUD_Generator
{
    [Generator]
    public class MyGenerator : IIncrementalGenerator
    {
        public void Initialize(IncrementalGeneratorInitializationContext context)
        {
                   
        }
    }
}

I didn’t want to generate the helper CRUD classes for just about any class in my source code. I wanted it to be an opt-in feature. To make this happen, I followed the usual pattern of marking the classes that required analysis or generation with an attribute.

Next, I used the RegisterPostInitializationOutput feature to register a call back that adds the attribute used for marking classes to the source code immediately after initialization of the source generator.

namespace CRUD_Generator
{
    [Generator]
    public class MyGenerator : IIncrementalGenerator
    {
        public const string GenerateCRUDAttribute = """
            namespace CRUD_Generator
            {
                [AttributeUsage(AttributeTargets.Class)]
                public class GenerateCRUDAttribute : Attribute
                {
                    
                }
            }
            """;
        public void Initialize(IncrementalGeneratorInitializationContext context)
        {

            context.RegisterPostInitializationOutput(ctx =>
            {
                ctx.AddSource("GenerateCRUDAttribute.g.cs", GenerateCRUDAttribute);
            });
        }
    }
}

I had to set the LangVersion to “latest” in my “csproj” file which enabled using the raw string literal feature in a project targeting netstandard2.0.

Next, I used the ForAttributeWithMetadataName method of the SyntaxProvider to filter out classes marked with the GenerateCRUDAttribute.

var classesMarkedWithTheGeneratorAttribute = context.SyntaxProvider
.ForAttributeWithMetadataName(
    "CRUD_Generator.GenerateCRUDAttribute",
    (node, _) => node is ClassDeclarationSyntax,
    (ctx, _) => (ClassDeclarationSyntax)ctx.TargetNode
);

Knowing how to filter and retrieve information from your source code requires some knowledge of the Syntax Factory API and the Semantic Model API which you can learn here and here

Generating The CRUD Classes For The Selected Types

To generate the classes for the selected types, I used the RegisterSourceOutput method on the context object, which accepts two parameters: An IncrementalValuesProvider<TSource> type (acts as the data source) and a callback that accepts two parameters: A SourceProductionContext which contains methods which can be used to add my generated code to the compilation and a TSource which gives access to the values in the IncrementalValuesProvider<TSource>.

I had to combine my classesMarkedWithTheGeneratorAttribute with the compilation provider, this creates a tuple in the callback having the ClassDeclarationSyntax on the left and the Compilation on the right.

Inside the callback, I used the semantic model API to retrieve the name of the class but that also can be retrieved using the ClassDeclarationSyntax.

I utilized the DiagnosticDescriptor to incorporate an error description for classes that possess the GenerateCRUD attribute but lack an Id property of the Guid type

Gif describing the display of error description for classes that possess the GenerateCRUD attribute but lack an Id property of the Guid type

Next, I composed my generated CRUD code for the class using raw string literals.

Finally, I added the code to the compilation. The snippet contains the RegisterSourceOutput code

 context.RegisterSourceOutput(classesMarkedWithTheGeneratorAttribute.Combine(context.CompilationProvider), (ctx, classesAndCompilation) =>
 {
     var @class = classesAndCompilation.Item1;
     var compilation = classesAndCompilation.Item2;
     var model = compilation.GetSemanticModel(@class.SyntaxTree);
     var classSymbol = model.GetDeclaredSymbol(@class);
     var className = classSymbol!.Name;
     // or className = @class.Identifier.Text if you don't want to use the semantic model API

     // check if class has a property named "Id" of type Guid
     var check = @class.Members.OfType<PropertyDeclarationSyntax>()
    .Any(p => p.Identifier.Text == "Id" && model.GetTypeInfo(p.Type)
    .Type?.Name == "Guid" && p.Modifiers.Any(m => m.Text == "public"));

     if (!check)
     {
         var descriptor = new DiagnosticDescriptor("CRUD01", "GUID type with the property, Id not found", "GUID type with the property, Id not found", "CRUD", DiagnosticSeverity.Error, true);
         var diagnostic = Diagnostic.Create(descriptor, @class.GetLocation());
         ctx.ReportDiagnostic(diagnostic);
         return;
     }
     var generatedCode = $$"""
     namespace CRUD_Generator
     {
         public class {{className}}Service {
             public Guid Id { get; set;}
             private static List<{{className}}> _data = new List<{{className}}>();
             public {{className}}? Get{{className}}(Guid id) => _data.FirstOrDefault(d => d.Id == id);
             public List<{{className}}> GetAll() => _data;
             public void Add({{className}} {{className.ToLower()}}) => _data.Add({{className.ToLower()}});
             public void Update({{className}} {{className.ToLower()}}) => _data[_data.FindIndex(d => d.Id == {{className.ToLower()}}.Id)] = {{className.ToLower()}};
             public void Delete(Guid id) => _data.Remove(_data.FirstOrDefault(d => d.Id == id));   
         }    
     }       
     """;
     ctx.AddSource($"{className}Service.g.cs", generatedCode);
 });

Connecting The Generator To the Target Project

For the target project to utilize the source generator feature, I had to connect the generator to it in the csproj of the target project.

  <ItemGroup>
    <ProjectReference Include="..\CRUD-Generator\CRUD-Generator.csproj" 
      OutputItemType="Analyzer"
      ReferenceOutputAssembly="false" />
  </ItemGroup>

The OutputItem=Analyzer setting enables the target project to view the referenced project as an analyzer.

The ReferenceOutputAssembly=false setting ensures that the target project doesn’t reference the DLL of the source generator during compilation which can be checked in the bin/debug/{dotnetversion} file.

Debugging The Generator

I did have issues debugging my source generators. When I clicked on the debug button of my target project in Visual Studio, the breakpoints added to the source generator did not work 🥲🥲.

In my bid to find a solution, I finally came across a solution that involved setting up a test framework using a library to load the generators. This worked for me with no issues. But then it wasn’t a straightforward and seamless process. 🥲

Finally, I stumbled across a solution on the net that made my day. The gif below demonstrates the steps I followed to replicate.

And that is how easy it is to debug source generators.

Now, when the generator identifies a class that has the GenerateCRUD attribute and includes an Id property of the Guid type, it generates a corresponding CRUD class for it.

Packaging the project as a Nuget Packet

To package the generator as a Nuget package, I made a few edit to the csproj file of the source generator

<Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
		<TargetFramework>netstandard2.0</TargetFramework>
		<RootNamespace>CRUD_Generator</RootNamespace>
		<LangVersion>Latest</LangVersion>
		<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
		<IsRoslynComponent>true</IsRoslynComponent>
		<Nullable>enable</Nullable>
		<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
		<Description>This generator creates generates the CRUD version of a type</Description>
		<PackageOutputPath>my-nuget</PackageOutputPath>
		<IncludeBuiltOutput>false</IncludeBuiltOutput>
	</PropertyGroup>
	<ItemGroup>
		<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
	</ItemGroup>
	<ItemGroup>
		<None Include="$(OutputPath)\$(AssemblyName).dll" 
          Pack="true" 
          PackagePath="analyzers/dotnet/cs" Visible="false" />
	</ItemGroup>
</Project>

In the configuration above, I had to package the DLL in the “analyzers/dotnet/cs” path for the target or consuming project to detect the Nuget as a generator. When dealing with third-party packages required by the generator, the configuration is different. You can learn about that here.

Finally, after building the project I got the NuGet package in the location set in <PackageOutputPath>my-nuget</PackageOutputPath> configuration.

Thanks for reading through. You can check out the code used in this article on my GitHub via this link.

To find out more about Incremental Generators in C#, check this

Prev Article
Next Article

Related Articles

easyssh
Hola 🖐….. Sometimes you might find it hard to keep …

Easy SSH Via Local DNS

C#: Logging array values to the Console
In this my video I described some tips on how …

C#: Logging Array Values to the Console

About The Author

Samson Amaugo

I am Samson Amaugo. I am a full-stack developer and I specialize in DotNet and the MERN stack.

17 Comments

  1. Message- You got a transfer №OV99. LOG IN >>> https://telegra.ph/Go-to-your-personal-cabinet-08-25?hs=8e96ba1babd69839ffe797e81f4f794c&

    pcn68b

    October 21, 2024
  2. You have received 1 message № 573. Read - https://telegra.ph/Go-to-your-personal-cabinet-08-25?hs=8e96ba1babd69839ffe797e81f4f794c&

    r07cdg

    November 7, 2024
  3. Notification- TRANSACTION 1.82000 BTC. Continue =>> https://telegra.ph/Go-to-your-personal-cabinet-08-25?hs=8e96ba1babd69839ffe797e81f4f794c&

    dwqm9q

    December 10, 2024
  4. Message: Transfer №EP94. RECEIVE > https://telegra.ph/Ticket--6974-01-15?hs=8e96ba1babd69839ffe797e81f4f794c&

    h7zcf7

    January 19, 2025
  5. Notification- Process 0,75716261 BTC. Get => https://telegra.ph/Get-BTC-right-now-01-22?hs=8e96ba1babd69839ffe797e81f4f794c&

    5szwbo

    January 27, 2025
  6. Reminder: Transfer №WF27. GET >>> https://telegra.ph/Get-BTC-right-now-01-22?hs=7f5cdad8a371185bc8b2dc7dfbaa6259&

    7q63ri

    February 1, 2025
  7. We send a transaction from us. Verify =>> https://telegra.ph/Get-BTC-right-now-01-22?hs=8e96ba1babd69839ffe797e81f4f794c&

    r54bdc

    February 7, 2025
  8. You have a transaction from unknown user. Receive =>> https://telegra.ph/Binance-Support-02-18?hs=7f5cdad8a371185bc8b2dc7dfbaa6259&

    srmzxb

    February 18, 2025
  9. You have 1 message # 860329. Open > https://graph.org/GET-BITCOIN-TRANSFER-02-23-2?hs=8e96ba1babd69839ffe797e81f4f794c&

    t0v3kb

    February 28, 2025
  10. Notification- SENDING 0.75284862 BTC. Next >> https://graph.org/GET-BITCOIN-TRANSFER-02-23-2?hs=8e96ba1babd69839ffe797e81f4f794c&

    m3t1gd

    March 3, 2025
  11. You have a email # 141599. Go >>> https://graph.org/GET-BITCOIN-TRANSFER-02-23-2?hs=7f5cdad8a371185bc8b2dc7dfbaa6259&

    6q3rra

    March 15, 2025
  12. Reminder- + 0.7569312 BTC. Get > https://graph.org/GET-BITCOIN-TRANSFER-02-23-2?hs=8e96ba1babd69839ffe797e81f4f794c&

    ywbmu7

    March 16, 2025
  13. Ticket: TRANSACTION 0,75778643 bitcoin. Get >>> https://graph.org/GET-BITCOIN-TRANSFER-02-23-2?hs=8e96ba1babd69839ffe797e81f4f794c&

    d6vljz

    March 19, 2025
  14. Robert, your Bitcoin transfer was successful. https://graph.org/Message--17856-03-25?hs=8e96ba1babd69839ffe797e81f4f794c&

    n9beu8

    March 27, 2025
  15. + 1.685350 BTC.GET - https://graph.org/Message--17856-03-25?hs=7f5cdad8a371185bc8b2dc7dfbaa6259&

    14q48t

    March 30, 2025
  16. + 1.38888 BTC.GET - https://graph.org/Message--17856-03-25?hs=7f5cdad8a371185bc8b2dc7dfbaa6259&

    hb9325

    April 16, 2025
  17. + 1.799415 BTC.NEXT - https://graph.org/Ticket--58146-05-02?hs=7f5cdad8a371185bc8b2dc7dfbaa6259&

    p80hte

    May 8, 2025

Leave a Reply

Cancel reply

Search Site

Recent Posts

  • Running Entity Framework Migrations in an Aspire-Bootstrapped Orleans Project
  • Using XDebug in Laravel Sail (VSCODE)
  • Custom Redis Streams Provider for Orleans
  • Creating a Custom File Storage Provider for Microsoft Orleans
  • Incremental Generators In C#

Categories

  • EDUCATION
  • Motivation
  • Programming
  • Uncategorized

Get more stuff

Subscribe to our mailing list and get interesting stuff and updates to your email inbox.

Thank you for subscribing.

Something went wrong.

we respect your privacy and take protecting it seriously

RSS Swacblooms

  • Running Entity Framework Migrations in an Aspire-Bootstrapped Orleans Project
  • Using XDebug in Laravel Sail (VSCODE)
  • Custom Redis Streams Provider for Orleans
  • Creating a Custom File Storage Provider for Microsoft Orleans
  • Incremental Generators In C#
  • Hot Chocolate Data Loader: A Quick Guide
  • Exploring Policy-Based Authorization in Minimal API with .NET 8
  • Using Cloud Firestore in Blazor WebAssembly
  • Serving A VueJs App from DotNet
  • Dynamic Subscriptions in Hot Chocolate 🔥🍫

Swacblooms🦋

Making the Moves
Copyright © 2025 Swacblooms🦋
Swacblooms - Making the Moves