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 можуть згенерувати код серіалізації/десеріалізації заздалегідь — максимально швидко й без накладних витрат.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ