CodeBlanks Logo Icon CodeBlanks

Source generator

C# course · Expert · Lesson 46/47

Generate code at compile time with a source generator.

Source generator

In this lesson we add a DTO generator simulation. The static DtoGenerator class produces DTO class definitions from type names and adds them to a list with AddSource. This mimics the core flow of a real source generator.

The generator creates a class definition for each name with the hint name name + ".g.cs". Main calls the generator with three names and prints the hint names.

The DtoGenerator class

The static DtoGenerator class has three members: a List<(string Hint, string Source)> property, an AddSource(string, string) method, and a Generate(string[]) method. Generate adds each name with a .g.cs extension to the list.

This class shows how source generators produce code at compile time. The list preserves insertion order, so the output is deterministic.

Exercise Add the DtoGenerator class

Add a static class DtoGenerator to the project. Give it a public static System.Collections.Generic.List<(string Hint, string Source)> Sources { get; } = new(); property, a public static void AddSource(string hintName, string source) method (with Sources.Add((hintName, source));), and a public static void Generate(string[] typeNames) method. In Generate, loop foreach (var name in typeNames) and call AddSource(name + ".g.cs", $"public class {name}Dto {{ }}"). In Main, call DtoGenerator.Generate(new[] { "Info", "Warn", "Error" }) and print each entry.Hint with foreach (var entry in DtoGenerator.Sources).

Show example solution
DtoGenerator.Generate(new[] { "Info", "Warn", "Error" }); and foreach (var entry in DtoGenerator.Sources) Console.WriteLine(entry.Hint);
Example solution:
using System;
using System.Linq;

static class LogProcessor
{
    public static int ParseLevel(string level)
    {
        return level switch
        {
            "INFO" => 0,
            "WARN" => 1,
            "ERROR" => 2,
            _ => -1
        };
    }

    public static int CountByLevel(string[] logs, int targetLevel)
    {
        int count = 0;
        foreach (var log in logs)
        {
            if (ParseLevel(log.Split(':')[0]) == targetLevel)
            {
                count++;
            }
        }
        return count;
    }

    public static string ParseWithSpan(ReadOnlySpan<char> log)
    {
        int colon = log.IndexOf(':');
        if (colon < 0) return "";
        return log.Slice(0, colon).ToString();
    }

    public static unsafe int UnsafeSum(int a, int b)
    {
        int* pa = &a;
        int* pb = &b;
        return *pa + *pb;
    }

    public static async System.Threading.Tasks.Task Run(string[] logs)
    {
        var channel = System.Threading.Channels.Channel.CreateUnbounded<string>();
        var counts = new System.Collections.Concurrent.ConcurrentDictionary<string, int>();
        foreach (var log in logs)
        {
            await channel.Writer.WriteAsync(log);
        }
        channel.Writer.Complete();
        await foreach (var log in channel.Reader.ReadAllAsync())
        {
            var level = ParseWithSpan(log.AsSpan());
            counts.AddOrUpdate(level, 1, (_, n) => n + 1);
        }
        foreach (var kvp in counts.OrderBy(k => k.Key))
        {
            Console.WriteLine($"{kvp.Key}: {kvp.Value}");
        }
    }
}

static class DtoGenerator
{
    public static System.Collections.Generic.List<(string Hint, string Source)> Sources { get; } = new();

    public static void AddSource(string hintName, string source)
    {
        Sources.Add((hintName, source));
    }

    public static void Generate(string[] typeNames)
    {
        foreach (var name in typeNames)
        {
            AddSource(name + ".g.cs", $"public class {name}Dto {{ }}");
        }
    }
}

class Program
{
    static void Main()
    {
        Console.WriteLine("LogProcessor skeleton ready");
        var logs = new[] { "INFO: a", "WARN: b", "INFO: c", "ERROR: d" };
        Console.WriteLine(LogProcessor.CountByLevel(logs, 0));
        Console.WriteLine(LogProcessor.ParseWithSpan("ERROR: disk".AsSpan()));
        Console.WriteLine(LogProcessor.UnsafeSum(3, 4));
        LogProcessor.Run(logs).Wait();
        DtoGenerator.Generate(new[] { "Info", "Warn", "Error" });
        foreach (var entry in DtoGenerator.Sources)
        {
            Console.WriteLine(entry.Hint);
        }
    }
}

Test yourself

  1. What is the purpose of a source generator?

    • Do runtime reflection
    • Read files
    • Generate code at compile time
    • Write tests
    Show answer

    Generate code at compile time — Correct. A source generator generates code at compile time.

  2. Match each DtoGenerator step

    Generate
    Produce DTO classes
    AddSource
    Add generated code to the list
    .g.cs
    Generated file extension
  3. Complete the AddSource call

    AddSource(name + ____, source);
    Show answer
    AddSource(name + ".g.cs", source);

All lessons in Expert