Skip to content

Commit 445357b

Browse files
committed
added source generator doc
1 parent dfc950d commit 445357b

File tree

1 file changed

+189
-0
lines changed

1 file changed

+189
-0
lines changed

docs/dotnet/source_generator.md

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
---
2+
title: Source Generators
3+
parent: .NET
4+
layout: default
5+
grand_parent: My Docs
6+
published: true
7+
---
8+
9+
### Source Generators in .Net Standard 2.0
10+
11+
#### How to write one ?
12+
13+
1. Create a blank solution
14+
2. Create a class libary in .Net Standard 2.0. Ex: `PrivateFieldGenerator.csproj`
15+
3. Update the `csproj` file with below things.
16+
```xml
17+
<LangVersion>latest</LangVersion>
18+
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
19+
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
20+
<IsRoslynComponent>true</IsRoslynComponent>
21+
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
22+
```
23+
4. Install `Microsoft.CodeAnalysis.CSharp` & `Microsoft.CodeAnalysis.Analyzers`
24+
5. Add a class file and name is `PrivateFieldIncrementalGenerator`. We will be using the `IIncrementalGenerator`
25+
6. Decorate the class with the `[GeneratorAttribute]`
26+
7. Implement the class like below
27+
```c#
28+
public class PrivateFieldGenerator : IIncrementalGenerator
29+
{
30+
private readonly ConcurrentDictionary<SyntaxTree, SemanticModel> _semanticModelCache = new();
31+
32+
/// <summary>
33+
/// The full name of the attribute this generator should process.
34+
/// Example: "MyNamespace.MyCustomAttribute"
35+
/// </summary>
36+
private string TargetAttributeFullName { get; }
37+
38+
public void Initialize(IncrementalGeneratorInitializationContext context)
39+
{
40+
IncrementalValuesProvider<(SyntaxNode Node, SemanticModel SemanticModel)> syntaxProvider = context.SyntaxProvider
41+
.ForAttributeWithMetadataName(
42+
TargetAttributeFullName,
43+
IsSyntaxTarget,
44+
GetSemanticTarget)
45+
.Where(m => m.Node is not null)
46+
.Select((m, _) => m!);
47+
48+
IncrementalValueProvider<(Compilation Left, ImmutableArray<(SyntaxNode Node, SemanticModel SemanticModel)> Right)> compilationAndNodes = context.CompilationProvider.Combine(syntaxProvider.Collect());
49+
50+
context.RegisterSourceOutput(compilationAndNodes, (spc, source) =>
51+
{
52+
ImmutableArray<(SyntaxNode Node, SemanticModel SemanticModel)> classes = [.. source.Right.Distinct()];
53+
if (classes.IsDefaultOrEmpty) return;
54+
Execute(source.Left, classes, spc);
55+
});
56+
}
57+
58+
/// <summary>
59+
/// Check if the node is a target for this generator. Ex: the generator should handle nodes that are classes only with a specific attribute.
60+
/// </summary>
61+
/// <param name="node"></param>
62+
/// <param name="cancellationToken"></param>
63+
/// <returns></returns>
64+
private bool IsSyntaxTarget(SyntaxNode node, CancellationToken cancellationToken)
65+
{
66+
return node is ClassDeclarationSyntax;
67+
}
68+
/// <summary>
69+
/// Get the semantic target for the node. Ex: get the class symbol for a class node.
70+
/// </summary>
71+
/// <param name="context"></param>
72+
/// <param name="cancellationToken"></param>
73+
/// <returns></returns>
74+
protected (SyntaxNode Node, SemanticModel SemanticModel) GetSemanticTarget(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken)
75+
{
76+
return (context.TargetNode as ClassDeclarationSyntax, context.SemanticModel);
77+
}
78+
79+
/// <summary>
80+
/// Execute the generator for the given compilation and nodes.
81+
/// </summary>
82+
/// <param name="compilation"></param>
83+
/// <param name="nodes"></param>
84+
/// <param name="context"></param>
85+
protected virtual void Execute(Compilation compilation, ImmutableArray<(SyntaxNode Node, SemanticModel SemanticModel)> nodes, SourceProductionContext context)
86+
{
87+
var filteredNodes = nodes.Where(_ => _.Node is ClassDeclarationSyntax).ToList();
88+
89+
if (filteredNodes.Count == 0)
90+
{
91+
// when attribute is set for member but not class then get the class.
92+
foreach ((SyntaxNode Node, SemanticModel SemanticModel) node in filteredNodes)
93+
{
94+
if (node.Node.Parent is ClassDeclarationSyntax cls)
95+
{
96+
filteredNodes.Add((cls, GetSemanticModel(compilation, cls.SyntaxTree)));
97+
}
98+
}
99+
}
100+
101+
foreach (var node in filteredNodes.Distinct())
102+
{
103+
if (node.SemanticModel.GetDeclaredSymbol(node.Node) is not INamedTypeSymbol classSymbol) continue;
104+
105+
var code = GenerateClass(node.Node as ClassDeclarationSyntax, node.SemanticModel, classSymbol, nodes);
106+
AddGeneratedSource(context, GetFileName(classSymbol), code);
107+
}
108+
}
109+
110+
protected void AddGeneratedSource(SourceProductionContext context, string fileName, string content)
111+
{
112+
if (System.Diagnostics.Debugger.IsAttached) // don't remove this. it's for debugging purposes.
113+
System.Diagnostics.Debugger.Break();
114+
115+
context.AddSource(fileName, SourceText.From(content, Encoding.UTF8));
116+
}
117+
118+
public string GenerateClass(ClassDeclarationSyntax classDeclarationSyntax, SemanticModel semanticModel, INamedTypeSymbol classSymbol, ImmutableArray<(SyntaxNode Node, SemanticModel SemanticModel)> matchedNodes)
119+
{
120+
string code = "build your code. You can use StringBuilder for better performance";
121+
return code;
122+
}
123+
124+
protected virtual string GetFileName(INamedTypeSymbol classSymbol)
125+
{
126+
return $"{GetClassName(classSymbol)}.g.cs";
127+
}
128+
129+
protected string GetClassName(INamedTypeSymbol classSymbol)
130+
{
131+
return classSymbol.Name;
132+
}
133+
134+
protected SemanticModel GetSemanticModel(Compilation compilation, SyntaxTree syntaxTree)
135+
{
136+
if (!_semanticModelCache.TryGetValue(syntaxTree, out var model))
137+
{
138+
model = compilation.GetSemanticModel(syntaxTree);
139+
_semanticModelCache.TryAdd(syntaxTree, model);
140+
}
141+
return model;
142+
}
143+
}
144+
```
145+
8. Done you are ready with your generator.
146+
147+
#### How to use this generator in your real project.
148+
149+
1. In this case, I am taking a class library to create a nuget but you can shoose a API or MVC or a WPF project too.
150+
2. What ever project you choose add below line to that `csproj` where you want this generator to work.
151+
152+
```xml
153+
<ItemGroup>
154+
<ProjectReference Include="path to .csproj file" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
155+
</ItemGroup>
156+
```
157+
158+
159+
#### How to Debug ?
160+
161+
I learnt from [here](https://github.com/JoanComasFdz/dotnet-how-to-debug-source-generator-vs2022)
162+
163+
1. Go to your source generator project and create a Properties Folder and then `launchSettings.json` with the below contents.
164+
```json
165+
{
166+
"profiles": {
167+
"Name of your generator": {
168+
"commandName": "DebugRoslynComponent",
169+
"targetProject": "path to .csproj file"
170+
}
171+
}
172+
}
173+
```
174+
2. Set your generator as a startup project in the visual studio and run it just like a console app.
175+
176+
Tips:
177+
178+
If debugger stops without even hitting your breakpoint, then
179+
1. Ensure your generator class has the `GeneratorAttribute`
180+
2. Ensure you are in in Debug mode.
181+
3. Try applying breakpoint in `Initialize`, `IsSyntaxTarget` and `GetSemanticTarget` in this given order.
182+
4. Most cases `IsSyntaxTarget` might be giving the empty results. In this case, ensure you have set the attribute on your class and you correctly set it for `TargetAttributeFullName`
183+
184+
Every change to the generator project you must close and open the solution (not VS) becuase `IIncrementalGenerator` is written to cache the generator logic and speed up the build time.
185+
186+
And if there is any c# error in the code the code won't be generated. So in that just debug the code, copy the generated source and keep in a new `.cs` file to find the error and fix it in source code generation logic.
187+
188+
189+
[My Sample Generators](https://github.com/DotNetExtended/Default)

0 commit comments

Comments
 (0)