C#SeniorTechnical

Что такое source generators в C# и какие проблемы они решают?

Source generators — Roslyn-компоненты, генерирующие C#-код во время компиляции. Устраняют runtime-reflection в сериализаторах, DI-контейнерах и маппинге, давая нулевой overhead в production.

Source Generators в C#

Source generators — это компоненты компилятора Roslyn, которые запускаются во время сборки, анализируют синтаксическое дерево исходного кода и добавляют новые файлы .cs в компиляцию. Сгенерированный код становится частью сборки — без reflection, без emit, без overhead во время выполнения.

Какие проблемы решают

  • Производительность сериализацииSystem.Text.Json source generation вместо runtime reflection.
  • DI-регистрация — автосканирование типов без Assembly.GetTypes() при старте.
  • Маппинг объектов — Mapperly генерирует typed маппер вместо AutoMapper с reflection.
  • ЛогированиеLoggerMessage.Define source gen для zero-allocation логов.
  • NativeAOT-совместимость — reflection недоступно или обрезается триммером; source gen даёт статический код.

System.Text.Json Source Generation

// 1. Определяем модель
public record OrderDto(int Id, string Product, decimal Price);

// 2. Создаём JsonSerializerContext с атрибутом
[JsonSerializable(typeof(OrderDto))]
[JsonSerializable(typeof(List<OrderDto>))]
public partial class AppJsonContext : JsonSerializerContext { }

// 3. Используем при сериализации
var order = new OrderDto(1, "Laptop", 999.99m);
string json = JsonSerializer.Serialize(order, AppJsonContext.Default.OrderDto);

// Регистрация в ASP.NET Core
builder.Services.ConfigureHttpJsonOptions(opts =>
    opts.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default));

Structured Logging с LoggerMessage Source Gen

public partial class OrderService
{
    private readonly ILogger<OrderService> _logger;

    public OrderService(ILogger<OrderService> logger) => _logger = logger;

    // Компилятор генерирует zero-allocation метод
    [LoggerMessage(Level = LogLevel.Information, Message = "Order {OrderId} created by {UserId}")]
    private partial void LogOrderCreated(int orderId, string userId);

    public void CreateOrder(int id, string userId)
    {
        // ...
        LogOrderCreated(id, userId); // нет boxing, нет аллокаций
    }
}

Написание собственного Source Generator

[Generator]
public class HelloGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        // Ищем классы с атрибутом [GenerateGreeting]
        var classes = context.SyntaxProvider
            .ForAttributeWithMetadataName(
                "GenerateGreetingAttribute",
                predicate: (node, _) => node is ClassDeclarationSyntax,
                transform: (ctx, _) => (INamedTypeSymbol)ctx.TargetSymbol)
            .Where(t => t is not null);

        context.RegisterSourceOutput(classes, (spc, type) =>
        {
            var source = $$"""
                namespace {{type.ContainingNamespace}};
                partial class {{type.Name}}
                {
                    public string Greet() => "Hello from {{type.Name}}!";
                }
                """;
            spc.AddSource($"{type.Name}.g.cs", source);
        });
    }
}

Подводные камни

  • Source generators работают только в IIncrementalGenerator (v2) — старый ISourceGenerator медленнее и устарел.
  • Генератор не может изменять существующий код — только добавлять новые файлы; для патчинга используйте partial классы/методы.
  • Ошибки в генераторе приводят к непонятным ошибкам компиляции — всегда добавляйте диагностические дескрипторы (DiagnosticDescriptor).
  • Горячая перезагрузка в Rider/VS может работать нестабильно с генераторами — иногда требуется полный rebuild.
  • Сгенерированные файлы не отображаются в проекте по умолчанию — смотри obj/Generated/ или включи <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> в .csproj.
  • Тестирование генераторов требует отдельного проекта с Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing.
  • Зависимости генератора (AnalyzerReference) не должны тянуть тяжёлые пакеты — это замедляет компиляцию.

Common mistakes

  • Путать source generators и генерация кода на этапе компиляции с похожим механизмом из другой версии или платформы.
  • Игнорировать runtime-границы C#: lifecycle, DI scope, SQL translation, UI thread или platform API.
  • Не обсуждать null/empty/error cases и поведение под нагрузкой.

What the interviewer is testing

  • Кандидат объясняет source generators и генерация кода на этапе компиляции на конкретном примере, а не только определением.
  • Указывает последствия для производительности, тестируемости и поддержки.
  • Различает документированное поведение текущего стека и устаревшие практики.

Sources

Related topics