SourceNa temat Source Generator jest bardzo głośnio. Czy jednak jest to srebrna kula, która jest w stanie zastąpić każde istniejące już rozwiązanie? A co robi w ogóle Source Generator?

Jego zadaniem jest wygenerować kod, czyli piszesz kod, który automatycznie za Ciebie ma wygenerować kolejny kod. Wyobraź sobie, że masz z 1000 klas i chciałbyś do każdej tej klasy dodać implementacje interfejsu.

Zakładając, że każda klasa ma słowo kluczowe "partial class" przy pomocy Source Generator dodanie wszystkich metod z tego interfejsu automatycznie byłoby dziecinie proste.

Source Generators są częścią procesu kompilacji. Wszystkie wygenerowane pliki powstają, gdy wciskać przycisk "Build" w Visual Studio. Source Generators mają dostęp do modelu twojego kodu, ale nie mogą go modyfikować. Mogą go tylko rozszerzać.

Introducing C# Source Generators

Source Generators istnieje od .NET 5. Wczoraj chciałem zrobić szybkie demo, aby porównać te rozwiązanie do :

  • T4
  • PostSharp
  • CodeDom
  • Fody
  • ILGenerator

Mój pierwszy problem z aplikacją Demo polegał na tym, że gdy ściągnąłem pierwszy lepszy projekt Source Generator z tej kolekcji projektów :

amis92/csharp-source-generators: A list of C# Source Generators (not necessarily awesome) and associated resources: articles, talks, demos. (github.com)

...to zaraz moje Visual Studio eksplodowało.

Miałem też problem ze znalezieniem jakiegokolwiek wpisu na czyimś blogu czy to po polsku, czy po angielsku, który pokazał mi najbardziej minimalne rozwiązanie, aby zobaczyć, jak to działa.

Pliki projektu Visual Studio Source Generators

Na start potrzebujesz dwóch projektów. Pierwszy projekt będzie zawierał nasz generator, a drugi będzie z niego korzystał.

Projekt generatora musi być napisany w ".NET Standard 2". Trzeba także zainstalować dwie paczki NuGet : "Microsoft.CodeAnalysis.Analyzers" i "Microsoft.CodeAnalysis.CSharp"

<Project Sdk="Microsoft.NET.Sdk">
    
    <PropertyGroup>
        <TargetFramework>netstandard2.0</TargetFramework>
        <IsRoslynComponent>true</IsRoslynComponent>
    </PropertyGroup>
    <ItemGroup>
        <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.2" PrivateAssets="all" />
        <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.11.0" />
    </ItemGroup>
    <ItemGroup>
      <Folder Include="Properties" />
    </ItemGroup>
    <ItemGroup>
      <None Remove="Properties\launchSettings.json" />
    </ItemGroup>
</Project>

Warto także do projektu, który będzie korzystać z naszego generatora dodać "CompilerGeneratedFilesOutputPath" i "EmitCompilerGeneratedFiles" . W ten sposób będziesz mógł zobaczyć wygenerowane pliki.

Inaczej one ląduje w folderze TEMP :"C:\Users<user-name>\AppData\Local\Temp\VSGeneratedDocuments".

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
  </PropertyGroup>
    <PropertyGroup>
        <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
        <CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GeneratedFiles</CompilerGeneratedFilesOutputPath>
    </PropertyGroup>
  <ItemGroup>
    <ProjectReference OutputItemType="Analyzer" ReferenceOutputAssembly="false" Include="..\HelloWorldGenerator\HelloWorldGenerator.csproj" />
  </ItemGroup>
  <ItemGroup>
    <Folder Include="obj\" />
  </ItemGroup>
</Project>

Wygenerowane pliki w Visual Studio będą się znajdować w specjalnej zakładce "Analyzers"

Projekt hello world

Teraz gdy mamy już ustawienia tego projektu za sobą to możemy napisać najprostszy generator kodu.

[Generator]
public class HelloWorldGenerator : ISourceGenerator
{
    public void Execute(GeneratorExecutionContext context)
    {
        //Debugger.Launch();

        context.AddSource(
            "HelloWorldProvider",
            @"public class HelloWorldCezary
            {
                public static string HelloWorld()
                {
                    return ""Hello World!!"";
                }
            }");
    }

    public void Initialize(GeneratorInitializationContext context)
    {

    }
}

Klasa generująca musi implementować interface : ISourceGenerator. W metodzie Execute będziemy mogli dodać nasz wygenerowany kod. Kod można wygenerować kod na dwa sposoby:

  • Wysłać napis string z wygenerowany kodem
    • Jeżeli formatowanie kodu jest ważne to ten sposób może Cię kopnąć
    • Łatwo napisać słowa kluczowe w złych miejscach
    • Łatwo o proste błędy
  • Skorzystać z klas pomocniczych jak "Microsoft.CodeAnalysis.CSharp.SyntaxFactory"
    • Nie widać dokładnie co tworzysz. Musi spojrzeć do wygenerowanego pliku
    • To wciąż Cię nie ratuje od błędów
    • Musi użyć więcej metod, nawet gdy tworzysz banalny kod

My robimy to najłatwiej więc tworzymy klasę poprzez goły napis. Jakbyś się zastanawiał, o co chodzi za komentowanym "Debugger.Lanuch()" to odpowiedź sobie na pytanie, jak twój generator debugować, gdy jego wykonuje się on w trakcie przyciśnięcia guzika "Build".

Umieszczając kod "Debugger.Lanuch()"  sprawisz, że proces debugowania uruchomi się przy następnym procesie budowania kodu. Wtedy wyskoczy Ci takie okno "Just in Time Debbuger" i będziesz mógł analizować co twój generator robi

Okno debugera właśnie na czas

Teraz gdy mamy już napisaliśmy generator to możemy z niego skorzystać w drugim projekcie.

using System;

var helloWorld = HelloWorldCezary.HelloWorld();           
Console.WriteLine(helloWorld);

Kod możesz uruchomić, ale rodzi się pytanie, które ja sobie zadałem. Czy to normalne, że wygenerowany kod jest niewidoczny dla Visual Studio i podkreśla je na czerwono?

Problem z Visual Studio 2022 gdy generuje plik z kodem

Na chwilę obecną nie mam na to rozwiązania. Z jakiegoś powodu restart Visual Studio 2022 magicznie sprawia, że wygenerowane klasy są widoczne przez Intellisense 

Po restarcie Visual Studio 2022 nie ma tego problemu

Z tego, co rozumiem obecne rozwiązanie tymczasowe na ten problem polega na dodaniu wygenerowany plików do projektu, a potem usunięcie tych plików z procesu kompilacji, aby kompilator nie liczył ich podwójnie

<PropertyGroup>
        <IncludePackageReferencesDuringMarkupCompilation>true</IncludePackageReferencesDuringMarkupCompilation>
        <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
        <CompilerGeneratedFilesOutputPath>GeneratedFiles</CompilerGeneratedFilesOutputPath>
    </PropertyGroup>
    <Target Name="RemoveSourceGeneratedFiles" BeforeTargets="BeforeBuild">
        <ItemGroup>
            <Compile Remove="GeneratedFiles\**" />
        </ItemGroup>
    </Target>

Na chwile obecną jest to dla mnie największa wada Source Generators. 

Source Generators wciąż wydają się lepszą alternatywą do różnych rozwiązań, które modyfikują kod IL. Chociaż te modyfikatory kodu IL natomiast potrafią modyfikować istniejący już kod.

T4 moim zdaniem nadal może zdać egzamin, jeśli chcemy wygenerować kod w małych ilościach. Jak generowanie typów wyliczeniowych na podstawie wartości w bazie danych.

Dlaczego Source Generators stały się tak popularne? Można każdy taki generator dodać jako paczkę Nuget. Source Generator rozwiązuje bardzo dobrze typowe problemy meta programowania jak :

  • Debugowanie i logowanie
  • Pokrycie testów i tworzenie samych metod testowych
  • Dodawanie masowo metod pomocniczych i implementacji interfejsów
  • Generowania gotowych klas Query, Command, Repozytorium, Web Rest API na podstawie danego schematu

To wszystko, co musisz wiedzieć, aby zacząć swoją przygodę z Source Generator