JavaRush /Курси /C# SELF /Вступ до Source Generators...

Вступ до Source Generators

C# SELF
Рівень 63 , Лекція 3
Відкрита

1. Вступ

Коли ви пишете код, напевно час від часу ловите себе на думці: «Чому я знову й знову копіюю один і той самий шаблон коду для різних класів?» Або: «Чому серіалізація, логування, мапінг даних — це стільки однотипних рядків?» Іноді хочеться, щоб хтось (або щось) написав цей трудомісткий шаблон за вас.

І тут на сцену виходять Source Generators — нова можливість у C#, що з’явилася в .NET 5 і досі активно розвивається. Source Generator — це бібліотека, яка запускається на етапі компіляції та може динамічно генерувати C#‑код, який автоматично вбудовується у ваш проєкт до фінальної збірки.

Навіщо це потрібно?

  • Автоматизація рутини: звільняє від написання однотипних класів/методів (boilerplate).
  • Безпека, яку перевіряє компілятор: згенерований код компілюється разом із вашим (на відміну від T4 або рефлексії).
  • Висока продуктивність: серіалізація, DI, мапінг тощо без витрат на рефлексію під час виконання програми.
  • Підтримка сучасних шаблонів: реалізація підходів, які складно або дорого було реалізувати без генерації коду.

Як Source Generators працюють «під капотом»?

Source Generator — це .NET‑бібліотека (зазвичай проєкт типу Class Library), що реалізує інтерфейс ISourceGenerator. Під час компіляції Roslyn запускає всі під’єднані генератори, надаючи їм доступ до синтаксичного дерева вашого коду.

Генератор аналізує ваш код, вирішує, що і де потрібно згенерувати, і створює нові C#‑файли, які компілятор одразу компілює.

Автоматична генерація ToString

Почнімо з простого. Уявімо, що маємо клас із багатьма властивостями, і потрібно реалізувати ToString. Вручну це виглядає так:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public override string ToString()
        => $"Person(Name={Name}, Age={Age})";
}

Та якщо властивостей дуже багато — це стає клопітно, та ще й легко щось пропустити під час оновлення. Source Generator може зробити це за вас!

2. Як створити власний Source Generator?

Створення проєкту

Відкрийте JetBrains Rider або Visual Studio, створіть новий проєкт типу Class Library (.NET Standard) — саме такі проєкти можуть бути генераторами. Далі додайте NuGet‑пакети:

  • Microsoft.CodeAnalysis.CSharp
  • Microsoft.CodeAnalysis.Analyzers

Важливі атрибути

  • [Generator] — вказує, що цей клас є Source Generator.

Найпростіший шаблон генератора

Ось мінімальний робочий приклад:

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

[Generator]
public class HelloWorldGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        // Можна зареєструвати додаткові дії (необов’язково)
    }

    public void Execute(GeneratorExecutionContext context)
    {
        var code = @"
namespace Generated
{
    public static class HelloWorld
    {
        public static string SayHello() => ""Привіт, світе! Я згенерований!"";
    }
}";
        context.AddSource("HelloWorldGenerator", SourceText.From(code, Encoding.UTF8));
    }
}

Цей простий генератор завжди додає статичний клас HelloWorld із методом SayHello під час компіляції.

Як використовувати Source Generators в основному застосунку?

Під’єднайте проєкт‑генератор як NuGet‑пакет або Project Reference у розділі Analyzer (детальніше — офіційна документація).

Згенерований код одразу доступний у вашому проєкті — нічого додатково під’єднувати не потрібно, просто використовуйте:

// Це згенерується автоматично!
using Generated;

Console.WriteLine(HelloWorld.SayHello());

3. Реальний приклад: автоматична генерація ToString

Припустімо, ми хочемо, щоб усі класи, позначені атрибутом [AutoToString], автоматично отримували реалізацію методу ToString. Для цього знадобиться:

  • Створити власний атрибут.
  • Проаналізувати всі класи з цим атрибутом.
  • Для кожного такого класу згенерувати метод ToString.

Атрибут

[AttributeUsage(AttributeTargets.Class)]
public class AutoToStringAttribute : Attribute
{
}

Використання в коді

[AutoToString]
public class Product
{
    public string Name { get; set; }
    public int Price { get; set; }
}

Проста логіка генерації

Генератор шукатиме класи з [AutoToString] і генеруватиме приблизно такий код:

public override string ToString() 
    => $"Product(Name={Name}, Price={Price})";

Фрагмент реального коду генератора

Базова ідея — обхід синтаксичного дерева з Roslyn:

public void Execute(GeneratorExecutionContext context)
{
    // Аналізуємо всі синтаксичні дерева
    foreach (var tree in context.Compilation.SyntaxTrees)
    {
        var root = tree.GetRoot();
        // Шукаємо всі класи з потрібним атрибутом (приблизно!)
        var classes = root.DescendantNodes()
            .OfType<ClassDeclarationSyntax>()
            .Where(c => c.AttributeLists
                         .SelectMany(al => al.Attributes)
                         .Any(a => a.Name.ToString().Contains("AutoToString")));

        foreach (var @class in classes)
        {
            var className = @class.Identifier.Text;
            // Отримуємо всі властивості класу
            var props = @class.Members
                .OfType<PropertyDeclarationSyntax>()
                .Select(p => p.Identifier.Text)
                .ToArray();

            var toStringCode = string.Join(", ", props.Select(p => $"{p}={{this.{p}}}"));
            var generated = $@"
partial class {className}
{{
    public override string ToString() => $""{className}({toStringCode})"";
}}";

            context.AddSource($"{className}_ToString", SourceText.From(generated, Encoding.UTF8));
        }
    }
}

Зверніть увагу: для production‑коду застосовують коректніший аналіз через SemanticModel Roslyn.

4. Корисні нюанси

На що варто звертати увагу

Source Generators не можуть модифікувати наявний вихідний код — лише створювати нові файли (наприклад, додаткові partial-класи, методи тощо). Це означає, що якщо ваш клас оголошено як partial, можна згенерувати для нього додаткові методи або властивості.

Іноді складно коректно аналізувати синтаксис і враховувати всі нюанси мови (вкладені класи, узагальнення, модифікатори тощо). Автор генератора має стежити, щоб згенерований код компілювався й не «ламав» проєкт.

Ще одна пастка — якщо ви генеруєте методи, що реалізують інтерфейс, переконайтеся, що файли під час кожної збірки генеруються наново. Інакше можливі дивні помилки компіляції. Сучасні інструменти зазвичай це вирішують, але пам’ятати про це важливо.

Source Generators vs. рефлексія

Рефлексія: виконується під час роботи програми, ресурсомістка, не перевіряється компілятором, часто повільна на великих обсягах даних.

Source Generator: генерує код на етапі компіляції. Усе статично перевіряється, IDE бачить методи, працює автодоповнення, а продуктивність — як у звичайного C#‑коду.

Практична користь

  • System.Text.Json: генерація серіалізації/десеріалізації без рефлексії.
  • Проєктування DI‑контейнерів: наприклад, Microsoft.Extensions.DependencyInjection із генерацією графа залежностей.
  • Мапери типу Mapster: перехід від рефлексії до compile-time генерації mapping-коду.
  • Тестові фреймворки: автогенерація тестових методів на основі атрибутів.
  • ASP.NET Minimal APIs (починаючи з .NET 7): генерація endpoint‑хендлерів.

Конфігурація, параметри й опції

Генератори можна налаштовувати через параметри MSBuild, додаткові файли та конвенції. Можна, наприклад, генерувати різний ToString залежно від оточення (Debug/Release) або конфігурації застосунку.

Як Source Generators пов’язані з реальними завданнями

Для розробників, які прагнуть чистого й швидкого коду, це потужний інструмент: менше рутини, більше перевірок під час компіляції, підказки IDE та прозорий рефакторинг. Знання генераторів дедалі частіше запитують на співбесідах — від серіалізації та DI до мапінгу.

Життєвий цикл Source Generator

Стадія Що відбувається
1. Проєкт під’єднано Ваш генератор додано як analyzer/reference
2. Roslyn компілює вихідний код Генератор отримує AST (абстрактне синтаксичне дерево)
3. Генератор виконується Додає нові .cs‑файли для компіляції
4. Усе компілюється Згенеровані файли стають частиною вашої збірки
5. Код готовий! Згенеровані методи/класи доступні для виклику

5. Налагодження і типові помилки

Одна з найчастіших помилок новачків під час написання генераторів — забути про ключове слово partial у класі, до якого ви хочете додати код. Якщо не вказати partial, компілятор просто не побачить ваші зміни. Іноді згенерований файл може не підхопитися в IDE до першої повторної збірки — не лякайтеся.

Обережно з іменуванням згенерованих файлів: якщо всім надати однаковий Name, вони перезапишуть одне одного. Порада: у назву файла можна підставляти імʼя класу, для якого генерується код — наприклад, context.AddSource($"{className}_ToString", ...).

Помилка з подвійним імпортом атрибутів — якщо згенерувати клас із атрибутом, який уже є в основному проєкті, виникне конфлікт. Краще виносити потрібні атрибути в спільний проєкт або додавати згенерований код лише за потреби.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ