diff --git a/docs/csharp/roslyn-sdk/index.md b/docs/csharp/roslyn-sdk/index.md index ce52af2d1b1ea..4130760db1993 100644 --- a/docs/csharp/roslyn-sdk/index.md +++ b/docs/csharp/roslyn-sdk/index.md @@ -121,10 +121,17 @@ practices. ## Source generators Source generators aim to enable *compile time metaprogramming*, that is, code that can be created -at compile time and added to the compilation. Source generators are able to read the contents of +at compile time and added to the compilation. Source generators read the contents of the compilation before running, as well as access any *additional files*. This ability enables them to -introspect both user C# code and generator-specific files. You can learn how to build incremental -source generators using the [source generator cookbook](https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.cookbook.md). +introspect both user C# code and generator-specific files. Incremental source generators use a +pipeline model that filters and transforms data incrementally, keeping the IDE responsive. + +To learn more, see the following resources: + +- [Source generators overview](source-generators-overview.md) +- [Tutorial: Create an incremental source generator](tutorials/incremental-source-generator-tutorial.md) +- [Source generator samples](https://github.com/dotnet/samples/tree/main/csharp/roslyn-sdk/SourceGenerators) in the dotnet/samples repository +- [Roslyn incremental generators specification](https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md) ## Next steps diff --git a/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/GenerateMembersDemo/.gitignore b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/GenerateMembersDemo/.gitignore new file mode 100644 index 0000000000000..cd42ee34e873b --- /dev/null +++ b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/GenerateMembersDemo/.gitignore @@ -0,0 +1,2 @@ +bin/ +obj/ diff --git a/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/GenerateMembersDemo/GenerateMembersDemo.csproj b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/GenerateMembersDemo/GenerateMembersDemo.csproj new file mode 100644 index 0000000000000..e255de0784042 --- /dev/null +++ b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/GenerateMembersDemo/GenerateMembersDemo.csproj @@ -0,0 +1,16 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/GenerateMembersDemo/Program.cs b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/GenerateMembersDemo/Program.cs new file mode 100644 index 0000000000000..5a58bfe547cc4 --- /dev/null +++ b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/GenerateMembersDemo/Program.cs @@ -0,0 +1,20 @@ +// +using GenerateMembersGenerator; + +var person = new Person { FirstName = "Alice", LastName = "Smith", Age = 30 }; + +Console.WriteLine(person.Describe()); +Console.WriteLine("Properties:"); +foreach (string name in Person.PropertyNames) +{ + Console.WriteLine($" {name}"); +} + +[GenerateMembers] +public partial class Person +{ + public string FirstName { get; set; } = string.Empty; + public string LastName { get; set; } = string.Empty; + public int Age { get; set; } +} +// diff --git a/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/GenerateMembersGenerator/.gitignore b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/GenerateMembersGenerator/.gitignore new file mode 100644 index 0000000000000..cd42ee34e873b --- /dev/null +++ b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/GenerateMembersGenerator/.gitignore @@ -0,0 +1,2 @@ +bin/ +obj/ diff --git a/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/GenerateMembersGenerator/GenerateMembersGenerator.csproj b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/GenerateMembersGenerator/GenerateMembersGenerator.csproj new file mode 100644 index 0000000000000..295eee91dc8da --- /dev/null +++ b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/GenerateMembersGenerator/GenerateMembersGenerator.csproj @@ -0,0 +1,14 @@ + + + + netstandard2.0 + latest + true + + + + + + + + diff --git a/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/GenerateMembersGenerator/GenerateMembersIncrementalGenerator.cs b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/GenerateMembersGenerator/GenerateMembersIncrementalGenerator.cs new file mode 100644 index 0000000000000..f1a6f7c537b3f --- /dev/null +++ b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/GenerateMembersGenerator/GenerateMembersIncrementalGenerator.cs @@ -0,0 +1,168 @@ +#nullable enable + +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace GenerateMembersGenerator; + +// +/// +/// A source generator that adds a Describe() method and a +/// PropertyNames list to any class or struct +/// decorated with [GenerateMembers]. +/// +[Generator] +public class GenerateMembersIncrementalGenerator : IIncrementalGenerator +{ + private const string AttributeFullName = "GenerateMembersGenerator.GenerateMembersAttribute"; + + // + public void Initialize(IncrementalGeneratorInitializationContext context) + { + // 1. Emit the marker attribute so users don't need a separate reference. + context.RegisterPostInitializationOutput(static ctx => + { + ctx.AddSource("GenerateMembersAttribute.g.cs", SourceText.From(AttributeSource, Encoding.UTF8)); + }); + + // 2. Filter for type declarations annotated with [GenerateMembers]. + IncrementalValuesProvider typeInfos = context.SyntaxProvider + .ForAttributeWithMetadataName( + AttributeFullName, + predicate: static (node, _) => node is TypeDeclarationSyntax, + transform: static (ctx, _) => GetTypeInfo(ctx)) + .Where(static t => t is not null)!; + + // 3. Generate source for each qualifying type. + context.RegisterSourceOutput(typeInfos, static (spc, typeInfo) => + { + string source = GenerateSource(typeInfo); + string hintName = typeInfo.FullyQualifiedName + .Replace("global::", "") + .Replace("::", ".") + .Replace("<", "_") + .Replace(">", "_"); + spc.AddSource($"{hintName}.GeneratedMembers.g.cs", SourceText.From(source, Encoding.UTF8)); + }); + } + // + + // + private static TypeInfo? GetTypeInfo(GeneratorAttributeSyntaxContext context) + { + if (context.TargetSymbol is not INamedTypeSymbol typeSymbol) + return null; + + string typeKeyword = context.TargetNode is StructDeclarationSyntax ? "struct" : "class"; + + ImmutableArray properties = typeSymbol.GetMembers() + .OfType() + .Where(p => !p.IsStatic && !p.IsIndexer) + .ToImmutableArray(); + + return new TypeInfo( + typeSymbol.ContainingNamespace.IsGlobalNamespace + ? null + : typeSymbol.ContainingNamespace.ToDisplayString(), + typeSymbol.Name, + typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + typeKeyword, + properties.Select(p => (p.Name, p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))) + .ToImmutableArray()); + } + // + + // + private static string GenerateSource(TypeInfo typeInfo) + { + var sb = new StringBuilder(); + + sb.AppendLine("// "); + + if (typeInfo.Namespace is not null) + { + sb.AppendLine($"namespace {typeInfo.Namespace}"); + sb.AppendLine("{"); + } + + sb.AppendLine($" partial {typeInfo.TypeKeyword} {typeInfo.Name}"); + sb.AppendLine(" {"); + + // PropertyNames + sb.AppendLine(" /// Gets the names of all instance properties."); + sb.AppendLine(" public static global::System.Collections.Generic.IReadOnlyList PropertyNames { get; } ="); + sb.Append(" new string[] { "); + sb.Append(string.Join(", ", typeInfo.Properties.Select(p => $"\"{p.Name}\""))); + sb.AppendLine(" };"); + sb.AppendLine(); + + // Describe method + sb.AppendLine(" /// Returns a human-readable description of this instance."); + sb.AppendLine(" public string Describe()"); + sb.AppendLine(" {"); + sb.AppendLine($" var sb = new global::System.Text.StringBuilder();"); + sb.AppendLine($" sb.AppendLine(\"{typeInfo.Name}\");"); + foreach (var (name, _) in typeInfo.Properties) + { + sb.AppendLine($" sb.AppendLine($\" {name} = {{{name}}}\");"); + } + sb.AppendLine(" return sb.ToString();"); + sb.AppendLine(" }"); + + sb.AppendLine(" }"); + + if (typeInfo.Namespace is not null) + { + sb.AppendLine("}"); + } + + return sb.ToString(); + } + // + + // + private const string AttributeSource = @"// +namespace GenerateMembersGenerator +{ + /// + /// Add this attribute to a partial class or struct to automatically generate + /// a Describe() method and a PropertyNames list. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Struct)] + internal sealed class GenerateMembersAttribute : global::System.Attribute + { + } +} +"; + // + + // + private sealed class TypeInfo + { + public string? Namespace { get; } + public string Name { get; } + public string FullyQualifiedName { get; } + public string TypeKeyword { get; } + public ImmutableArray<(string Name, string TypeFullName)> Properties { get; } + + public TypeInfo( + string? ns, + string name, + string fullyQualifiedName, + string typeKeyword, + ImmutableArray<(string Name, string TypeFullName)> properties) + { + Namespace = ns; + Name = name; + FullyQualifiedName = fullyQualifiedName; + TypeKeyword = typeKeyword; + Properties = properties; + } + } + // +} +// diff --git a/docs/csharp/roslyn-sdk/snippets/source-generators-overview/csharp/.gitignore b/docs/csharp/roslyn-sdk/snippets/source-generators-overview/csharp/.gitignore new file mode 100644 index 0000000000000..cd42ee34e873b --- /dev/null +++ b/docs/csharp/roslyn-sdk/snippets/source-generators-overview/csharp/.gitignore @@ -0,0 +1,2 @@ +bin/ +obj/ diff --git a/docs/csharp/roslyn-sdk/snippets/source-generators-overview/csharp/MinimalGenerator.cs b/docs/csharp/roslyn-sdk/snippets/source-generators-overview/csharp/MinimalGenerator.cs new file mode 100644 index 0000000000000..5b76d34a3f81b --- /dev/null +++ b/docs/csharp/roslyn-sdk/snippets/source-generators-overview/csharp/MinimalGenerator.cs @@ -0,0 +1,23 @@ +using Microsoft.CodeAnalysis; + +// +[Generator] +public class MinimalGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + // Register a static source that always gets added to the compilation. + context.RegisterPostInitializationOutput(ctx => + { + ctx.AddSource("GeneratedHelper.g.cs", """ + // + public static class GeneratedHelper + { + public static string Greet(string name) => + $"Hello, {name}! (generated at compile time)"; + } + """); + }); + } +} +// diff --git a/docs/csharp/roslyn-sdk/snippets/source-generators-overview/csharp/SourceGeneratorsOverview.csproj b/docs/csharp/roslyn-sdk/snippets/source-generators-overview/csharp/SourceGeneratorsOverview.csproj new file mode 100644 index 0000000000000..b3b376f542de2 --- /dev/null +++ b/docs/csharp/roslyn-sdk/snippets/source-generators-overview/csharp/SourceGeneratorsOverview.csproj @@ -0,0 +1,15 @@ + + + + netstandard2.0 + latest + enable + true + + + + + + + + diff --git a/docs/csharp/roslyn-sdk/source-generators-overview.md b/docs/csharp/roslyn-sdk/source-generators-overview.md new file mode 100644 index 0000000000000..aa23fcc7ce180 --- /dev/null +++ b/docs/csharp/roslyn-sdk/source-generators-overview.md @@ -0,0 +1,173 @@ +--- +title: Source generators overview +description: Learn about source generators in .NET, how they work, and when to use incremental source generators to generate code at compile time. +ms.date: 02/06/2026 +ai-usage: ai-assisted +--- + +# Source generators overview + +Source generators let you generate C# source code during compilation. A source generator reads your existing code, performs analysis, and produces new files that the compiler includes in the compilation. Unlike reflection-based approaches, source generators run at compile time, improving performance and enabling ahead-of-time (AOT) compilation scenarios. + +## What source generators do + +A source generator is a component that plugs into the C# compiler. It runs as part of compilation and can inspect the program being compiled. Based on what it finds, it produces additional C# source files that the compiler includes in the final output. + +Source generators can: + +- Read the source code of the project being compiled. +- Read additional files included in the compilation. +- Create new C# source files that become part of the compilation. +- Report diagnostics (warnings and errors) to the user. + +Source generators *can't* modify existing source code. They can only *add* new source files. + +## Why use source generators + +Source generators solve several common problems in .NET development: + +- **Replace runtime reflection.** Instead of inspecting types at runtime, generate strongly typed code at compile time. This improves startup performance and enables AOT compilation. +- **Eliminate boilerplate.** Automatically generate repetitive code patterns like `INotifyPropertyChanged` implementations, serialization logic, or dependency injection registrations. +- **Enforce patterns at compile time.** Generate code that conforms to specific patterns, catching issues during compilation instead of at runtime. +- **Improve performance.** Pre-compute work during compilation rather than at runtime, avoiding the overhead of runtime code generation or reflection. + +## Source generators in the .NET platform + +Several .NET libraries and frameworks use source generators. Some notable examples include: + +- **System.Text.Json** generates serialization code for JSON processing, avoiding runtime reflection. +- **LoggerMessage** generates high-performance logging methods from partial method declarations. +- **Regex** generates optimized regular expression matching code at compile time. +- **LibraryImport** generates P/Invoke marshalling code for native interop. + +## Incremental source generators + +Incremental source generators implement the interface and represent the current recommended approach for building source generators. They replace the older `ISourceGenerator` interface. + +Incremental generators use a *pipeline model* that filters and transforms data incrementally. The compiler caches intermediate results and only reruns the parts of the pipeline that are affected by changes. This design keeps the IDE responsive even in large projects. + +### Pipeline stages + +An incremental generator defines a pipeline in its `Initialize` method. The pipeline typically follows these stages: + +1. **Select nodes.** Use a syntax provider to find candidate syntax nodes (for example, class declarations with a specific attribute). +1. **Filter with semantics.** Apply the semantic model to confirm the candidate matches your criteria. +1. **Generate output.** Produce new source code and add it to the compilation. + +The following example shows a minimal incremental source generator that produces a static helper class: + +:::code language="csharp" source="./snippets/source-generators-overview/csharp/MinimalGenerator.cs" id="MinimalGenerator"::: + +The `[Generator]` attribute marks the class as a source generator. The `Initialize` method registers a post-initialization output that injects the `GeneratedHelper` class into every compilation that references this generator. + +### Common input providers + +The `IncrementalGeneratorInitializationContext` exposes several providers that supply data to the pipeline: + +- **`SyntaxProvider`**: Use `CreateSyntaxProvider` to select syntax nodes based on a fast predicate, then transform them with the semantic model. Use `ForAttributeWithMetadataName` when you need to find types or members that carry a specific attribute—it combines syntactic filtering and semantic resolution in a single call. +- **`AdditionalTextsProvider`**: Access non-C# files (for example, `.csv`, `.json`, or `.xml` files) that are included in the project as `AdditionalFiles`. This provider enables generators that compile external data into C# code at build time. +- **`CompilationProvider`**: Access the full `Compilation` object for advanced scenarios where you need global compilation information. +- **`AnalyzerConfigOptionsProvider`**: Read configuration from `.editorconfig` files or MSBuild properties. + +### How the compiler caches results + +The compiler tracks the inputs to each pipeline stage. When a file changes, the compiler reruns only the stages whose inputs were affected. If a stage produces the same output as before, downstream stages don't rerun. This caching behavior is automatic—you get it by structuring your generator as a pipeline. + +## Create a source generator project + +Source generator projects require a specific setup: + +- **Target `netstandard2.0`.** The compiler host loads generators as .NET Standard 2.0 assemblies. +- **Reference `Microsoft.CodeAnalysis.CSharp`.** This package provides the Roslyn APIs for syntax analysis and code generation. +- **Reference `Microsoft.CodeAnalysis.Analyzers`.** This package provides analyzer rules that help you author correct generators. +- **Set `EnforceExtendedAnalyzerRules` to `true`.** This property enables additional rules that help you avoid common source generator pitfalls. + +A typical source generator project file looks like this: + +```xml + + + + netstandard2.0 + latest + true + + + + + + + + +``` + +### Reference a source generator from a consuming project + +The consuming project references the generator as an analyzer, not as a regular project reference: + +```xml + + + +``` + +The `OutputItemType="Analyzer"` attribute tells the compiler to load the referenced project as an analyzer. The `ReferenceOutputAssembly="false"` attribute prevents the consuming project from referencing the generator's types directly at runtime. + +### Expose additional files to a generator + +To pass non-C# files to a source generator (for example, `.csv` data files), include them as `AdditionalFiles` in the consuming project: + +```xml + + + +``` + +The generator accesses these files through `context.AdditionalTextsProvider` in its pipeline. + +## Debug source generators + +To debug a source generator, add the following code at the start of the `Initialize` method: + +```csharp +#if DEBUG +if (!System.Diagnostics.Debugger.IsAttached) +{ + System.Diagnostics.Debugger.Launch(); +} +#endif +``` + +When you build the consuming project, the debugger attach dialog appears, and you can step through the generator code. + +## Best practices + +Follow these guidelines when building source generators: + +- **Use `ForAttributeWithMetadataName` for attribute-driven generators.** It's simpler and more efficient than manually filtering with `CreateSyntaxProvider`. +- **Keep the predicate fast.** The `predicate` callback in `CreateSyntaxProvider` or `ForAttributeWithMetadataName` runs on every syntax node. Perform only quick syntactic checks. +- **Use `static` lambdas.** Mark your pipeline callbacks as `static` to avoid accidental closures that prevent caching. +- **Return small, immutable data objects from transforms.** This lets the pipeline compare results between runs and skip downstream stages when nothing changed. +- **Handle the global namespace.** Types declared without a namespace need special handling in your output. +- **Generate `partial` types.** Emit `partial` classes and methods so your generated code integrates with user-written code. +- **Use fully qualified type names.** Prefix generated type references with `global::` (for example, `global::System.Text.StringBuilder`) to avoid namespace conflicts. +- **Include `auto-generated` comments.** Start generated files with `// ` so code analysis tools skip them. +- **Report errors as diagnostics.** Use `SourceProductionContext.ReportDiagnostic` instead of throwing exceptions. + +## Samples + +Working source generator samples are available in the [dotnet/samples](https://github.com/dotnet/samples/tree/main/csharp/roslyn-sdk/SourceGenerators) repository: + +- [**GenerateMembers**](https://github.com/dotnet/samples/tree/main/csharp/roslyn-sdk/SourceGenerators/GenerateMembers)—Uses `ForAttributeWithMetadataName` to add a `Describe()` method and a `PropertyNames` list to any type decorated with a marker attribute. +- [**CsvGenerator**](https://github.com/dotnet/samples/tree/main/csharp/roslyn-sdk/SourceGenerators/CsvGenerator)—Uses `AdditionalTextsProvider` to read `.csv` files at build time and generate strongly-typed C# classes from them. + +## Next steps + +- [Tutorial: Create an incremental source generator](tutorials/incremental-source-generator-tutorial.md) +- [Roslyn incremental generators specification](https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md) diff --git a/docs/csharp/roslyn-sdk/tutorials/incremental-source-generator-tutorial.md b/docs/csharp/roslyn-sdk/tutorials/incremental-source-generator-tutorial.md new file mode 100644 index 0000000000000..e1c935ab97750 --- /dev/null +++ b/docs/csharp/roslyn-sdk/tutorials/incremental-source-generator-tutorial.md @@ -0,0 +1,251 @@ +--- +title: "Tutorial: Create an incremental source generator" +description: Build a complete incremental source generator step-by-step using the IIncrementalGenerator interface and the Roslyn APIs. +ms.date: 02/06/2026 +ai-usage: ai-assisted +--- + +# Tutorial: Create an incremental source generator + +In this tutorial, you build an incremental source generator that generates new members inside a `partial` class when a marker attribute is applied. You'll learn how to: + +- Set up a source generator project. +- Inject a marker attribute into the compilation with `RegisterPostInitializationOutput`. +- Use `ForAttributeWithMetadataName` to find types with a specific attribute. +- Extract type metadata from the semantic model. +- Emit generated source code with `RegisterSourceOutput`. +- Reference the generator from a consuming application. + +The generator you build creates a `Describe()` method and a `PropertyNames` list for any class or struct decorated with `[GenerateMembers]`. + +> [!TIP] +> The complete sample code for this tutorial is available in the [dotnet/samples](https://github.com/dotnet/samples/tree/main/csharp/roslyn-sdk/SourceGenerators/GenerateMembers) repository. + +## Prerequisites + +- [.NET 8 SDK](https://dotnet.microsoft.com/download) or later. +- A code editor such as [Visual Studio](https://visualstudio.microsoft.com/downloads/), [Visual Studio Code](https://code.visualstudio.com/), or any text editor. + +## Create the generator project + +Start by creating a class library for the source generator. Source generators must target .NET Standard 2.0 because the compiler loads them as .NET Standard assemblies. + +1. Create a folder for the solution and navigate into it: + + ```bash + mkdir SourceGeneratorDemo + cd SourceGeneratorDemo + ``` + +1. Create the source generator class library: + + ```bash + dotnet new classlib -n GenerateMembersGenerator + ``` + +1. Replace the contents of `GenerateMembersGenerator/GenerateMembersGenerator.csproj` with the following project file: + + ```xml + + + + netstandard2.0 + latest + true + + + + + + + + + ``` + + Key settings: + + - **`netstandard2.0`**: Required because the compiler loads generators as .NET Standard 2.0 assemblies. + - **`LangVersion latest`**: Lets you use modern C# features in the generator code itself. + - **`EnforceExtendedAnalyzerRules`**: Enables extra compile-time checks that catch common source generator mistakes. + - **`PrivateAssets="all"`**: Prevents the Roslyn packages from flowing to consumers of the generator. + - **`Microsoft.CodeAnalysis.Analyzers`**: Provides analyzer rules that help you author correct generators. + +1. Delete the generated `Class1.cs` file: + + ```bash + rm GenerateMembersGenerator/Class1.cs + ``` + +## Write the source generator + +The generator has three responsibilities: + +- Inject a marker attribute (`[GenerateMembers]`) into the compilation. +- Find types decorated with that attribute and extract their property metadata. +- Generate a `Describe()` method and a `PropertyNames` list for each matching type. + +Create a file named `GenerateMembersIncrementalGenerator.cs` in the `GenerateMembersGenerator` folder and add the following code. + +### Define the marker attribute + +The generator injects its own marker attribute into the consuming project at compile time. This technique avoids requiring consumers to reference a separate shared library just for the attribute definition. + +Define the attribute source as a constant string in the generator class: + +:::code language="csharp" source="../snippets/incremental-source-generator-tutorial/csharp/GenerateMembersGenerator/GenerateMembersIncrementalGenerator.cs" id="AttributeSource"::: + +The attribute targets both classes and structs, and uses fully qualified type names (prefixed with `global::`) to avoid namespace conflicts in the consuming project. + +### Implement the Initialize method + +The `Initialize` method defines the incremental pipeline. It runs once when the compiler loads the generator. + +:::code language="csharp" source="../snippets/incremental-source-generator-tutorial/csharp/GenerateMembersGenerator/GenerateMembersIncrementalGenerator.cs" id="Initialize"::: + +The pipeline has three parts: + +1. **`RegisterPostInitializationOutput`** injects the `GenerateMembersAttribute` into the compilation so consumers can reference it without an extra assembly. +1. **`ForAttributeWithMetadataName`** combines the syntactic predicate and semantic matching into a single call. It filters for type declarations that carry the `[GenerateMembers]` attribute, then calls `GetTypeInfo` to extract metadata. This approach is simpler and more efficient than writing a separate `CreateSyntaxProvider` with manual attribute-name checking. +1. **`RegisterSourceOutput`** wires up the code generation step. + +### Extract type metadata + +The `GetTypeInfo` method receives a `GeneratorAttributeSyntaxContext` and extracts the information needed to generate code—the namespace, type name, type keyword (`class` or `struct`), and a list of instance properties: + +:::code language="csharp" source="../snippets/incremental-source-generator-tutorial/csharp/GenerateMembersGenerator/GenerateMembersIncrementalGenerator.cs" id="GetTypeInfo"::: + +This method: + +- Checks `IsGlobalNamespace` so the generator handles types declared without a namespace. +- Filters out `static` and indexer properties, keeping only instance properties. +- Returns an immutable data object (`TypeInfo`) that the pipeline can cache and compare between runs. + +### Define the TypeInfo data class + +The `TypeInfo` class holds the extracted metadata. It's an immutable container that the pipeline uses for caching: + +:::code language="csharp" source="../snippets/incremental-source-generator-tutorial/csharp/GenerateMembersGenerator/GenerateMembersIncrementalGenerator.cs" id="TypeInfoClass"::: + +### Add the code generation step + +The `GenerateSource` method takes a `TypeInfo` and builds the generated source code as a string. It emits a `partial` type with two members: + +:::code language="csharp" source="../snippets/incremental-source-generator-tutorial/csharp/GenerateMembersGenerator/GenerateMembersIncrementalGenerator.cs" id="GenerateSource"::: + +Key points: + +- **`PropertyNames`** is a `static IReadOnlyList` containing the names of all instance properties. +- **`Describe()`** is an instance method that returns a human-readable description of the object using `StringBuilder`. +- Generated types use fully qualified names (for example, `global::System.Text.StringBuilder`) to avoid `using` conflicts. +- Each generated file starts with `// ` so code analysis tools skip it. + +### Build the generator + +Build the project to verify there are no errors: + +```bash +dotnet build GenerateMembersGenerator +``` + +## Create the consuming application + +Now create a console application that uses the generator. + +1. From the `SourceGeneratorDemo` folder, create a console application: + + ```bash + dotnet new console -n GenerateMembersDemo + ``` + +1. Replace the contents of `GenerateMembersDemo/GenerateMembersDemo.csproj` with the following: + + ```xml + + + + Exe + net8.0 + enable + enable + + + + + + + + ``` + + The `ProjectReference` uses `OutputItemType="Analyzer"` to tell the compiler to load `GenerateMembersGenerator` as a source generator. The `ReferenceOutputAssembly="false"` attribute prevents the consuming project from referencing the generator assembly at runtime. + +1. Replace the contents of `GenerateMembersDemo/Program.cs`: + + :::code language="csharp" source="../snippets/incremental-source-generator-tutorial/csharp/GenerateMembersDemo/Program.cs" id="Program"::: + + The `Person` class is marked `partial` so the compiler can merge it with the generated `partial class` that contains the `Describe()` method and `PropertyNames` property. + +## Run the application + +Run the application from the `SourceGeneratorDemo` folder: + +```bash +dotnet run --project GenerateMembersDemo +``` + +The output is: + +```output +Person + FirstName = Alice + LastName = Smith + Age = 30 + +Properties: + FirstName + LastName + Age +``` + +The `Describe()` method and `PropertyNames` list don't exist in your source code—the source generator created them at compile time. + +## Explore the generated code + +To see the generated files, add the following property to `GenerateMembersDemo.csproj` inside the ``: + +```xml +true +``` + +Build the project again, and look in the `obj/Debug/net8.0/generated/` folder. You'll find: + +- `GenerateMembersAttribute.g.cs`: The marker attribute injected by `RegisterPostInitializationOutput`. +- `Person.GeneratedMembers.g.cs`: The generated partial class with `Describe()` and `PropertyNames`. + +## How the pipeline works + +Understanding the incremental pipeline helps you write efficient generators: + +1. **`ForAttributeWithMetadataName` combines filtering and matching.** The compiler resolves attribute metadata internally, so you don't need a separate syntactic predicate and semantic transform. The `predicate` parameter receives only the syntax node type check. +1. **`GetTypeInfo` extracts only what's needed.** By returning a small, immutable data object, you keep the pipeline cacheable. The compiler compares `TypeInfo` outputs between runs and skips code generation when nothing changed. +1. **Caching eliminates redundant work.** If a file that doesn't contain `[GenerateMembers]` changes, the generator doesn't rerun the `GenerateSource` step. +1. **Output runs only when inputs change.** The `RegisterSourceOutput` callback runs only when the `TypeInfo` value differs from the previous compilation. + +## Next steps + +Try extending this generator or exploring other source generator patterns: + +- Add diagnostic reporting with `SourceProductionContext.ReportDiagnostic` for error cases (for example, when `[GenerateMembers]` is applied to a non-partial type). +- Build a generator that reads non-C# files using `AdditionalTextsProvider`—see the [CsvGenerator sample](https://github.com/dotnet/samples/tree/main/csharp/roslyn-sdk/SourceGenerators/CsvGenerator) for an example that turns `.csv` files into strongly-typed C# classes. +- Read additional configuration from `.editorconfig` or MSBuild properties via `AnalyzerConfigOptionsProvider`. + +For more information, see: + +- [Source generators overview](../source-generators-overview.md) +- [Roslyn incremental generators specification](https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md) +- [Tutorial: Write your first analyzer and code fix](how-to-write-csharp-analyzer-code-fix.md) diff --git a/docs/csharp/toc.yml b/docs/csharp/toc.yml index 5324dd7a241eb..f86afd9e00201 100644 --- a/docs/csharp/toc.yml +++ b/docs/csharp/toc.yml @@ -419,6 +419,8 @@ items: href: roslyn-sdk/syntax-visualizer.md - name: Choose diagnostic IDs href: roslyn-sdk/choosing-diagnostic-ids.md + - name: Source generators overview + href: roslyn-sdk/source-generators-overview.md - name: Quick starts items: - name: Syntax analysis @@ -431,6 +433,8 @@ items: items: - name: Build your first analyzer and code fix href: roslyn-sdk/tutorials/how-to-write-csharp-analyzer-code-fix.md + - name: Create an incremental source generator + href: roslyn-sdk/tutorials/incremental-source-generator-tutorial.md # Taken from https://github.com/dotnet/roslyn/wiki/Samples-and-Walkthroughs # - name: Get started writing custom analyzers and code fixes # - name: Tutorials