Generatory źródeł

Ten artykuł zawiera omówienie generatorów źródeł dostarczanych w ramach zestawu SDK .NET Compiler Platform ("Roslyn"). Generatory źródeł umożliwiają deweloperom języka C# sprawdzanie kodu użytkownika podczas kompilowania. Generator może tworzyć nowe pliki źródłowe języka C# na bieżąco dodawane do kompilacji użytkownika. W ten sposób masz kod uruchamiany podczas kompilacji. Sprawdza on program w celu utworzenia dodatkowych plików źródłowych, które są kompilowane wraz z resztą kodu.

Generator źródła to nowy rodzaj składnika, który deweloperzy języka C# mogą napisać, który umożliwia wykonywanie dwóch głównych czynności:

  1. Pobierz obiekt kompilacji reprezentujący cały kod użytkownika, który jest kompilowany. Ten obiekt można sprawdzić i napisać kod, który działa z modelami składni i semantycznymi dla kompilowanego kodu, podobnie jak w przypadku analizatorów.

  2. Generowanie plików źródłowych języka C#, które można dodać do obiektu kompilacji podczas kompilacji. Innymi słowy, można podać dodatkowy kod źródłowy jako dane wejściowe kompilacji podczas kompilowania kodu.

Po połączeniu te dwie rzeczy sprawiają, że generatory źródeł są tak przydatne. Kod użytkownika można sprawdzić za pomocą wszystkich bogatych metadanych kompilatora kompilatora podczas kompilacji. Następnie generator emituje kod języka C# z powrotem do tej samej kompilacji, która jest oparta na przeanalizowanych danych. Jeśli znasz analizatory Roslyn, możesz traktować generatory źródeł jako analizatory, które mogą emitować kod źródłowy języka C#.

Generatory źródeł działają jako faza kompilacji zwizualizowane poniżej:

Grafika opisująca różne części generowania źródła

Generator źródła to zestaw .NET Standard 2.0 ładowany przez kompilator wraz z dowolnymi analizatorami. Można go używać w środowiskach, w których składniki platformy .NET Standard można ładować i uruchamiać.

Ważne

Obecnie jako generatory źródeł mogą być używane tylko zestawy .NET Standard 2.0.

Typowe scenariusze

Istnieją trzy ogólne podejścia do sprawdzania kodu użytkownika i generowania informacji lub kodu na podstawie tej analizy używanej obecnie przez technologie:

  • Odbicie środowiska uruchomieniowego.
  • Żonglowanie zadaniami programu MSBuild.
  • Tkanie języka pośredniego (IL) (nie omówiono w tym artykule).

Generatory źródeł mogą być ulepszeniem każdego podejścia.

Odbicie środowiska uruchomieniowego

Odbicie środowiska uruchomieniowego to zaawansowana technologia, która została dodana do platformy .NET dawno temu. Istnieją niezliczone scenariusze korzystania z niego. Typowym scenariuszem jest przeprowadzenie analizy kodu użytkownika podczas uruchamiania aplikacji i używania tych danych do generowania elementów.

Na przykład ASP.NET Core używa odbicia, gdy usługa internetowa jest uruchamiana po raz pierwszy w celu odnalezienia zdefiniowanych konstrukcji, dzięki czemu może "połączyć się" z elementami, takimi jak kontrolery i strony razor. Mimo że umożliwia to pisanie prostego kodu z zaawansowanymi abstrakcjami, wiąże się to z karą wydajności w czasie wykonywania: po pierwszym uruchomieniu usługi internetowej lub aplikacji nie można zaakceptować żadnych żądań, dopóki nie zostanie uruchomiony cały kod odbicia środowiska uruchomieniowego, który odnajduje informacje o kodzie. Mimo że ta kara za wydajność nie jest ogromna, jest to nieco stały koszt, którego nie można poprawić samodzielnie we własnej aplikacji.

W przypadku generatora źródła faza odnajdywania kontrolera podczas uruchamiania może się zdarzyć w czasie kompilacji. Generator może analizować kod źródłowy i emitować kod, który musi "połączyć" aplikację. Użycie generatorów źródeł może spowodować skrócenie czasu uruchamiania, ponieważ akcja wykonywana w czasie wykonywania dzisiaj może zostać wypchnięta do czasu kompilacji.

Żonglowanie zadań programu MSBuild

Generatory źródeł mogą poprawić wydajność w sposób, który nie jest ograniczony do odbicia w czasie wykonywania w celu odnajdywania typów. Niektóre scenariusze obejmują wielokrotne wywoływanie zadania C# programu MSBuild (nazywanego CSC), aby umożliwić inspekcję danych z kompilacji. Jak można sobie wyobrazić, wywołanie kompilatora więcej niż raz wpływa na całkowity czas potrzebny na skompilowanie aplikacji. Badamy, w jaki sposób generatory źródeł mogą służyć do rozwiązywania problemów związanych z żonglowaniem takich zadań programu MSBuild, ponieważ generatory źródeł nie oferują tylko pewnych korzyści z wydajności, ale także umożliwiają działanie narzędzi na odpowiednim poziomie abstrakcji.

Inną funkcją generatorów źródeł może być wykorzystanie niektórych interfejsów API wpisanych ciągowo, takich jak sposób działania ASP.NET Core routingu między kontrolerami i stronami razor. W przypadku generatora źródła routing może być silnie typizowany przy użyciu niezbędnych ciągów generowanych jako szczegóły czasu kompilacji. Spowoduje to zmniejszenie liczby nieporozumionych literałów ciągu prowadzi do żądania, które nie osiąga poprawnego kontrolera.

Wprowadzenie do generatorów źródeł

W tym przewodniku zapoznasz się z tworzeniem generatora źródeł przy użyciu interfejsu ISourceGenerator API.

  1. Utwórz aplikację konsolową platformy .NET. W tym przykładzie użyto platformy .NET 6.

  2. Zastąp klasę Program następującym kodem. Poniższy kod nie używa instrukcji najwyższego poziomu. Formularz klasyczny jest wymagany, ponieważ ten pierwszy generator źródła zapisuje w tej Program klasie metodę częściową:

    namespace ConsoleApp;
    
    partial class Program
    {
        static void Main(string[] args)
        {
            HelloFrom("Generated Code");
        }
    
        static partial void HelloFrom(string name);
    }
    

    Uwaga

    Możesz uruchomić ten przykład w stanie takim, w jakim jest, ale nic się jeszcze nie stanie.

  3. Następnie utworzymy projekt generatora źródła, który zaimplementuje partial void HelloFrom odpowiednik metody.

  4. Utwórz standardowy projekt biblioteki platformy .NET przeznaczony dla netstandard2.0 docelowego monikera platformy (TFM). Dodaj pakiety NuGet Microsoft.CodeAnalysis.Analyzers i Microsoft.CodeAnalysis.CSharp:

    <Project Sdk="Microsoft.NET.Sdk">
    
      <PropertyGroup>
        <TargetFramework>netstandard2.0</TargetFramework>
      </PropertyGroup>
    
      <ItemGroup>
        <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" PrivateAssets="all" />
        <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
      </ItemGroup>
    
    </Project>
    

    Porada

    Projekt generatora źródłowego musi być przeznaczony dla serwera netstandard2.0 TFM. W przeciwnym razie nie będzie działać.

  5. Utwórz nowy plik w języku C# o nazwie HelloSourceGenerator.cs , który określa własny generator źródła w następujący sposób:

    using Microsoft.CodeAnalysis;
    
    namespace SourceGenerator
    {
        [Generator]
        public class HelloSourceGenerator : ISourceGenerator
        {
            public void Execute(GeneratorExecutionContext context)
            {
                // Code generation goes here
            }
    
            public void Initialize(GeneratorInitializationContext context)
            {
                // No initialization required for this one
            }
        }
    }
    

    Generator źródła musi zaimplementować Microsoft.CodeAnalysis.ISourceGenerator interfejs i mieć element Microsoft.CodeAnalysis.GeneratorAttribute. Nie wszystkie generatory źródeł wymagają inicjowania, a w przypadku tej przykładowej implementacji — gdzie ISourceGenerator.Initialize jest pusta.

  6. Zastąp zawartość ISourceGenerator.Execute metody następującą implementacją:

    using Microsoft.CodeAnalysis;
    
    namespace SourceGenerator
    {
        [Generator]
        public class HelloSourceGenerator : ISourceGenerator
        {
            public void Execute(GeneratorExecutionContext context)
            {
                // Find the main method
                var mainMethod = context.Compilation.GetEntryPoint(context.CancellationToken);
    
                // Build up the source code
                string source = $@"// <auto-generated/>
    using System;
    
    namespace {mainMethod.ContainingNamespace.ToDisplayString()}
    {{
        public static partial class {mainMethod.ContainingType.Name}
        {{
            static partial void HelloFrom(string name) =>
                Console.WriteLine($""Generator says: Hi from '{{name}}'"");
        }}
    }}
    ";
                var typeName = mainMethod.ContainingType.Name;
    
                // Add the source code to the compilation
                context.AddSource($"{typeName}.g.cs", source);
            }
    
            public void Initialize(GeneratorInitializationContext context)
            {
                // No initialization required for this one
            }
        }
    }
    

    context Z obiektu możemy uzyskać dostęp do punktu wejścia kompilacji lub Main metody. Wystąpienie mainMethod jest elementem IMethodSymboli reprezentuje metodę lub symbol przypominający metodę (w tym konstruktora, destruktora, operatora lub metodę dostępu do właściwości/zdarzeń). Metoda Microsoft.CodeAnalysis.Compilation.GetEntryPoint zwraca IMethodSymbol wartość punktu wejścia programu. Inne metody umożliwiają znalezienie dowolnego symbolu metody w projekcie. Z tego obiektu możemy wnioskować o zawierającej przestrzeń nazw (jeśli istnieje) i typ. W source tym przykładzie jest ciąg interpolowany, który szablonuje kod źródłowy do wygenerowania, gdzie otwory interpolowane są wypełnione zawierającymi przestrzeniami nazw i informacjami o typie. Element source jest dodawany do elementu context z nazwą wskazówki. W tym przykładzie generator tworzy nowy wygenerowany plik źródłowy, który zawiera implementację partial metody w aplikacji konsolowej. Możesz pisać generatory źródeł, aby dodać dowolne źródło.

    Porada

    Parametr hintName z metody może być dowolną GeneratorExecutionContext.AddSource unikatową nazwą. Często udostępnia się jawne rozszerzenie pliku języka C#, takie jak ".g.cs" lub ".generated.cs" dla nazwy. Nazwa pliku ułatwia zidentyfikowanie pliku jako wygenerowanego źródła.

  7. Mamy teraz działający generator, ale musimy połączyć go z naszą aplikacją konsolową. Zmodyfikuj oryginalny projekt aplikacji konsolowej i dodaj następujący kod, zastępując ścieżkę projektu ścieżką z projektu .NET Standard utworzonego powyżej:

    <!-- Add this as a new ItemGroup, replacing paths and names appropriately -->
    <ItemGroup>
        <ProjectReference Include="..\PathTo\SourceGenerator.csproj"
                          OutputItemType="Analyzer"
                          ReferenceOutputAssembly="false" />
    </ItemGroup>
    

    To nowe odwołanie nie jest tradycyjnym odwołaniem do projektu i należy je edytować ręcznie, aby uwzględnić OutputItemType atrybuty i ReferenceOutputAssembly . Aby uzyskać więcej informacji na OutputItemType temat atrybutów ProjectReferencei ReferenceOutputAssembly programu , zobacz Typowe elementy projektu MSBuild: ProjectReference.

  8. Teraz po uruchomieniu aplikacji konsolowej powinien zostać wyświetlony komunikat o uruchomieniu wygenerowanego kodu i wydrukowaniu go na ekranie. Sama aplikacja konsolowa nie implementuje HelloFrom metody, zamiast tego jest źródłem generowanym podczas kompilacji z projektu Generator źródła. Poniższy tekst to przykładowe dane wyjściowe aplikacji:

    Generator says: Hi from 'Generated Code'
    

    Uwaga

    Może być konieczne ponowne uruchomienie programu Visual Studio, aby wyświetlić funkcję IntelliSense i pozbyć się błędów, ponieważ środowisko narzędzi jest aktywnie ulepszane.

  9. Jeśli używasz programu Visual Studio, zobaczysz pliki wygenerowane przez źródło. W oknie Eksplorator rozwiązań rozwiń węzeł >SourceGenerator>SourceGenerator SourceGenerator.HelloSourceGenerator>, a następnie kliknij dwukrotnie plik Program.g.cs.

    Visual Studio: Eksplorator rozwiązań pliki wygenerowane przez źródło.

    Po otwarciu tego wygenerowanego pliku program Visual Studio wskaże, że plik jest generowany automatycznie i że nie można go edytować.

    Visual Studio: automatycznie wygenerowany plik Program.g.cs.

  10. Można również ustawić właściwości kompilacji, aby zapisać wygenerowany plik i kontrolować miejsce przechowywania wygenerowanych plików. W pliku projektu aplikacji konsolowej dodaj <EmitCompilerGeneratedFiles> element do <PropertyGroup>elementu i ustaw jego wartość na true. Ponownie skompiluj projekt. Teraz wygenerowane pliki są tworzone w folderze obj/Debug/net6.0/generated/SourceGenerator/SourceGenerator.HelloSourceGenerator. Składniki mapy ścieżki do konfiguracji kompilacji, platformy docelowej, nazwy projektu generatora źródła i w pełni kwalifikowanej nazwy typu generatora. Możesz wybrać wygodniejszy folder wyjściowy, dodając <CompilerGeneratedFilesOutputPath> element do pliku projektu aplikacji.

Następne kroki

Książka Kucharka Generatorów źródeł przejrzy niektóre z tych przykładów z zalecanymi metodami ich rozwiązywania. Ponadto mamy zestaw przykładów dostępnych w usłudze GitHub , które możesz wypróbować samodzielnie.

Więcej informacji na temat generatorów źródeł można dowiedzieć się w następujących artykułach: