1. Введение
Давайте честно: большинство людей думают о генерации кода примерно так же, как о машинке для нарезки яиц — полезно, но редко жизненно необходимо. Однако современные .NET-проекты становятся всё сложнее, и автоматизация рутинной или шаблонной работы над кодом — это не просто "фишка", а важный инструмент повышения качества и производительности.
Source Generators — это механизм, появившийся начиная с C# 9 и .NET 5. Они, как супергерои на этапе компиляции, могут генерировать C#-код, который потом компилируется как часть вашего проекта. При этом они не вмешиваются в уже скомпилированный ассемблированный код во время выполнения, а лишь расширяют проект новыми исходниками до компиляции.
Ниже будут типовые сценарии использования Source Generators.
Автоматическое создание шаблонного кода
Во многих проектах приходится создавать однотипный код: конструкторы, методы ToString, сериализаторы, property change notification (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 — предупреждение в компиляторе!". Так работают многие линтеры и analyzers, но генератор может добавить свои сообщения или даже подсунуть готовый код-заглушку.
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(Ваш Source Code) -->|Компилятор вызывает| B(Source Generator)
B -->|Добавленный Code| C(Новые .cs-файлы)
C --> D(Компиляция проекта)
- Сначала компилятор (Roslyn) анализирует ваши исходники.
- Потом вызывает "ваш" генератор (реализующий ISourceGenerator).
- Генератор добавляет новые .cs-файлы, которые становятся частью общего дерева компиляции.
- В итоге сборка содержит и ваш, и сгенерированный код.
Специализированные задачи: что ещё можно генерировать
- Биндинги для нативных библиотек (обёртки под C-код или WinAPI).
- AOP: автологирование вызовов, аспекты (в духе Fody, PostSharp и др.).
- Описание API для автогенераторов документации.
- Обработка внешних ресурсов: SVG, SQL, Razor — генератор создаёт strongly-typed классы для типобезопасного доступа.
3. Динамическая генерация кода с помощью System.Reflection.Emit
Если Source Generators генерируют C#-код до сборки, то System.Reflection.Emit — инструмент для магии во время исполнения. Ваша программа может сама создавать новые типы, методы и даже сборки — прямо на лету!
Звучит пугающе? Немного. Но иногда без этого не обойтись: например, если вы пишете фреймворки для динамического проксирования (AOP, профилирование, мокинг), генераторы сериализаторов под runtime-данные, динамические 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, спец. runtime-пайплайны |
| Сложность использования | Средняя, нужны знания Roslyn | Высокая, нужны знания IL |
| Поддержка IDE и отладки | Отличная (видно исходники) | Сложная |
| Производительность | Очень высокая | Может быть высокой |
5. Практические сценарии
1. Генерация strongly-typed API
Организация предоставляет OpenAPI-спецификацию. Генератор анализирует её и создаёт классы контроллеров, DTO и клиентский код для работы с REST API — типобезопасно и с поддержкой IntelliSense.
Код (псевдо):
// Spec: GET /users -> returns User[]
// Source Generator сгенерирует:
public class ApiClient
{
public Task<User[]> GetUsersAsync() { ... }
}
2. Автоматический injection/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 могут сгенерировать код сериализации/десериализации заранее — максимально быстро и без накладных расходов.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ