JavaRush /Курси /C# SELF /Source Generators і ...

Source Generators і System.Reflection.Emit

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

1. Вступ

Чесно кажучи, більшість людей сприймає генерацію коду приблизно так само, як яйцерізку — річ корисна, але рідко справді необхідна. Проте сучасні .NET‑проєкти стають дедалі складнішими, і автоматизація рутинної та шаблонної роботи з кодом — це не просто «фіча», а важливий інструмент підвищення якості й продуктивності.

Source Generators — це механізм, що зʼявився, починаючи з C# 9 і .NET 5. Вони, мов «супергерої» етапу компіляції, можуть генерувати C#‑код, який потім компілюється як частина вашого проєкту. Під час виконання вони в уже скомпільований код не втручаються; генератори лише доповнюють проєкт новими вихідними файлами до компіляції.

Нижче — типові сценарії використання Source Generators.

Автоматичне створення шаблонного коду

У багатьох проєктах доводиться писати однотипний код: конструктори, методи ToString, серіалізатори, сповіщення про зміну властивостей (INotifyPropertyChanged) тощо. Source Generators можуть генерувати цей код автоматично, звільняючи розробників від рутини та ризику друкарських помилок.

Приклад: Генератор ToString

Припустімо, ви розробляєте DTO‑класи (Data Transfer Object) для передавання даних. Вам потрібно реалізувати для кожного класу змістовний ToString, у якому буде перелічено всі властивості.

Замість ручного копіювання можна створити Source Generator, який для кожного класу з атрибутом [AutoToString] згенерує реалізацію методу ToString. Так роблять, наприклад, бібліотеки AutoToString.


// Ваш клас з атрибутом
[AutoToString] 
public partial class Person 
{
    public string Name { get; set; }
    public int Age { get; set; }
}

// Source Generator згенерує (спрощено):
public partial class Person
{
    public override string ToString() => $"Person: Name={Name}, Age={Age}";
}

Плюси: код читабельний і завжди оновлений під час додавання нових властивостей.

Спрощення серіалізації та десеріалізації

Source Generators активно використовуються у складі стандартної бібліотеки System.Text.Json для генерації швидкого коду серіалізації та десеріалізації. До появи генераторів серіалізація часто вимагала рефлексії (що дорого за продуктивністю), а тепер згенерований код робить усе швидко й безпечно.

Що отримує розробник? Додаєте [JsonSerializable(typeof(MyType))] — і генератор створює продуктивний код серіалізації для цього типу.

Генерація коду для конфігурацій, маперів, DI‑контейнерів

  • Конфігурації: генератори автоматично створюють класи конфігурації на основі JSON‑файлів.
  • Мапери: наприклад, проєкт Mapster за допомогою генераторів створює мапінг між типами без ручного копіювання полів.
  • Dependency Injection: деякі контейнери (наприклад, StrongInject) використовують генератори для коду реєстрації сервісів.

Інтеграція зі сторонньою інфраструктурою

Деякі генератори аналізують зовнішні ресурси (опис API, GraphQL‑схеми, Thrift‑протоколи тощо) і генерують C#‑класи для роботи з ними. Це звільняє від ручного оновлення коду за зміни контрактів.

Перевірка й діагностика коду на етапі компіляції

Source Generators можна використовувати й для перевірки коду: «Якщо в проєкті трапляється метод X, але не виконано умову Y — попередження від компілятора!». Так працює багато лінтерів і аналізаторів, а генератор може додати свої повідомлення або навіть підкласти готовий код‑заглушку.

2. Власний Source Generator для автоматичної серіалізації

Розглянемо простий приклад напряму: генератор, який для класу з атрибутом [AutoJson] генерує метод для серіалізації у формат JSON.

Код нижче — лише ілюстрація; у реальному житті використовуйте System.Text.Json.SourceGeneration.


// Пишемо вручну:
[AutoJson]
public partial class Book
{
    public string Title { get; set; }
    public int Year { get; set; }
}

// Генератор додасть:
public partial class Book
{
    public string ToJson() => $"{{ \"Title\": \"{Title}\", \"Year\": {Year} }}";
}

Схема взаємодії Source Generator і проєкту

flowchart TD
    A(Ваш вихідний код) -->|Компілятор викликає| B(Source Generator)
    B -->|Доданий код| C(Нові .cs-файли)
    C --> D(Компіляція проєкту)
  • Спершу компілятор (Roslyn) аналізує ваші вихідники.
  • Потім викликає «ваш» генератор (що реалізує ISourceGenerator).
  • Генератор додає нові .cs‑файли, які стають частиною спільного дерева компіляції.
  • У підсумку збірка містить і ваш, і згенерований код.

Спеціалізовані завдання: що ще можна генерувати

  • Звʼязування для нативних бібліотек (обгортки над C‑кодом або WinAPI).
  • AOP: автоматичне логування викликів, аспекти (у дусі Fody, PostSharp тощо).
  • Опис API для автогенераторів документації.
  • Обробка зовнішніх ресурсів: SVG, SQL, Razor — генератор створює суворо типізовані класи для типобезпечного доступу.

3. Динамічна генерація коду за допомогою System.Reflection.Emit

Якщо Source Generators генерують C#‑код до збірки, то System.Reflection.Emit — інструмент для роботи під час виконання. Ваша програма може сама створювати нові типи, методи й навіть збірки — просто на льоту.

Звучить лячно? Трохи. Та інколи без цього ніяк: наприклад, якщо ви створюєте фреймворки для динамічного проксування (AOP, профілювання, мокінг), генератори серіалізаторів під дані під час виконання, динамічні ORM тощо.

Коли потрібен Reflection.Emit

  • Типи заздалегідь невідомі (користувач задає структуру на льоту).
  • Динамічне проксування (обгортки для перехоплення викликів).
  • Високопродуктивна серіалізація (наприклад, protobuf-net).
  • Плагіни та скриптові рушії зі складними сценаріями завантаження.

Що можна створювати через Reflection.Emit

  • AssemblyBuilder — створення нової збірки.
  • ModuleBuilder — модуль у збірці.
  • TypeBuilder — опис нового типу.
  • MethodBuilder — метод з IL‑кодом.
  • PropertyBuilder, FieldBuilder, EventBuilder — властивості, поля, події.

Міні‑приклад: збирання нового класу на льоту

using System;
using System.Reflection;
using System.Reflection.Emit;

public static class DynamicTypeGenerator
{
    public static Type GenerateSimpleType(string typeName)
    {
        // 1. Створюємо збірку і модуль
        var assemblyName = new AssemblyName("DynamicAssembly");
        var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
        var moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");

        // 2. Створюємо новий клас
        var typeBuilder = moduleBuilder.DefineType(
            typeName, 
            TypeAttributes.Public | TypeAttributes.Class
        );

        // 3. Додаємо публічну рядкову властивість Title
        var field = typeBuilder.DefineField("_title", typeof(string), FieldAttributes.Private);
        var prop = typeBuilder.DefineProperty("Title", PropertyAttributes.HasDefault, typeof(string), null);
        var getMethod = typeBuilder.DefineMethod("get_Title", MethodAttributes.Public, typeof(string), Type.EmptyTypes);
        var il = getMethod.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);      // this
        il.Emit(OpCodes.Ldfld, field); // _title
        il.Emit(OpCodes.Ret);
        prop.SetGetMethod(getMethod);

        var setMethod = typeBuilder.DefineMethod("set_Title", MethodAttributes.Public, null, new[] { typeof(string) });
        il = setMethod.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Ldarg_1);
        il.Emit(OpCodes.Stfld, field);
        il.Emit(OpCodes.Ret);
        prop.SetSetMethod(setMethod);

        // 4. Готово! Створюємо Type
        return typeBuilder.CreateTypeInfo();
    }
}

Тепер можна використовувати цей тип як звичайний C#‑обʼєкт, наприклад, через рефлексію:

var dynamicType = DynamicTypeGenerator.GenerateSimpleType("Book");
var obj = Activator.CreateInstance(dynamicType);
dynamicType.GetProperty("Title").SetValue(obj, "C# у прикладах");
Console.WriteLine(dynamicType.GetProperty("Title").GetValue(obj)); // C# у прикладах

Так народжуються ORM, проксі, серіалізатори, профайлери та навіть деякі тестові фреймворки.

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

Source Generators vs Reflection.Emit: хто кого?

Source Generators працюють на етапі компіляції: роблять ваш вихідний код розумнішим, а результат потрапляє у скомпільовану збірку. Їх не можна використовувати для генерації коду на підставі даних, отриманих уже під час виконання програми.

Reflection.Emit працює під час виконання: дає змогу динамічно створювати збірки, типи й методи, але такий код складніше налагоджувати й підтримувати.

Коли що використовувати?

Аналогія з життя:

  • Source Generators — як фабрика, що випускає готові деталі до збирання машини.
  • Reflection.Emit — як інженер, що на льоту приварює до машини ракетний двигун просто під час поїздки.

Особливості та «підводні камені»

  • Згенерований код може бути складним для налагодження. У генераторів часто є можливість зберігати вихідники на диск — шукайте їх у папці obj\Generated.
  • Reflection.Emit створює збірки в памʼяті, які не вивантажуються з AppDomain. Використовуйте AssemblyBuilderAccess.RunAndCollect для тимчасових збірок (якщо підтримується).
  • Не зловживайте Reflection.Emit для простих завдань — інколи генератор вихідників або звичайний шаблон коду простіший і надійніший.
  • Генератори вимагають розуміння Roslyn (для Source Generators) та IL (для Reflection.Emit).

Source Generators і Reflection.Emit

Критерій Source Generators Reflection.Emit
Використовується На етапі компіляції Під час виконання
Результат C#‑вихідники, частина збірки IL‑код, нові типи/збірки
Типові сценарії Автогенерація шаблонного коду, DI, мапінг, серіалізація Проксі, динамічні ORM, спеціальні конвеєри під час виконання
Складність використання Середня, потрібні знання Roslyn Висока, потрібні знання IL
Підтримка IDE й налагодження Відмінна (видно вихідники) Складна
Продуктивність Дуже висока Може бути високою

5. Практичні сценарії

1. Генерація суворо типізованого API

Організація надає OpenAPI‑специфікацію. Генератор аналізує її й створює класи контролерів, DTO та клієнтський код для роботи з REST API — типобезпечно й із підтримкою IntelliSense.

Код (псевдо):
// Spec: GET /users -> returns User[]
// Source Generator згенерує:
public class ApiClient
{
    public Task<User[]> GetUsersAsync() { ... }
}

2. Автоматичне впровадження залежностей/DI‑контейнер (Compile‑time IoC)

Генератори створюють білдери для реєстрації залежностей і побудови графа обʼєктів. Не потрібно вручну писати services.AddSingleton<IMyService, MyService>().

Код (псевдо):
[Injectable]
public class MyService : IMyService { ... }

// Source Generator згенерує:
partial class DIContainer
{
    public void RegisterServices()
    {
        AddSingleton<IMyService, MyService>();
    }
}

3. Динамічні проксі — приклад на Reflection.Emit

Бібліотека Castle DynamicProxy створює проксі‑типи, що перехоплюють виклики методів — основа для AOP, логування, трейсингу, мокінгу.

Код (спрощено):
public interface IBookService { string GetBook(); }
public class BookService : IBookService { public string GetBook() => "C#"; }

var proxy = ProxyGenerator.CreateProxy<IBookService>(new BookService(), interceptor);
proxy.GetBook(); // Виклик перехоплено, можна логувати/змінювати результат

4. Швидка серіалізація без рефлексії

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

1
Опитування
Рефлексія, рівень 63, лекція 4
Недоступний
Рефлексія
Рефлексія та динамічні типи
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ