Roslyn, Analyzers and Source Generators

Roslyn is the C# compiler, and it is also a library (Microsoft.CodeAnalysis.CSharp) that exposes every stage of compilation:

Concept

The Roslyn Compiler Platform Roslyn is the C# compiler, and it is also a library (Microsoft.CodeAnalysis.CSharp) that exposes every stage of compilation:

Syntax tree The exact structure of the source: nodes (ClassDeclarationSyntax), tokens, trivia (whitespace, comments). Immutable Syntax trees + references + options Semantic model

Meaning: what type an expression has, which method a call binds to

INamedTypeSymbol, IMethodSymbol, IPropertySymbol — declared entities Errors and warnings (CS0219)

Code that inspects syntax/semantics and reports custom diagnostics in the IDE and build (e.g. the.NET code-quality CAxxxx rules) Code fixes

Suggested edits that resolve a diagnostic (the IDE light bulb) Source generators

Code that runs during compilation and adds new source files

A generator is a class implementing IIncrementalGenerator in a netstandard2.0 library referenced as an analyzer. Incremental generators build a pipeline (context.SyntaxProvider.ForAttributeWithMetadataName(...)) that the compiler caches, so the IDE stays fast. They can only add code, never modify existing code. The hooks are partial classes (one type split across files) and partial methods/properties: the user declares static partial Regex Vin(); and the generator supplies the body..NET ships several generators you use every day: [GeneratedRegex] (Module 43), [LibraryImport] (Module 42), System.Text.Json JsonSerializerContext (Module 28), [LoggerMessage] logging (Module 45). Benefits over reflection: no run-time cost, AOT/trimming friendly, errors at compile time, and the generated code is inspectable (<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> or "Go to definition" in the IDE).

flowchart LR
    SRC["Source text"] --> PARSE["Parser"]
    PARSE --> TREE["Syntax tree"]
    TREE --> COMP["Compilation + references"]
    COMP --> SEM["Semantic model / symbols"]
    SEM --> DIAG["Diagnostics"]
    SEM --> EMIT["Emit IL assembly"]
    TREE --> GEN["Source generators"]
    GEN --> COMP

Why It Matters

Analyzers enforce team rules (e.g. "no public fields", "every CAN signal must have a unit") on every build. Generators remove boilerplate — DTOs, signal decoders generated from a DBC file, ToString/equality members — without reflection's cost.

Syntax

SyntaxTree tree = CSharpSyntaxTree.ParseText(code);
var compilation = CSharpCompilation.Create("Demo", [tree], references, options);
SemanticModel model = compilation.GetSemanticModel(tree);
IMethodSymbol? m = model.GetDeclaredSymbol(methodSyntax);
[Generator] public class MyGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext ctx) { ... } }

Example

39.1 Syntax trees, semantic model, diagnostics, a custom rule and generated code

This program references the Roslyn NuGet package (#:package [email protected]). It parses a class, asks the semantic model about a method, lists the compiler's own diagnostics, applies a custom "analyzer" rule, and generates a partial class part — the core loop of an analyzer and a source generator.

39.2 A source generator you already use: [GeneratedRegex]

#:package [email protected]
#:property PublishAot=false
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

string code = """
    public partial class Ecu
    {
        public string Code { get; set; }
        public int Address;
        public void Reset() { int unused = 5; }
        public int Read(int did) => did * 2;
    }
    """;

// 1. Syntax tree: the structure of the source text
SyntaxTree tree = CSharpSyntaxTree.ParseText(code);
CompilationUnitSyntax root = tree.GetCompilationUnitRoot();
foreach (var m in root.DescendantNodes().OfType<MemberDeclarationSyntax>())
    Console.WriteLine($"syntax: {m.Kind(),-28} {Describe(m)}");

// 2. Compilation + semantic model: types and symbols
var compilation = CSharpCompilation.Create("Demo", [tree],
    [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)],
    new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
SemanticModel model = compilation.GetSemanticModel(tree);
var readMethod = root.DescendantNodes().OfType<MethodDeclarationSyntax>().First(m => m.Identifier.Text == "Read");
IMethodSymbol symbol = model.GetDeclaredSymbol
…
using System.Text.RegularExpressions;

// Source generators already built into .NET:
// [GeneratedRegex] writes the regex matching code at compile time (no run-time regex parsing/compilation)
Console.WriteLine($"VIN valid: {Patterns.Vin().IsMatch("EHBAUREV250000001")}");
Console.WriteLine($"DTCs: {string.Join(" ", Patterns.Dtc().Matches("P0A80 and U0100").Select(m => m.Value))}");
Console.WriteLine($"generated type: {Patterns.Vin().GetType().Name}");

static partial class Patterns
{
    [GeneratedRegex(@"^[A-HJ-NPR-Z0-9]{17}$")]
    public static partial Regex Vin();

    [GeneratedRegex(@"\b[PCBU][0-3][0-9A-F]{3}\b", RegexOptions.IgnoreCase)]
    public static partial Regex Dtc();
}

Expected Result

The compiler's CS0219 warning was obtained through the Roslyn API itself. The regex type name shows code that did not exist in the source file — the generator wrote it during compilation.

syntax: ClassDeclaration             Ecu
syntax: PropertyDeclaration          string Code
syntax: FieldDeclaration             int Address
syntax: MethodDeclaration            void Reset(0 params)
syntax: MethodDeclaration            int Read(1 params)
symbol: Ecu.Read(int) returns int, containing type Ecu
diagnostic: CS0219 The variable 'unused' is assigned but its value is never used at line 5
custom rule EH001: public field 'Address' should be a property
generated source:
public partial class Ecu
{
    public override string ToString() => $"Ecu(Code={Code})";
}
compiles after adding the generated part: True
VIN valid: True
DTCs: P0A80 U0100
generated type: <RegexGenerator_g>F07CB966D648F041A892E05839B832DFECF42601134FA50C56D0795C07F600B37__Vin_0

Common Mistakes

Writing a non-incremental (ISourceGenerator) generator — slow in the IDE; use IIncrementalGenerator.

Targeting net10.0 in the generator project — generators must target netstandard2.0.

Forgetting partial on the type or member that the generator completes.

Doing expensive semantic work for every syntax node instead of filtering early (ForAttributeWithMetadataName).

Generating code that depends on reflection, which defeats the purpose.

Best Practices

Prefer built-in generators (GeneratedRegex, LibraryImport, JSON source generation, LoggerMessage) over reflection.

Drive custom generators with attributes ([GenerateDecoder]) and keep them incremental.

Unit-test analyzers and generators with CSharpCompilation exactly like this example.

Ship team analyzers as a NuGet package and set severities in .editorconfig.

Interview Relevance

Medium (high for tooling/platform roles): "what is Roslyn?", "syntax tree vs semantic model?", "analyzer vs source generator?", "why incremental generators?", "why are source generators better than reflection for AOT?".

Practice Questions

Use Roslyn to count methods and lines of code per class in a source file.

Write a rule that reports methods longer than 30 lines.

Generate an Equals/GetHashCode pair for a class from its properties.

Inspect the generated code of a [GeneratedRegex] with EmitCompilerGeneratedFiles.

Quick Quiz

Q1. (Multiple choice) Which object tells you the *type* of an expression? A) Syntax tree B) Semantic model C) Syntax token D) Trivia Answer: B.

Q2. (True/False) A source generator can modify existing methods in your code.

Answer: False. It can only add new source; the pattern is partial declarations completed by generated code.

Q3. (Predict the output) The parsed class contains int unused = 5; inside a method. What does compilation.GetDiagnostics() report?

Answer: CS0219 The variable 'unused' is assigned but its value is never used (verified).

Q4. (Find the bug) A generator adds public partial class Ecu { ... } but the build fails with a duplicate-definition error.

Answer: The user's Ecu declaration is not marked partial: CS0260 *Missing partial modifier on declaration of type 'Ecu'* (verified).

Coding Challenge

Use Roslyn to list every method in a source string together with its number of parameters and whether it is async.

#:package [email protected]
#:property PublishAot=false
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

string code = """
    class Tester
    {
        public async Task FlashAsync(string file, int block) { await Task.Delay(1); }
        public bool Check(double v) => v > 12;
        void Reset() { }
    }
    """;

var root = CSharpSyntaxTree.ParseText(code).GetCompilationUnitRoot();
foreach (var m in root.DescendantNodes().OfType<MethodDeclarationSyntax>())
{
    bool isAsync = m.Modifiers.Any(SyntaxKind.AsyncKeyword);
    Console.WriteLine($"{m.Identifier.Text,-12} params {m.ParameterList.Parameters.Count}  async {isAsync}");
}