CodeGym /Kursy /C# SELF /Wprowadzenie do Source Gen...

Wprowadzenie do Source Generators

C# SELF
Poziom 63 , Lekcja 3
Dostępny

1. Wprowadzenie

Kiedy piszesz kod, pewnie czasem myślałeś: „Dlaczego znowu kopiuję ten sam wzorzec kodu dla różnych klas?” Albo: „Dlaczego serializacja, logowanie, mapowanie danych to tyle podobnych linijek?” Czasem chciałoby się, żeby ktoś (albo coś) napisał ten żmudny szablon za Ciebie.

Tutaj wchodzą na scenę Source Generators — nowe możliwości w C#, pojawione się w .NET 5 i dalej aktywnie rozwijane. Source Generator to biblioteka uruchamiana na etapie kompilacji, która może dynamicznie generować kod C#, który jest automatycznie włączany do projektu przed finalnym zbudowaniem.

Po co to?

  • Automatyzacja rutyny: Pozwala uniknąć pisania powtarzalnych klas/metod (boilerplate).
  • Bezpieczeństwo sprawdzane przez kompilator: Generowany kod kompiluje się razem z Twoim (w przeciwieństwie do T4 czy refleksji).
  • Wysoka wydajność: Serializacja, DI, mapowanie itp. bez kosztów refleksji w runtime.
  • Wsparcie nowoczesnych wzorców: Realizacja podejść, które bez generacji kodu byłyby trudne lub kosztowne.

I wask działają „pod maską” Source Generators?

Source Generator to biblioteka .NET (zazwyczaj projekt typu Class Library), implementująca interfejs ISourceGenerator. Podczas kompilacji Roslyn uruchamia wszystkie podłączone generatory, dając im dostęp do drzewa składniowego Twojego kodu.

Generator analizuje Twój kod, decyduje, co i gdzie trzeba wygenerować, i tworzy nowe pliki C#, które kompilator od razu kompiluje.

Automatyczna generacja ToString

Zacznijmy od prostego przykładu. Wyobraźmy sobie, że mamy klasę z wieloma właściwościami i musimy zaimplementować ToString. Ręcznie wygląda to tak:

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

Ale jeśli właściwości robi się dużo — robi się to nudne, a dodatkowo łatwo zapomnieć o aktualizacji. Source Generator może zrobić to za Ciebie!

2. I wask stworzyć własny Source Generator?

Tworzenie projektu

Otwórz JetBrains Rider lub Visual Studio, stwórz nowy projekt typu Class Library (.NET Standard) — to właśnie takie projekty mogą być generatorami. Następnie dodaj pakiety NuGet:

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

Ważne atrybuty

  • [Generator] — oznacza, że ta klasa jest Source Generatorem.

Najprostszy szablon generatora

Oto minimalny działający przykład:

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

[Generator]
public class HelloWorldGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        // Można zarejestrować dodatkowe akcje (opcjonalne)
    }

    public void Execute(GeneratorExecutionContext context)
    {
        var code = @"
namespace Generated
{
    public static class HelloWorld
    {
        public static string SayHello() => ""Hello, world! I was generated!"";
    }
}";
        context.AddSource("HelloWorldGenerator", SourceText.From(code, Encoding.UTF8));
    }
}

Ten prosty generator zawsze dodaje statyczną klasę HelloWorld z metodą SayHello podczas kompilacji.

I wask użyć Source Generators w głównym projekcie?

Podłącz projekt-generator jako pakiet NuGet lub Project Reference w sekcji Analyzer (więcej — oficjalna dokumentacja).

Wygenerowany kod jest od razu dostępny w Twoim projekcie — nie trzeba nic dodatkowo dołączać, po prostu użyj:

// To zostanie wygenerowane automatycznie!
using Generated;

Console.WriteLine(HelloWorld.SayHello());

3. Realny przykład: automatyczna generacja ToString

Załóżmy, że chcemy, by wszystkie klasy oznaczone atrybutem [AutoToString] automatycznie otrzymały implementację metody ToString. Potrzebne będzie:

  • Utworzyć własny atrybut.
  • Przeanalizować wszystkie klasy z tym atrybutem.
  • Dla każdej takiej klasy wygenerować metodę ToString.

Atrybut

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

Użycie w kodzie

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

Prosta logika generacji

Generator będzie szukać klas z [AutoToString] i generować mniej więcej taki kod:

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

Kawałek realnego kodu generatora

Podstawowy pomysł — przejść po drzewie składniowym z Roslyn:

public void Execute(GeneratorExecutionContext context)
{
    // Analizujemy wszystkie drzewa składni
    foreach (var tree in context.Compilation.SyntaxTrees)
    {
        var root = tree.GetRoot();
        // Szukamy wszystkich klas z potrzebnym atrybutem (przykładowo!)
        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;
            // Pobieramy wszystkie właściwości klasy
            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));
        }
    }
}

Zwróć uwagę: dla produkcyjnego kodu stosuje się bardziej poprawną analizę przez SemanticModel Roslyn.

4. Przydatne niuanse

Na co warto zwracać uwagę

Source Generators nie mogą modyfikować istniejącego źródła — mogą tylko tworzyć nowe pliki (np. dodatkowe partial-klasy, metody itp.). To oznacza, że jeśli Twoja klasa jest zadeklarowana jako partial, możesz wygenerować do niej dodatkowe metody lub właściwości.

Czasem trudno poprawnie parsować składnię i uwzględnić wszystkie niuanse języka (klasy zagnieżdżone, generyki, modyfikatory itd.). Autor generatora musi pilnować, żeby wygenerowany kod był kompilowalny i nie „psuł” projektu.

Kolejna pułapka — jeśli generujesz metody implementujące interfejs, upewnij się, że pliki generowane są przy każdym buildzie od nowa. W przeciwnym razie mogą pojawić się dziwne błędy kompilacji. Nowoczesne narzędzia zwykle radzą sobie z tym, ale warto o tym pamiętać.

Source Generators vs. refleksja

Refleksja: działa w runtime, kosztowna zasobowo, nie sprawdzana przez kompilator, często wolna przy dużych ilościach danych.

Source Generator: generuje kod na etapie kompilacji. Wszystko jest statycznie sprawdzane, IDE widzi metody, działa autouzupełnianie, a wydajność jest taka jak zwykły kod C#.

Praktyczne zastosowania

  • System.Text.Json: generacja serializacji/deserializacji bez refleksji.
  • Projektowanie kontenerów DI: np. Microsoft.Extensions.DependencyInjection z generacją grafu zależności.
  • Mapery typu Mapster: przejście od refleksji do generacji kodu mappingu w compile-time.
  • Frameworki testowe: autogeneracja metod testowych na podstawie atrybutów.
  • ASP.NET Minimal APIs (od .NET 7): generacja endpoint-handlerów.

Konfiguracja, parametry i opcje

Generatory można konfigurować przez parametry MSBuild, dodatkowe pliki i konwencje. Można np. generować różne wersje ToString w zależności od środowiska (Debug/Release) lub konfiguracji aplikacji.

I wask Source Generators łączą się z realnymi zadaniami

Dla dewelopera, który dąży do czystego i szybkiego kodu, to świetne narzędzie: mniej rutyny, więcej sprawdzeń w compile-time, podpowiedzi IDE i przejrzysty refaktoring. Znajomość generatorów coraz częściej pojawia się na rozmowach kwalifikacyjnych — od serializacji i DI po mapowanie.

Cykl życia Source Generator

Etap Co się dzieje
1. Projekt podłączony Twój generator dodany jako analyzer/reference
2. Roslyn kompiluje źródła Generator otrzymuje AST (abstrakcyjne drzewo składni)
3. Generator się wykonuje Dodaje nowe .cs-plików do kompilacji
4. Wszystko się kompiluje Wygenerowane pliki stają się częścią Twojego assembly
5. Kod gotowy! Wygenerowane metody/klasy są dostępne do wywołań

5. Debugowanie i typowe błędy

Jednym z najczęstszych błędów początkujących przy pisaniu generatorów jest zapomnienie o słowie kluczowym partial w klasie, do której chcesz dopisać kod. Jeśli nie dodasz partial, kompilator po prostu nie zobaczy Twoich zmian. Czasem wygenerowany plik może nie być widoczny w IDE do pierwszej przebudowy — nie panikuj.

Uważaj na nazewnictwo wygenerowanych plików: jeśli wszystkim dasz tę samą nazwę, będą się nadpisywać. Tip: do nazwy pliku możesz wkładać nazwę klasy, dla której generujesz kod — np. context.AddSource($"{className}_ToString", ...).

Błąd z podwójnym importem atrybutów — jeśli wygenerujesz klasę z atrybutem, który już występuje w głównym projekcie, pojawi się konflikt. Lepiej wyciągnąć potrzebne atrybuty do wspólnego projektu albo generować kod tylko wtedy, gdy jest to konieczne.

2
Zadanie
C# SELF, poziom 63, lekcja 3
Niedostępne
Automatyczne generowanie metody ToString
Automatyczne generowanie metody ToString
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION