From c6251e383c5bff24a9b6a8d63d9a82c7567588e0 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 6 Feb 2026 18:32:15 +0000
Subject: [PATCH 1/3] Initial plan
From fc4ba015ea3278f3328b00359cb3ee6245398ec9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 6 Feb 2026 18:41:37 +0000
Subject: [PATCH 2/3] Add source generators conceptual overview and incremental
source generator tutorial
Co-authored-by: agocke <515774+agocke@users.noreply.github.com>
---
docs/csharp/roslyn-sdk/index.md | 12 +-
.../csharp/MyApp/.gitignore | 2 +
.../csharp/MyApp/Greeter.cs | 8 +
.../csharp/MyApp/MyApp.csproj | 16 ++
.../csharp/MyApp/Program.cs | 5 +
.../csharp/MySourceGenerator/.gitignore | 2 +
.../MySourceGenerator/HelloFromGenerator.cs | 138 +++++++++++
.../MySourceGenerator.csproj | 14 ++
.../csharp/.gitignore | 2 +
.../csharp/MinimalGenerator.cs | 23 ++
.../csharp/SourceGeneratorsOverview.csproj | 14 ++
.../roslyn-sdk/source-generators-overview.md | 139 +++++++++++
.../incremental-source-generator-tutorial.md | 234 ++++++++++++++++++
docs/csharp/toc.yml | 4 +
14 files changed, 610 insertions(+), 3 deletions(-)
create mode 100644 docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MyApp/.gitignore
create mode 100644 docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MyApp/Greeter.cs
create mode 100644 docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MyApp/MyApp.csproj
create mode 100644 docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MyApp/Program.cs
create mode 100644 docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MySourceGenerator/.gitignore
create mode 100644 docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MySourceGenerator/HelloFromGenerator.cs
create mode 100644 docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MySourceGenerator/MySourceGenerator.csproj
create mode 100644 docs/csharp/roslyn-sdk/snippets/source-generators-overview/csharp/.gitignore
create mode 100644 docs/csharp/roslyn-sdk/snippets/source-generators-overview/csharp/MinimalGenerator.cs
create mode 100644 docs/csharp/roslyn-sdk/snippets/source-generators-overview/csharp/SourceGeneratorsOverview.csproj
create mode 100644 docs/csharp/roslyn-sdk/source-generators-overview.md
create mode 100644 docs/csharp/roslyn-sdk/tutorials/incremental-source-generator-tutorial.md
diff --git a/docs/csharp/roslyn-sdk/index.md b/docs/csharp/roslyn-sdk/index.md
index ce52af2d1b1ea..c0b046311c255 100644
--- a/docs/csharp/roslyn-sdk/index.md
+++ b/docs/csharp/roslyn-sdk/index.md
@@ -121,10 +121,16 @@ 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)
+- [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/MyApp/.gitignore b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MyApp/.gitignore
new file mode 100644
index 0000000000000..cd42ee34e873b
--- /dev/null
+++ b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MyApp/.gitignore
@@ -0,0 +1,2 @@
+bin/
+obj/
diff --git a/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MyApp/Greeter.cs b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MyApp/Greeter.cs
new file mode 100644
index 0000000000000..5419e11177f86
--- /dev/null
+++ b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MyApp/Greeter.cs
@@ -0,0 +1,8 @@
+//
+using MySourceGenerator;
+
+namespace MyApp;
+
+[HelloFrom]
+public partial class Greeter;
+//
diff --git a/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MyApp/MyApp.csproj b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MyApp/MyApp.csproj
new file mode 100644
index 0000000000000..e4361319b66ac
--- /dev/null
+++ b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MyApp/MyApp.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ net9.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MyApp/Program.cs b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MyApp/Program.cs
new file mode 100644
index 0000000000000..6b93e6f1161c5
--- /dev/null
+++ b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MyApp/Program.cs
@@ -0,0 +1,5 @@
+//
+using MyApp;
+
+Console.WriteLine(Greeter.HelloFrom());
+//
diff --git a/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MySourceGenerator/.gitignore b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MySourceGenerator/.gitignore
new file mode 100644
index 0000000000000..cd42ee34e873b
--- /dev/null
+++ b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MySourceGenerator/.gitignore
@@ -0,0 +1,2 @@
+bin/
+obj/
diff --git a/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MySourceGenerator/HelloFromGenerator.cs b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MySourceGenerator/HelloFromGenerator.cs
new file mode 100644
index 0000000000000..c6199bb2de2a9
--- /dev/null
+++ b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MySourceGenerator/HelloFromGenerator.cs
@@ -0,0 +1,138 @@
+//
+using System.Collections.Immutable;
+using System.Linq;
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Text;
+
+namespace MySourceGenerator;
+
+[Generator]
+public class HelloFromGenerator : IIncrementalGenerator
+{
+ //
+ private const string HelloFromAttributeSource = @"//
+namespace MySourceGenerator
+{
+ [System.AttributeUsage(System.AttributeTargets.Class)]
+ public class HelloFromAttribute : System.Attribute
+ {
+ }
+}";
+ //
+
+ //
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ // Inject the marker attribute into the user's compilation.
+ context.RegisterPostInitializationOutput(ctx =>
+ ctx.AddSource("HelloFromAttribute.g.cs",
+ SourceText.From(HelloFromAttributeSource, Encoding.UTF8)));
+
+ // Create a syntax provider that filters to class declarations
+ // with at least one attribute.
+ IncrementalValuesProvider classDeclarations =
+ context.SyntaxProvider
+ .CreateSyntaxProvider(
+ predicate: static (s, _) => IsCandidateClass(s),
+ transform: static (ctx, _) => GetSemanticTarget(ctx))
+ .Where(static m => m is not null)!;
+
+ // Combine the selected classes with the compilation.
+ IncrementalValueProvider<(Compilation, ImmutableArray)>
+ compilationAndClasses = context.CompilationProvider
+ .Combine(classDeclarations.Collect());
+
+ // Generate source code for each matching class.
+ context.RegisterSourceOutput(compilationAndClasses,
+ static (spc, source) => Execute(source.Item1, source.Item2, spc));
+ }
+ //
+
+ //
+ private static bool IsCandidateClass(SyntaxNode node) =>
+ node is ClassDeclarationSyntax c && c.AttributeLists.Count > 0;
+ //
+
+ //
+ private static ClassDeclarationSyntax? GetSemanticTarget(
+ GeneratorSyntaxContext context)
+ {
+ var classDeclaration = (ClassDeclarationSyntax)context.Node;
+
+ foreach (AttributeListSyntax attributeList in classDeclaration.AttributeLists)
+ {
+ foreach (AttributeSyntax attribute in attributeList.Attributes)
+ {
+ if (context.SemanticModel.GetSymbolInfo(attribute).Symbol
+ is not IMethodSymbol attributeSymbol)
+ {
+ continue;
+ }
+
+ string fullName = attributeSymbol.ContainingType.ToDisplayString();
+
+ if (fullName == "MySourceGenerator.HelloFromAttribute")
+ {
+ return classDeclaration;
+ }
+ }
+ }
+
+ return null;
+ }
+ //
+
+ //
+ private static void Execute(
+ Compilation compilation,
+ ImmutableArray classes,
+ SourceProductionContext context)
+ {
+ if (classes.IsDefaultOrEmpty)
+ {
+ return;
+ }
+
+ foreach (ClassDeclarationSyntax classDeclaration in classes.Distinct())
+ {
+ SemanticModel semanticModel =
+ compilation.GetSemanticModel(classDeclaration.SyntaxTree);
+
+ if (semanticModel.GetDeclaredSymbol(classDeclaration)
+ is not INamedTypeSymbol classSymbol)
+ {
+ continue;
+ }
+
+ string namespaceName = classSymbol.ContainingNamespace.IsGlobalNamespace
+ ? string.Empty
+ : classSymbol.ContainingNamespace.ToDisplayString();
+ string className = classSymbol.Name;
+
+ string source = string.IsNullOrEmpty(namespaceName)
+ ? $@"//
+partial class {className}
+{{
+ public static string HelloFrom() =>
+ ""Hello from '{className}'"";
+}}"
+ : $@"//
+namespace {namespaceName}
+{{
+ partial class {className}
+ {{
+ public static string HelloFrom() =>
+ ""Hello from '{className}'"";
+ }}
+}}";
+
+ context.AddSource(
+ $"{className}.HelloFrom.g.cs",
+ SourceText.From(source, Encoding.UTF8));
+ }
+ }
+ //
+}
+//
diff --git a/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MySourceGenerator/MySourceGenerator.csproj b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MySourceGenerator/MySourceGenerator.csproj
new file mode 100644
index 0000000000000..b14cf40279ec5
--- /dev/null
+++ b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MySourceGenerator/MySourceGenerator.csproj
@@ -0,0 +1,14 @@
+
+
+
+ netstandard2.0
+ latest
+ enable
+ true
+
+
+
+
+
+
+
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..b14cf40279ec5
--- /dev/null
+++ b/docs/csharp/roslyn-sdk/snippets/source-generators-overview/csharp/SourceGeneratorsOverview.csproj
@@ -0,0 +1,14 @@
+
+
+
+ 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..12fcfc9599750
--- /dev/null
+++ b/docs/csharp/roslyn-sdk/source-generators-overview.md
@@ -0,0 +1,139 @@
+---
+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.
+
+### 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.
+- **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
+ enable
+ 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.
+
+## 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:
+
+- **Keep the predicate fast.** The `predicate` callback in `CreateSyntaxProvider` runs on every syntax node. Perform only quick syntactic checks—don't access the semantic model in the predicate.
+- **Use `static` lambdas.** Mark your pipeline callbacks as `static` to avoid accidental closures that prevent caching.
+- **Handle the global namespace.** Classes 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.
+- **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.
+
+## 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..6958971434574
--- /dev/null
+++ b/docs/csharp/roslyn-sdk/tutorials/incremental-source-generator-tutorial.md
@@ -0,0 +1,234 @@
+---
+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 adds a `HelloFrom` method to any class decorated with a `[HelloFrom]` attribute. You'll learn how to:
+
+- Set up a source generator project.
+- Define a pipeline that finds classes with a specific attribute.
+- Use the semantic model to verify attribute matches.
+- Emit generated source code.
+- Reference the generator from a consuming application.
+
+## Prerequisites
+
+- [.NET 9 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 MySourceGenerator
+ ```
+
+1. Replace the contents of `MySourceGenerator/MySourceGenerator.csproj` with the following project file:
+
+ ```xml
+
+
+
+ netstandard2.0
+ latest
+ enable
+ 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 package from flowing to consumers of the generator.
+
+1. Delete the generated `Class1.cs` file:
+
+ ```bash
+ rm MySourceGenerator/Class1.cs
+ ```
+
+## Write the source generator
+
+The generator has three responsibilities:
+
+- Inject a marker attribute (`[HelloFrom]`) into the compilation.
+- Find classes decorated with that attribute.
+- Generate a `HelloFrom()` method for each matching class.
+
+Create a file named `HelloFromGenerator.cs` in the `MySourceGenerator` folder and add the following code.
+
+### Define the marker attribute
+
+The generator injects its own marker attribute into the consuming project. This technique avoids requiring consumers to reference a separate shared library just for the attribute definition.
+
+Add the attribute source as a constant string in the generator class:
+
+:::code language="csharp" source="./snippets/incremental-source-generator-tutorial/csharp/MySourceGenerator/HelloFromGenerator.cs" id="AttributeSource":::
+
+### 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/MySourceGenerator/HelloFromGenerator.cs" id="Initialize":::
+
+The pipeline has three parts:
+
+1. **`RegisterPostInitializationOutput`** injects the `HelloFromAttribute` into the compilation so consumers can reference it without an extra assembly.
+1. **`CreateSyntaxProvider`** defines a two-stage filter. The `predicate` does a fast syntactic check, and the `transform` uses the semantic model for precise matching.
+1. **`RegisterSourceOutput`** wires up the code generation step.
+
+### Add the syntactic predicate
+
+The predicate runs on every syntax node during compilation, so it needs to be fast. Check only whether the node is a class declaration with at least one attribute:
+
+:::code language="csharp" source="./snippets/incremental-source-generator-tutorial/csharp/MySourceGenerator/HelloFromGenerator.cs" id="Predicate":::
+
+This check is intentionally broad. It narrows the search cheaply—the transform stage handles the precise matching.
+
+### Add the semantic transform
+
+The transform stage receives nodes that passed the predicate. Use the semantic model to verify that the attribute is truly `HelloFromAttribute`:
+
+:::code language="csharp" source="./snippets/incremental-source-generator-tutorial/csharp/MySourceGenerator/HelloFromGenerator.cs" id="Transform":::
+
+This method loops through every attribute on the class and checks its fully qualified name against the semantic model. If none match, it returns `null`, and the `Where` clause in the pipeline filters it out.
+
+### Add the code generation step
+
+The `Execute` method receives the full compilation and the filtered list of class declarations. It generates a `partial class` with a `HelloFrom()` method for each match:
+
+:::code language="csharp" source="./snippets/incremental-source-generator-tutorial/csharp/MySourceGenerator/HelloFromGenerator.cs" id="Execute":::
+
+Key points:
+
+- The generator checks `IsGlobalNamespace` to handle classes declared without a namespace.
+- Each generated file uses a unique hint name (`{ClassName}.HelloFrom.g.cs`) to avoid conflicts.
+- Generated code is wrapped in the same namespace as the user's class so the `partial` declaration merges correctly.
+
+### Build the generator
+
+Build the project to verify there are no errors:
+
+```bash
+dotnet build MySourceGenerator
+```
+
+## 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 MyApp
+ ```
+
+1. Replace the contents of `MyApp/MyApp.csproj` with the following:
+
+ ```xml
+
+
+
+ Exe
+ net9.0
+ enable
+ enable
+
+
+
+
+
+
+
+ ```
+
+ The `ProjectReference` uses `OutputItemType="Analyzer"` to tell the compiler to load `MySourceGenerator` as a source generator. The `ReferenceOutputAssembly="false"` attribute prevents the consuming project from referencing the generator's types at runtime.
+
+1. Create a file named `Greeter.cs` in the `MyApp` folder:
+
+ :::code language="csharp" source="./snippets/incremental-source-generator-tutorial/csharp/MyApp/Greeter.cs" id="Greeter":::
+
+ The class is marked `partial` so the compiler can merge it with the generated `partial class` that contains the `HelloFrom()` method.
+
+1. Replace the contents of `MyApp/Program.cs`:
+
+ :::code language="csharp" source="./snippets/incremental-source-generator-tutorial/csharp/MyApp/Program.cs" id="Program":::
+
+## Run the application
+
+Run the application from the `SourceGeneratorDemo` folder:
+
+```bash
+dotnet run --project MyApp
+```
+
+The output is:
+
+```output
+Hello from 'Greeter'
+```
+
+The `HelloFrom()` method doesn't exist in your source code—the source generator created it at compile time.
+
+## Explore the generated code
+
+To see the generated files, add the following property to `MyApp.csproj` inside the ``:
+
+```xml
+true
+```
+
+Build the project again, and look in the `obj/Debug/net9.0/generated/` folder. You'll find:
+
+- `HelloFromAttribute.g.cs`: The marker attribute injected by `RegisterPostInitializationOutput`.
+- `Greeter.HelloFrom.g.cs`: The generated partial class with the `HelloFrom()` method.
+
+## How the pipeline works
+
+Understanding the incremental pipeline helps you write efficient generators:
+
+1. **Predicate runs on every node.** The compiler calls `IsCandidateClass` on every syntax node in the compilation. Because it only checks `AttributeLists.Count > 0`, this step is fast.
+1. **Transform runs on candidates.** `GetSemanticTarget` runs only on nodes that passed the predicate. It uses the semantic model—a more expensive operation—but runs on a much smaller set of nodes.
+1. **Caching eliminates redundant work.** The compiler caches results at each stage. If a file that doesn't contain `[HelloFrom]` changes, the generator doesn't rerun the `Execute` step.
+1. **Output runs only when inputs change.** The `Execute` method runs only when the list of matching classes changes, or when the compilation itself changes.
+
+## Next steps
+
+Here are some ideas for extending this generator:
+
+- Generate additional members (properties, methods) based on attribute arguments.
+- Use `ForAttributeWithMetadataName` for a more concise attribute filter (available in `Microsoft.CodeAnalysis` 4.3.1 and later).
+- Add diagnostic reporting with `SourceProductionContext.ReportDiagnostic` for error cases.
+- 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
From 771beb95c85dc13fe27d955b75632ca98de2cbdd Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 6 Feb 2026 19:09:52 +0000
Subject: [PATCH 3/3] Adapt docs and snippets to match dotnet/samples#7091
GenerateMembers sample
Co-authored-by: agocke <515774+agocke@users.noreply.github.com>
---
docs/csharp/roslyn-sdk/index.md | 1 +
.../{MyApp => GenerateMembersDemo}/.gitignore | 0
.../GenerateMembersDemo.csproj} | 6 +-
.../csharp/GenerateMembersDemo/Program.cs | 20 +++
.../.gitignore | 0
.../GenerateMembersGenerator.csproj} | 4 +-
.../GenerateMembersIncrementalGenerator.cs | 168 ++++++++++++++++++
.../csharp/MyApp/Greeter.cs | 8 -
.../csharp/MyApp/Program.cs | 5 -
.../MySourceGenerator/HelloFromGenerator.cs | 138 --------------
.../csharp/SourceGeneratorsOverview.csproj | 3 +-
.../roslyn-sdk/source-generators-overview.md | 42 ++++-
.../incremental-source-generator-tutorial.md | 143 ++++++++-------
13 files changed, 314 insertions(+), 224 deletions(-)
rename docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/{MyApp => GenerateMembersDemo}/.gitignore (100%)
rename docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/{MyApp/MyApp.csproj => GenerateMembersDemo/GenerateMembersDemo.csproj} (70%)
create mode 100644 docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/GenerateMembersDemo/Program.cs
rename docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/{MySourceGenerator => GenerateMembersGenerator}/.gitignore (100%)
rename docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/{MySourceGenerator/MySourceGenerator.csproj => GenerateMembersGenerator/GenerateMembersGenerator.csproj} (70%)
create mode 100644 docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/GenerateMembersGenerator/GenerateMembersIncrementalGenerator.cs
delete mode 100644 docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MyApp/Greeter.cs
delete mode 100644 docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MyApp/Program.cs
delete mode 100644 docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MySourceGenerator/HelloFromGenerator.cs
diff --git a/docs/csharp/roslyn-sdk/index.md b/docs/csharp/roslyn-sdk/index.md
index c0b046311c255..4130760db1993 100644
--- a/docs/csharp/roslyn-sdk/index.md
+++ b/docs/csharp/roslyn-sdk/index.md
@@ -130,6 +130,7 @@ 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/MyApp/.gitignore b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/GenerateMembersDemo/.gitignore
similarity index 100%
rename from docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MyApp/.gitignore
rename to docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/GenerateMembersDemo/.gitignore
diff --git a/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MyApp/MyApp.csproj b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/GenerateMembersDemo/GenerateMembersDemo.csproj
similarity index 70%
rename from docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MyApp/MyApp.csproj
rename to docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/GenerateMembersDemo/GenerateMembersDemo.csproj
index e4361319b66ac..e255de0784042 100644
--- a/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MyApp/MyApp.csproj
+++ b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/GenerateMembersDemo/GenerateMembersDemo.csproj
@@ -2,13 +2,13 @@
Exe
- net9.0
- enable
+ 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/MySourceGenerator/.gitignore b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/GenerateMembersGenerator/.gitignore
similarity index 100%
rename from docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MySourceGenerator/.gitignore
rename to docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/GenerateMembersGenerator/.gitignore
diff --git a/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MySourceGenerator/MySourceGenerator.csproj b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/GenerateMembersGenerator/GenerateMembersGenerator.csproj
similarity index 70%
rename from docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MySourceGenerator/MySourceGenerator.csproj
rename to docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/GenerateMembersGenerator/GenerateMembersGenerator.csproj
index b14cf40279ec5..295eee91dc8da 100644
--- a/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MySourceGenerator/MySourceGenerator.csproj
+++ b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/GenerateMembersGenerator/GenerateMembersGenerator.csproj
@@ -3,12 +3,12 @@
netstandard2.0
latest
- enable
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/incremental-source-generator-tutorial/csharp/MyApp/Greeter.cs b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MyApp/Greeter.cs
deleted file mode 100644
index 5419e11177f86..0000000000000
--- a/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MyApp/Greeter.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-//
-using MySourceGenerator;
-
-namespace MyApp;
-
-[HelloFrom]
-public partial class Greeter;
-//
diff --git a/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MyApp/Program.cs b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MyApp/Program.cs
deleted file mode 100644
index 6b93e6f1161c5..0000000000000
--- a/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MyApp/Program.cs
+++ /dev/null
@@ -1,5 +0,0 @@
-//
-using MyApp;
-
-Console.WriteLine(Greeter.HelloFrom());
-//
diff --git a/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MySourceGenerator/HelloFromGenerator.cs b/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MySourceGenerator/HelloFromGenerator.cs
deleted file mode 100644
index c6199bb2de2a9..0000000000000
--- a/docs/csharp/roslyn-sdk/snippets/incremental-source-generator-tutorial/csharp/MySourceGenerator/HelloFromGenerator.cs
+++ /dev/null
@@ -1,138 +0,0 @@
-//
-using System.Collections.Immutable;
-using System.Linq;
-using System.Text;
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp.Syntax;
-using Microsoft.CodeAnalysis.Text;
-
-namespace MySourceGenerator;
-
-[Generator]
-public class HelloFromGenerator : IIncrementalGenerator
-{
- //
- private const string HelloFromAttributeSource = @"//
-namespace MySourceGenerator
-{
- [System.AttributeUsage(System.AttributeTargets.Class)]
- public class HelloFromAttribute : System.Attribute
- {
- }
-}";
- //
-
- //
- public void Initialize(IncrementalGeneratorInitializationContext context)
- {
- // Inject the marker attribute into the user's compilation.
- context.RegisterPostInitializationOutput(ctx =>
- ctx.AddSource("HelloFromAttribute.g.cs",
- SourceText.From(HelloFromAttributeSource, Encoding.UTF8)));
-
- // Create a syntax provider that filters to class declarations
- // with at least one attribute.
- IncrementalValuesProvider classDeclarations =
- context.SyntaxProvider
- .CreateSyntaxProvider(
- predicate: static (s, _) => IsCandidateClass(s),
- transform: static (ctx, _) => GetSemanticTarget(ctx))
- .Where(static m => m is not null)!;
-
- // Combine the selected classes with the compilation.
- IncrementalValueProvider<(Compilation, ImmutableArray)>
- compilationAndClasses = context.CompilationProvider
- .Combine(classDeclarations.Collect());
-
- // Generate source code for each matching class.
- context.RegisterSourceOutput(compilationAndClasses,
- static (spc, source) => Execute(source.Item1, source.Item2, spc));
- }
- //
-
- //
- private static bool IsCandidateClass(SyntaxNode node) =>
- node is ClassDeclarationSyntax c && c.AttributeLists.Count > 0;
- //
-
- //
- private static ClassDeclarationSyntax? GetSemanticTarget(
- GeneratorSyntaxContext context)
- {
- var classDeclaration = (ClassDeclarationSyntax)context.Node;
-
- foreach (AttributeListSyntax attributeList in classDeclaration.AttributeLists)
- {
- foreach (AttributeSyntax attribute in attributeList.Attributes)
- {
- if (context.SemanticModel.GetSymbolInfo(attribute).Symbol
- is not IMethodSymbol attributeSymbol)
- {
- continue;
- }
-
- string fullName = attributeSymbol.ContainingType.ToDisplayString();
-
- if (fullName == "MySourceGenerator.HelloFromAttribute")
- {
- return classDeclaration;
- }
- }
- }
-
- return null;
- }
- //
-
- //
- private static void Execute(
- Compilation compilation,
- ImmutableArray classes,
- SourceProductionContext context)
- {
- if (classes.IsDefaultOrEmpty)
- {
- return;
- }
-
- foreach (ClassDeclarationSyntax classDeclaration in classes.Distinct())
- {
- SemanticModel semanticModel =
- compilation.GetSemanticModel(classDeclaration.SyntaxTree);
-
- if (semanticModel.GetDeclaredSymbol(classDeclaration)
- is not INamedTypeSymbol classSymbol)
- {
- continue;
- }
-
- string namespaceName = classSymbol.ContainingNamespace.IsGlobalNamespace
- ? string.Empty
- : classSymbol.ContainingNamespace.ToDisplayString();
- string className = classSymbol.Name;
-
- string source = string.IsNullOrEmpty(namespaceName)
- ? $@"//
-partial class {className}
-{{
- public static string HelloFrom() =>
- ""Hello from '{className}'"";
-}}"
- : $@"//
-namespace {namespaceName}
-{{
- partial class {className}
- {{
- public static string HelloFrom() =>
- ""Hello from '{className}'"";
- }}
-}}";
-
- context.AddSource(
- $"{className}.HelloFrom.g.cs",
- SourceText.From(source, Encoding.UTF8));
- }
- }
- //
-}
-//
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
index b14cf40279ec5..b3b376f542de2 100644
--- a/docs/csharp/roslyn-sdk/snippets/source-generators-overview/csharp/SourceGeneratorsOverview.csproj
+++ b/docs/csharp/roslyn-sdk/snippets/source-generators-overview/csharp/SourceGeneratorsOverview.csproj
@@ -8,7 +8,8 @@
-
+
+
diff --git a/docs/csharp/roslyn-sdk/source-generators-overview.md b/docs/csharp/roslyn-sdk/source-generators-overview.md
index 12fcfc9599750..aa23fcc7ce180 100644
--- a/docs/csharp/roslyn-sdk/source-generators-overview.md
+++ b/docs/csharp/roslyn-sdk/source-generators-overview.md
@@ -60,6 +60,15 @@ The following example shows a minimal incremental source generator that produces
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.
@@ -70,6 +79,7 @@ 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:
@@ -80,13 +90,15 @@ A typical source generator project file looks like this:
netstandard2.0
latest
- enable
true
+
@@ -107,6 +119,18 @@ The consuming project references the generator as an analyzer, not as a regular
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:
@@ -126,13 +150,23 @@ When you build the consuming project, the debugger attach dialog appears, and yo
Follow these guidelines when building source generators:
-- **Keep the predicate fast.** The `predicate` callback in `CreateSyntaxProvider` runs on every syntax node. Perform only quick syntactic checks—don't access the semantic model in the predicate.
+- **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.
-- **Handle the global namespace.** Classes declared without a namespace need special handling in your output.
+- **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)
diff --git a/docs/csharp/roslyn-sdk/tutorials/incremental-source-generator-tutorial.md b/docs/csharp/roslyn-sdk/tutorials/incremental-source-generator-tutorial.md
index 6958971434574..e1c935ab97750 100644
--- a/docs/csharp/roslyn-sdk/tutorials/incremental-source-generator-tutorial.md
+++ b/docs/csharp/roslyn-sdk/tutorials/incremental-source-generator-tutorial.md
@@ -7,17 +7,23 @@ ai-usage: ai-assisted
# Tutorial: Create an incremental source generator
-In this tutorial, you build an incremental source generator that adds a `HelloFrom` method to any class decorated with a `[HelloFrom]` attribute. You'll learn how to:
+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.
-- Define a pipeline that finds classes with a specific attribute.
-- Use the semantic model to verify attribute matches.
-- Emit generated source code.
+- 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 9 SDK](https://dotnet.microsoft.com/download) or later.
+- [.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
@@ -34,10 +40,10 @@ Start by creating a class library for the source generator. Source generators mu
1. Create the source generator class library:
```bash
- dotnet new classlib -n MySourceGenerator
+ dotnet new classlib -n GenerateMembersGenerator
```
-1. Replace the contents of `MySourceGenerator/MySourceGenerator.csproj` with the following project file:
+1. Replace the contents of `GenerateMembersGenerator/GenerateMembersGenerator.csproj` with the following project file:
```xml
@@ -45,13 +51,15 @@ Start by creating a class library for the source generator. Source generators mu
netstandard2.0
latest
- enable
true
+
@@ -63,78 +71,84 @@ Start by creating a class library for the source generator. Source generators mu
- **`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 package from flowing to consumers of the generator.
+ - **`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 MySourceGenerator/Class1.cs
+ rm GenerateMembersGenerator/Class1.cs
```
## Write the source generator
The generator has three responsibilities:
-- Inject a marker attribute (`[HelloFrom]`) into the compilation.
-- Find classes decorated with that attribute.
-- Generate a `HelloFrom()` method for each matching class.
+- 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 `HelloFromGenerator.cs` in the `MySourceGenerator` folder and add the following code.
+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. This technique avoids requiring consumers to reference a separate shared library just for the attribute definition.
+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:
-Add 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":::
-:::code language="csharp" source="./snippets/incremental-source-generator-tutorial/csharp/MySourceGenerator/HelloFromGenerator.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/MySourceGenerator/HelloFromGenerator.cs" id="Initialize":::
+:::code language="csharp" source="../snippets/incremental-source-generator-tutorial/csharp/GenerateMembersGenerator/GenerateMembersIncrementalGenerator.cs" id="Initialize":::
The pipeline has three parts:
-1. **`RegisterPostInitializationOutput`** injects the `HelloFromAttribute` into the compilation so consumers can reference it without an extra assembly.
-1. **`CreateSyntaxProvider`** defines a two-stage filter. The `predicate` does a fast syntactic check, and the `transform` uses the semantic model for precise matching.
+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.
-### Add the syntactic predicate
+### Extract type metadata
-The predicate runs on every syntax node during compilation, so it needs to be fast. Check only whether the node is a class declaration with at least one attribute:
+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/MySourceGenerator/HelloFromGenerator.cs" id="Predicate":::
+:::code language="csharp" source="../snippets/incremental-source-generator-tutorial/csharp/GenerateMembersGenerator/GenerateMembersIncrementalGenerator.cs" id="GetTypeInfo":::
-This check is intentionally broad. It narrows the search cheaply—the transform stage handles the precise matching.
+This method:
-### Add the semantic transform
+- 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.
-The transform stage receives nodes that passed the predicate. Use the semantic model to verify that the attribute is truly `HelloFromAttribute`:
+### Define the TypeInfo data class
-:::code language="csharp" source="./snippets/incremental-source-generator-tutorial/csharp/MySourceGenerator/HelloFromGenerator.cs" id="Transform":::
+The `TypeInfo` class holds the extracted metadata. It's an immutable container that the pipeline uses for caching:
-This method loops through every attribute on the class and checks its fully qualified name against the semantic model. If none match, it returns `null`, and the `Where` clause in the pipeline filters it out.
+:::code language="csharp" source="../snippets/incremental-source-generator-tutorial/csharp/GenerateMembersGenerator/GenerateMembersIncrementalGenerator.cs" id="TypeInfoClass":::
### Add the code generation step
-The `Execute` method receives the full compilation and the filtered list of class declarations. It generates a `partial class` with a `HelloFrom()` method for each match:
+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/MySourceGenerator/HelloFromGenerator.cs" id="Execute":::
+:::code language="csharp" source="../snippets/incremental-source-generator-tutorial/csharp/GenerateMembersGenerator/GenerateMembersIncrementalGenerator.cs" id="GenerateSource":::
Key points:
-- The generator checks `IsGlobalNamespace` to handle classes declared without a namespace.
-- Each generated file uses a unique hint name (`{ClassName}.HelloFrom.g.cs`) to avoid conflicts.
-- Generated code is wrapped in the same namespace as the user's class so the `partial` declaration merges correctly.
+- **`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 MySourceGenerator
+dotnet build GenerateMembersGenerator
```
## Create the consuming application
@@ -144,23 +158,23 @@ Now create a console application that uses the generator.
1. From the `SourceGeneratorDemo` folder, create a console application:
```bash
- dotnet new console -n MyApp
+ dotnet new console -n GenerateMembersDemo
```
-1. Replace the contents of `MyApp/MyApp.csproj` with the following:
+1. Replace the contents of `GenerateMembersDemo/GenerateMembersDemo.csproj` with the following:
```xml
Exe
- net9.0
- enable
+ net8.0
enable
+ enable
-
@@ -168,63 +182,66 @@ Now create a console application that uses the generator.
```
- The `ProjectReference` uses `OutputItemType="Analyzer"` to tell the compiler to load `MySourceGenerator` as a source generator. The `ReferenceOutputAssembly="false"` attribute prevents the consuming project from referencing the generator's types at runtime.
-
-1. Create a file named `Greeter.cs` in the `MyApp` folder:
-
- :::code language="csharp" source="./snippets/incremental-source-generator-tutorial/csharp/MyApp/Greeter.cs" id="Greeter":::
+ 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.
- The class is marked `partial` so the compiler can merge it with the generated `partial class` that contains the `HelloFrom()` method.
+1. Replace the contents of `GenerateMembersDemo/Program.cs`:
-1. Replace the contents of `MyApp/Program.cs`:
+ :::code language="csharp" source="../snippets/incremental-source-generator-tutorial/csharp/GenerateMembersDemo/Program.cs" id="Program":::
- :::code language="csharp" source="./snippets/incremental-source-generator-tutorial/csharp/MyApp/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 MyApp
+dotnet run --project GenerateMembersDemo
```
The output is:
```output
-Hello from 'Greeter'
+Person
+ FirstName = Alice
+ LastName = Smith
+ Age = 30
+
+Properties:
+ FirstName
+ LastName
+ Age
```
-The `HelloFrom()` method doesn't exist in your source code—the source generator created it at compile time.
+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 `MyApp.csproj` inside the ``:
+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/net9.0/generated/` folder. You'll find:
+Build the project again, and look in the `obj/Debug/net8.0/generated/` folder. You'll find:
-- `HelloFromAttribute.g.cs`: The marker attribute injected by `RegisterPostInitializationOutput`.
-- `Greeter.HelloFrom.g.cs`: The generated partial class with the `HelloFrom()` method.
+- `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. **Predicate runs on every node.** The compiler calls `IsCandidateClass` on every syntax node in the compilation. Because it only checks `AttributeLists.Count > 0`, this step is fast.
-1. **Transform runs on candidates.** `GetSemanticTarget` runs only on nodes that passed the predicate. It uses the semantic model—a more expensive operation—but runs on a much smaller set of nodes.
-1. **Caching eliminates redundant work.** The compiler caches results at each stage. If a file that doesn't contain `[HelloFrom]` changes, the generator doesn't rerun the `Execute` step.
-1. **Output runs only when inputs change.** The `Execute` method runs only when the list of matching classes changes, or when the compilation itself changes.
+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
-Here are some ideas for extending this generator:
+Try extending this generator or exploring other source generator patterns:
-- Generate additional members (properties, methods) based on attribute arguments.
-- Use `ForAttributeWithMetadataName` for a more concise attribute filter (available in `Microsoft.CodeAnalysis` 4.3.1 and later).
-- Add diagnostic reporting with `SourceProductionContext.ReportDiagnostic` for error cases.
+- 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: