Using C# source code generators to automate development tasks

alisson_podgurski

Alisson Podgurski

Posted on July 21, 2024

Using C# source code generators to automate development tasks

Source Generators are a powerful feature introduced in C# 9 and .NET 5 that allow you to inspect user code and generate new C# code files during compilation. They are particularly useful for automating repetitive tasks and improving development efficiency. In this article, we'll explore how to create a Source Generator that automatically generates enumerators (enums) and integrate it into a .NET console project.

What are Source Generators?

Source Generators are components that allow you to add code to your project during the compilation phase. They are run by the compiler and can generate new C# code files based on analysis of existing code. This can include boilerplate code generation, automatic validations, and more. The main advantage of Source Generators is that they help reduce the amount of repetitive code that developers need to write and maintain.

Benefits of Source Generators

  • Boilerplate Code Automation: Source Generators can automatically create the necessary repetitive code, such as properties, methods, or even entire classes, based on defined patterns. Less time to write repetitive code, more time to drink that special coffee.

  • Performance Improvement: As the code is generated at compile time, there is no overhead at run time, which can result in improved performance. Your code might even win an efficiency award.

  • Easier Maintenance: Reducing manual code reduces the chance of errors and makes code maintenance easier. Less bugs, more smiles.

  • Flexibility: They allow you to define consistent standards and practices across the entire codebase, ensuring a cleaner, more efficient architecture. Who doesn't like a well-organized codebase?

Practical Example: Enum Generator

In this example, we will create two projects:

  1. EnumGenerator - The code generator project that creates enums.
  2. EnumGeneratorDemoApp - The main project that consumes the generated enums.

Project Structure
The directory structure will be as follows:

EnumGeneratorDemo/
├── EnumGenerator/
│ ├── EnumGenerator.csproj
│ ├── EnumGenerator.cs
│ └── obj/
├── EnumGeneratorDemoApp/
│ ├── EnumGeneratorDemoApp.csproj
│ ├── Program.cs
│ └── obj/
└── EnumGeneratorDemo.sln

Step-by-Step Creation and Configuration

1. Creation of the Directory Structure

Create a directory for the project:

mkdir EnumGeneratorDemo
cd EnumGeneratorDemo
Enter fullscreen mode Exit fullscreen mode

Create directories for the code generator and application:

mkdir EnumGenerator
mkdir EnumGeneratorDemoApp
Enter fullscreen mode Exit fullscreen mode

2. Creation of the EnumGenerator Project

Navigate to the EnumGenerator directory and create a class library project:

cd EnumGenerator
dotnet new classlib -n EnumGenerator

Add the required dependencies in the EnumGenerator.csproj file:

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

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" />
  </ItemGroup>

</Project>
Enter fullscreen mode Exit fullscreen mode

Create the EnumGenerator.cs file with the following content:

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Linq;
using System.Text;

namespace EnumGenerator
{
    [Generator]
    public class EnumGenerator : ISourceGenerator
    {
        public void Initialize(GeneratorInitializationContext context)
        {
            System.Diagnostics.Debug.WriteLine("EnumGenerator initialized");
        }

        public void Execute(GeneratorExecutionContext context)
        {
            System.Diagnostics.Debug.WriteLine("EnumGenerator is running!");

            var enums = new[]
            {
                new { Name = "OrderStatus", Values = new[] { "Pending", "Processing", "Shipped", "Delivered", "Cancelled" } },
                new { Name = "UserRole", Values = new[] { "Admin", "User", "Guest" } }
            };

            foreach (var enumDef in enums)
            {
                var source = GenerateEnumSource(enumDef.Name, enumDef.Values);
                context.AddSource($"{enumDef.Name}.g.cs", SourceText.From(source, Encoding.UTF8));
                System.Diagnostics.Debug.WriteLine($"Generated {enumDef.Name}.g.cs");
            }
        }

        private string GenerateEnumSource(string enumName, string[] values)
        {
            var enumValues = string.Join(",\n    ", values);
            return $@"
namespace EnumGeneratorDemoApp.Enums
{{
    public enum {enumName}
    {{
        {enumValues}
    }}
}}
";
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Creation of the EnumGeneratorDemoApp Project

Navigate to the EnumGeneratorDemoApp directory and create a console application project:

cd ../EnumGeneratorDemoApp
dotnet new console -n EnumGeneratorDemoApp
Enter fullscreen mode Exit fullscreen mode

Add the reference to the EnumGenerator project in the EnumGeneratorDemoApp.csproj file:

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

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

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

</Project>
Enter fullscreen mode Exit fullscreen mode

Create the Program.cs file with the following content:

using EnumGeneratorDemoApp.Enums;
using System;

namespace EnumGeneratorDemoApp
{
    class Program
    {
        static void Main(string[] args)
        {
            var order = new Order
            {
                Id = Guid.NewGuid(),
                Status = OrderStatus.Pending
            };

            var user = new User
            {
                Id = Guid.NewGuid(),
                Role = UserRole.Admin
            };

            Console.WriteLine($"Order ID: {order.Id}, Status: {order.Status}");
            Console.WriteLine($"User ID: {user.Id}, Role: {user.Role}");
        }
    }

    public class Order
    {
        public Guid Id { get; set; }
        public OrderStatus Status { get; set; }
    }

    public class User
    {
        public Guid Id { get; set; }
        public UserRole Role { get; set; }
    }
}
Enter fullscreen mode Exit fullscreen mode

4. Solution Creation and Adding Projects

Navigate back to the EnumGeneratorDemo directory and create a solution:

cd ..
dotnet new sln -n EnumGeneratorDemo
dotnet sln add EnumGenerator/EnumGenerator.csproj
dotnet sln add EnumGeneratorDemoApp/EnumGeneratorDemoApp.csproj

Enter fullscreen mode Exit fullscreen mode

5. Compilation and Execution

Compile the EnumGenerator project first:

cd EnumGenerator
dotnet build
Enter fullscreen mode Exit fullscreen mode

Build the EnumGeneratorDemoApp main project:

cd ../EnumGeneratorDemoApp
dotnet build
Enter fullscreen mode Exit fullscreen mode

Run the main project:

dotnet run
Enter fullscreen mode Exit fullscreen mode

6. Verification of Generated Files

Manually check if the .g.cs files were generated in the obj folder of the EnumGeneratorDemoApp project:

cd EnumGeneratorDemoApp/obj/Debug/net8.0/generated/EnumGeneratorDemoApp/
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this article, we explore how Source Generators can revolutionize the way you write and maintain code in C#. With our practical example, we saw how to create and configure an enum generator that automates the creation of enumerators during compilation, reducing the need to write repetitive code manually.

Source Generators offer a series of benefits, from improving performance to ease of maintenance and code standardization. By automating repetitive and tedious tasks, you free up more time to focus on the really important and creative parts of software development.

Additionally, the flexibility of Source Generators allows you to implement consistent standards and practices across your entire codebase, promoting a cleaner, more efficient architecture. Imagine a world where you don't have to worry about generating boilerplate code and can focus on developing innovative and robust solutions.

To complement your exploration of Source Generators, I recommend visiting the amis92/csharp-source-generators repository, which contains several examples and libraries that can help with development.

If you haven't tried Source Generators yet, now is the time. They not only increase your productivity but also improve the quality of your code. And as we've seen, setup and usage is simple and straightforward, especially with the power of .NET tools and libraries.

To check out the full example code discussed in this article, visit my Alisson Podgurski GitHub. I'll add the example further so you can explore and test it on your own.

So explore Source Generators and see how they can transform your workflow. Your codebase and your sanity will thank you!

💖 💪 🙅 🚩
alisson_podgurski
Alisson Podgurski

Posted on July 21, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related