BrandGhost
Roslyn Analyzers vs Incremental Source Generators: Understanding the Pipeline

Roslyn Analyzers vs Incremental Source Generators: Understanding the Pipeline

When building compile-time tooling in .NET 10, two Roslyn-based extension points are available: Roslyn analyzers and incremental source generators. The question of Roslyn analyzers vs incremental source generators -- what each one does, when to reach for one over the other, and whether they can coexist in the same NuGet package -- comes up almost immediately once you start extending the compiler. The short answer is yes, they coexist by design, and understanding why is the key to building powerful compile-time tooling.

Analyzers and incremental source generators both tap into the Roslyn compilation pipeline -- but at different stages and for entirely different purposes. An analyzer reads your code and reports problems. A source generator reads your code and creates new code. Conflating the two leads to confused designs: trying to emit code from an analyzer (you can't), or trying to report diagnostics from a source generator (you mostly can't). Getting the mental model right from the start saves a lot of backtracking.

This guide is for developers who have explored both APIs and want a precise mental model for selecting the right tool -- including cases where the right answer is to use both at the same time. We will cover the compilation pipeline, the responsibilities of each API, a decision framework, a realistic "both together" example, the performance implications of the incremental design, and the migration path from the V1 ISourceGenerator interface to the modern IIncrementalGenerator in .NET 10.

If you want a broader overview of the analyzer side of this toolchain, the Roslyn Analyzers complete guide covers the full picture from attribute design through NuGet packaging.

The Roslyn Compilation Pipeline

To understand where analyzers and generators fit, you need to understand the pipeline they hook into. The Roslyn compiler does not treat your .cs files as a monolithic blob. It works in discrete phases, and each phase produces outputs that the next phase consumes.

Parsing is the first phase. Roslyn reads your source files and produces a syntax tree -- an immutable, structured representation of the text. At this stage, Roslyn does not know what any symbol means. List<string> is just a node in a tree; the fact that it is a generic type from System.Collections.Generic is not yet established.

Binding is the second phase. Roslyn walks the syntax trees and resolves all the names. Each identifier gets connected to a symbol -- a type, a method, a property, or a namespace. The output is a semantic model: a queryable map of what every symbol in your code actually refers to.

Analysis runs after binding. This is where DiagnosticAnalyzer implementations execute. They receive symbol and syntax information through registered callbacks, and they produce Diagnostic objects that surface in the IDE as warnings, errors, or informational messages. Analyzers are read-only participants at this stage. They cannot change the compilation.

Source generation runs after analysis but before emission. IIncrementalGenerator implementations execute here. They receive a snapshot of the compilation and can add new source files to it. The final compilation is the union of your original files plus everything generators added.

Emission is the final phase. Roslyn produces the binary output -- the .dll or .exe -- from the complete compilation. At this point, both analyzers and generators have finished their work.

The critical architectural point: analyzers hook into the analysis phase and cannot touch emission. Generators hook into a synthetic pre-emission phase and add inputs to the compilation. In practice, analyzers stay entirely in the analysis phase and generators stay in the pre-emission phase -- crossing those boundaries is technically possible in limited ways but is strongly discouraged by the Roslyn team, and pairing dedicated tools is always the cleaner design.

What Analyzers Do

A Roslyn analyzer is an implementation of DiagnosticAnalyzer. Its entire purpose is to read the compilation and report findings. It never adds code, never modifies symbols, and never produces output that the compiler emits.

Here is the minimal structure of an analyzer that checks whether a method decorated with a custom [MustBeAsync] attribute actually has an async modifier:

using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class MustBeAsyncAnalyzer : DiagnosticAnalyzer
{
    private static readonly DiagnosticDescriptor Rule = new(
        id: "DL0001",
        title: "Method marked [MustBeAsync] must be async",
        messageFormat: "Method '{0}' is marked [MustBeAsync] but is not declared async",
        category: "Design",
        defaultSeverity: DiagnosticSeverity.Error,
        isEnabledByDefault: true);

    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
        ImmutableArray.Create(Rule);

    public override void Initialize(AnalysisContext context)
    {
        // Required: opt-in to concurrent analysis and generated code analysis
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
        context.EnableConcurrentExecution();
        context.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method);
    }

    private static void AnalyzeMethod(SymbolAnalysisContext context)
    {
        var method = (IMethodSymbol)context.Symbol;

        var hasAttribute = method.GetAttributes().Any(a =>
            a.AttributeClass?.Name == "MustBeAsyncAttribute");

        if (hasAttribute && !method.IsAsync)
        {
            var diagnostic = Diagnostic.Create(
                Rule,
                method.Locations.FirstOrDefault() ?? Location.None,
                method.Name);
            context.ReportDiagnostic(diagnostic);
        }
    }
}

The analyzer registers a callback on SymbolKind.Method. Roslyn calls it once per method symbol during the analysis phase. The callback checks the attribute, checks the modifier, and reports a diagnostic if the constraint is violated. There is no output beyond diagnostics.

Analyzers pair naturally with CodeFixProvider implementations, which let you offer automated corrections for the diagnostics you raise. The code fix runs in the IDE as an edit action and does not affect the binary output. For a full walkthrough of this pattern, the guide to building an analyzer covers everything from scaffolding to packaging.

To summarize the scope of a DiagnosticAnalyzer:

  • Reports: warnings, errors, and informational messages via DiagnosticDescriptor
  • Pairs with: CodeFixProvider for automated IDE corrections
  • Registers: symbol, syntax node, operation, and compilation event callbacks
  • Runs: concurrently when EnableConcurrentExecution() is called
  • Cannot: add source files to the compilation
  • Cannot: modify existing syntax trees or symbols
  • Cannot: change what the compiler emits to the .dll

What Incremental Source Generators Do

An incremental source generator implements IIncrementalGenerator. It participates in the compilation from the opposite angle: instead of inspecting the code that already exists, it synthesizes new code and adds it to the compilation before emission.

Here is a minimal incremental generator that finds every class decorated with a [GenerateFactory] attribute and emits a factory method for it:

using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;

[Generator]
public sealed class FactoryGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        // Step 1: Build a pipeline that selects only the relevant syntax nodes
        IncrementalValuesProvider<ClassDeclarationSyntax> classDeclarations =
            context.SyntaxProvider
                .ForAttributeWithMetadataName(
                    "GenerateFactoryAttribute",
                    predicate: static (node, _) => node is ClassDeclarationSyntax,
                    transform: static (ctx, _) => (ClassDeclarationSyntax)ctx.TargetNode)
                .Where(static c => c is not null)!;

        // Step 2: Combine with the compilation for semantic information
        IncrementalValueProvider<(Compilation, ImmutableArray<ClassDeclarationSyntax>)> combined =
            context.CompilationProvider.Combine(classDeclarations.Collect());

        // Step 3: Register the source output
        context.RegisterSourceOutput(combined, static (spc, source) =>
        {
            var (comp, classes) = source;
            foreach (var cls in classes)
            {
                var model = comp.GetSemanticModel(cls.SyntaxTree);
                var symbol = model.GetDeclaredSymbol(cls) as INamedTypeSymbol;
                if (symbol is null) { continue; }

                var generatedSource = GenerateFactory(symbol);
                spc.AddSource(
                    $"{symbol.Name}_Factory.g.cs",
                    SourceText.From(generatedSource, Encoding.UTF8));
            }
        });
    }

    private static string GenerateFactory(INamedTypeSymbol symbol) =>
        $$"""
        public static class {{symbol.Name}}Factory
        {
            public static {{symbol.Name}} Create() => new {{symbol.Name}}();
        }
        """;
}

The key structural difference from an analyzer: context.RegisterSourceOutput calls spc.AddSource(...), which injects a new .cs file into the compilation. The compiler treats that generated file exactly like a hand-written one. The generator never calls ReportDiagnostic on the main SourceProductionContext -- that is not its job.

There is one exception worth knowing. Source generators can use PostInitializationContext to emit marker attribute source during generator initialization. Diagnostics via SourceProductionContext.ReportDiagnostic are technically available but are generally discouraged -- analyzers are the correct tool for diagnostic reporting.

To summarize the scope of an IIncrementalGenerator:

  • Synthesizes: new .cs source files injected into the compilation via AddSource
  • Pipeline-based: uses SyntaxValueProvider and IncrementalValueProvider for incremental caching
  • Cannot: reliably report IDE diagnostics (use a paired analyzer for that)
  • Operates: before emission -- generated code becomes part of the compiled binary
  • Replaces: the obsolete ISourceGenerator API (deprecated starting with .NET 7 / Roslyn 4.3+)

Decision Framework: Analyzer or Generator?

The choice between an incremental generator vs analyzer Roslyn almost always comes down to whether you need to produce new code or just inspect existing code. The table below captures the practical breakdown:

Concern Analyzer Generator Both
Enforce a naming convention
Detect a missing required attribute
Emit boilerplate from an attribute
Warn when generated code is misused
Replace runtime reflection with compile-time code
Validate that a generator precondition is met
Generate a serialization or mapping method
Report a diagnostic on a specific syntax node
Enforce attribute + synthesize code from it

The practical rule of thumb: if the user needs to see a warning or error in their editor, use an analyzer. If the user needs code that does not yet exist in their project to appear at compile time, use a generator. If you need both -- usually because your generator requires a specific precondition and you want meaningful error messages when that precondition is missing -- use both.

Source generators are also the right answer when comparing the Roslyn compilation pipeline against source generators vs reflection. Where runtime reflection inspects types and members at execution time, a source generator does the same work at compile time -- with zero runtime overhead.

When You Need Both

The most common "use both" scenario is the attribute-driven pattern. A generator discovers marked types and synthesizes code for them. An analyzer validates that the types satisfy the constraints the generator requires.

Consider a [Singleton] attribute that tells a generator to create DI registration extensions. For the generator to produce correct code, the decorated class must be partial and must not be abstract. An analyzer can report a clear, actionable diagnostic when either constraint is violated -- before the generator even tries to process the type.

// Analyzer: enforces that [Singleton] types are partial and not abstract
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class SingletonAttributeAnalyzer : DiagnosticAnalyzer
{
    private static readonly DiagnosticDescriptor MustBePartial = new(
        "DL0010",
        "[Singleton] classes must be partial",
        "Class '{0}' is marked [Singleton] but is not declared partial",
        "Design",
        DiagnosticSeverity.Error,
        isEnabledByDefault: true);

    private static readonly DiagnosticDescriptor MustNotBeAbstract = new(
        "DL0011",
        "[Singleton] classes must not be abstract",
        "Class '{0}' is marked [Singleton] but is abstract",
        "Design",
        DiagnosticSeverity.Error,
        isEnabledByDefault: true);

    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
        ImmutableArray.Create(MustBePartial, MustNotBeAbstract);

    public override void Initialize(AnalysisContext context)
    {
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
        context.EnableConcurrentExecution();
        context.RegisterSymbolAction(Analyze, SymbolKind.NamedType);
    }

    private static void Analyze(SymbolAnalysisContext ctx)
    {
        var type = (INamedTypeSymbol)ctx.Symbol;
        if (!type.GetAttributes().Any(a => a.AttributeClass?.Name == "SingletonAttribute"))
        {
            return;
        }

        var syntax = type.DeclaringSyntaxReferences
            .Select(r => r.GetSyntax())
            .OfType<ClassDeclarationSyntax>()
            .FirstOrDefault();

        var loc = type.Locations.FirstOrDefault() ?? Location.None;
        if (syntax is not null &&
            !syntax.Modifiers.Any(SyntaxKind.PartialKeyword))
        {
            ctx.ReportDiagnostic(Diagnostic.Create(
                MustBePartial, loc, type.Name));
        }

        if (type.IsAbstract)
        {
            ctx.ReportDiagnostic(Diagnostic.Create(
                MustNotBeAbstract, loc, type.Name));
        }
    }
}
// Generator: consumes [Singleton] and emits the DI registration extension
[Generator]
public sealed class SingletonRegistrationGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        var singletons = context.SyntaxProvider
            .ForAttributeWithMetadataName(
                "SingletonAttribute",
                predicate: static (n, _) => n is ClassDeclarationSyntax,
                transform: static (ctx, _) => ctx.TargetSymbol as INamedTypeSymbol)
            .Where(static s => s is not null)!;

        context.RegisterSourceOutput(singletons, static (spc, symbol) =>
        {
            var ns = symbol!.ContainingNamespace.ToDisplayString();
            var code = $$"""
                namespace {{ns}};

                public static partial class ServiceCollectionExtensions
                {
                    public static IServiceCollection Add{{symbol.Name}}(
                        this IServiceCollection services)
                        => services.AddSingleton<{{symbol.Name}}>();
                }
                """;

            spc.AddSource(
                $"{symbol.Name}_Registration.g.cs",
                SourceText.From(code, Encoding.UTF8));
        });
    }
}

This pattern gives users early, clear feedback -- "you must mark this class partial" -- before the generator runs. Without the analyzer, the generator would encounter the missing modifier and either fail silently or produce code that does not compile, neither of which gives the user useful information. With the analyzer in place, the Roslyn compilation pipeline surfaces a targeted error at the exact location of the problem, long before emission is attempted.

Why This Pattern Is Powerful

The combination delivers two guarantees that neither tool can provide alone.

First, compile-time type safety with zero reflection overhead. The generator synthesizes Add{ClassName} extension methods using the type's fully-qualified name extracted during compilation. The consuming project calls services.AddMyService() -- a strongly-typed method the IDE can autocomplete, rename-refactor, and navigate to definition. There is no typeof(MyService) resolved at runtime, no Assembly.GetTypes() scanning the assembly, and no string-based registration lookup. The type safety is absolute, and the runtime cost is zero because the binding happened at compile time.

Second, error messages that point to the right place. Without the analyzer, a user who forgets partial on their [Singleton] class sees either nothing (the generator silently skips them) or a confusing secondary error -- something like "missing method AddMyService" that points to the call site rather than the declaration. The analyzer changes this entirely. The diagnostic fires at the [Singleton] attribute on the class declaration, with a message that says exactly what is wrong: "Class 'MyService' is marked [Singleton] but is not declared partial." No guessing, no tracing through generated output.

This is the defining characteristic of well-designed Roslyn tooling: the analyzer owns the contract, and the source generator owns the output. The user experiences the combination as a single coherent feature -- a smart attribute that both validates and generates. The fact that two separate compilation-phase participants are cooperating behind the scenes is entirely invisible, and that invisibility is the goal.

The pattern scales naturally as the tooling grows. When you add new generator capabilities, you add new analyzer rules independently. The analyzer rules and the generator rules share marker attributes and utility code, but they are not coupled to each other's implementation logic. Each component stays focused, testable in isolation, and straightforward to extend without touching the other.

Performance: Incremental Pipeline Concepts

This is where IIncrementalGenerator earns its name. The V1 ISourceGenerator interface ran the generator's Execute method on every keystroke that changed the compilation -- even trivial edits like adding a space. In codebases with many attributed types, this meant constant CPU work, IDE sluggishness, and slow autocomplete responses.

The incremental API solves this by making the generator pipeline explicit and cacheable. Instead of one monolithic Execute call, you describe a graph of transformations. Roslyn tracks the inputs at each stage and only re-executes downstream steps when an upstream input actually changes.

The central tool is SyntaxValueProvider.ForAttributeWithMetadataName. This method tells Roslyn: "only give me syntax nodes that carry this specific attribute." Because it operates on syntax before full semantic binding, it is extremely fast to evaluate per keystroke. Roslyn skips the expensive semantic work entirely when the syntax filter does not match.

// Predicate: syntax-only check -- runs on every edit, must be fast
// Transform: semantic check -- only runs when predicate matches
var provider = context.SyntaxProvider
    .ForAttributeWithMetadataName(
        "MyAttributeAttribute",
        predicate: static (node, _) => node is ClassDeclarationSyntax,
        transform: static (ctx, _) =>
        {
            var symbol = ctx.SemanticModel.GetDeclaredSymbol(ctx.TargetNode)
                as INamedTypeSymbol;

            // Extract only what you need into a lightweight record
            return symbol is null
                ? null
                : new MyData(symbol.Name, symbol.ContainingNamespace.ToString());
        })
    .Where(static d => d is not null)!;

Caching requires that the outputs of each pipeline step implement IEquatable<T>. Roslyn uses equality checks to decide whether downstream steps need to re-run. If you pass raw ISymbol or SyntaxNode objects through the pipeline, you lose incremental caching entirely -- these objects change reference equality on every compilation snapshot, so every step always re-runs.

The correct approach is to transform semantic data into a plain C# record that implements IEquatable<T> as early in the pipeline as possible, and carry only that lightweight record through the rest of the stages.

// ✅ Correct: lightweight, IEquatable record flows through the pipeline
internal sealed record MyData(string ClassName, string Namespace); // Records synthesize IEquatable<T> automatically

// ❌ Incorrect: INamedTypeSymbol has reference equality -- defeats caching
IncrementalValuesProvider<INamedTypeSymbol> badProvider = ...; // every step re-runs

This caching model is what makes the incremental generator vs analyzer Roslyn comparison compelling at scale. The analyzer pipeline already fires per symbol registration and is designed for incremental use. The generator pipeline, when properly structured, pays its compilation cost only when something relevant actually changes. Both APIs reward careful attention to where expensive work happens and how its outputs are represented.

Migration Path: ISourceGenerator to IIncrementalGenerator in .NET 10

If you have a V1 generator (ISourceGenerator), migrating to IIncrementalGenerator is the right move for .NET 8, 9, and 10. The V1 interface is marked obsolete in .NET 7 / Roslyn 4.3+ and produces build warnings. The migration follows a consistent pattern.

V1 pattern (obsolete):

[Generator]
public sealed class OldGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        context.RegisterForSyntaxNotifications(() => new MySyntaxReceiver());
    }

    public void Execute(GeneratorExecutionContext context)
    {
        if (context.SyntaxReceiver is not MySyntaxReceiver receiver) { return; }

        foreach (var candidate in receiver.Candidates)
        {
            var model = context.Compilation.GetSemanticModel(candidate.SyntaxTree);
            // ... process candidate ...
            context.AddSource("generated.g.cs", "// generated code");
        }
    }
}

Incremental pattern (current for .NET 10):

[Generator]
public sealed class NewGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        // Replace ISyntaxReceiver with a SyntaxValueProvider pipeline
        var targets = context.SyntaxProvider
            .ForAttributeWithMetadataName(
                "MyAttributeAttribute",
                predicate: static (n, _) => n is ClassDeclarationSyntax,
                transform: static (ctx, ct) =>
                {
                    var symbol = ctx.SemanticModel
                        .GetDeclaredSymbol(ctx.TargetNode, ct) as INamedTypeSymbol;

                    // Return an IEquatable record -- not the raw symbol
                    return symbol is null
                        ? null
                        : new GeneratorTarget(
                            symbol.Name,
                            symbol.ContainingNamespace.ToDisplayString());
                })
            .Where(static t => t is not null)!;

        // RegisterSourceOutput replaces Execute
        context.RegisterSourceOutput(targets, static (spc, target) =>
        {
            spc.AddSource(
                $"{target!.ClassName}_Generated.g.cs",
                SourceText.From($"// Generated for {target.ClassName}", Encoding.UTF8));
        });
    }
}

// IEquatable record carries data between pipeline steps
internal sealed record GeneratorTarget(string ClassName, string Namespace);

The migration steps in order:

  1. Replace ISourceGenerator with IIncrementalGenerator -- note the signature of Initialize changes completely
  2. Move syntax selection from ISyntaxReceiver to a SyntaxValueProvider pipeline using ForAttributeWithMetadataName where possible
  3. Replace the Execute body with RegisterSourceOutput
  4. Extract semantic data into IEquatable records at the transform step -- never pass raw symbols downstream
  5. Remove any GeneratorInitializationContext overloads -- the new context type is IncrementalGeneratorInitializationContext

For generators that do not use attributes as the selection criterion, SyntaxProvider.CreateSyntaxProvider is the alternative. It takes a predicate and a transform, giving you the same two-phase execution model. The predicate runs per keystroke; the transform runs only when the predicate passes. The IEquatable record rule applies equally in both cases.

FAQ

What is the difference between ISourceGenerator and IIncrementalGenerator in Roslyn?

ISourceGenerator is an older interface where the entire Execute method ran on every compilation change. IIncrementalGenerator is the modern API that exposes an explicit pipeline of transformations with built-in caching. Roslyn tracks which pipeline steps produce new outputs and only re-executes downstream work when an upstream input actually changes. ISourceGenerator is deprecated starting with .NET 7 / Roslyn 4.3+ -- new generators should always implement IIncrementalGenerator to avoid IDE performance regressions.

Can a Roslyn source generator report diagnostics?

Technically yes -- SourceProductionContext.ReportDiagnostic exists. In practice, it is generally considered a bad pattern. Diagnostics from generators fire later in the pipeline than diagnostics from analyzers, they can appear at unexpected locations in the IDE, and they are harder for users to navigate to a fix. The recommended pattern is to pair a dedicated DiagnosticAnalyzer with your generator. The analyzer handles all diagnostic reporting through the normal analysis phase; the generator handles all code synthesis during the pre-emission phase. Each tool stays in its lane.

When should I use both a Roslyn analyzer and a source generator in the same package?

Use both when your generator has preconditions that, if violated, produce confusing failures. If your generator requires a decorated class to be partial and the user omits that modifier, the generator encounters an impossible situation -- it can either skip the type silently or emit malformed code. Neither gives the user actionable feedback. An analyzer that fires earlier in the pipeline can detect the missing partial modifier and report a precise error before the generator's output causes confusion. The analyzer and generator ship in the same NuGet package and share the marker attribute, but they operate in entirely separate compilation phases.

Does the incremental generator vs analyzer Roslyn distinction affect IDE performance?

Yes -- significantly. Analyzers that perform expensive semantic analysis on broad symbol callbacks can make the IDE sluggish on large solutions. The same is true for source generators that bypass ForAttributeWithMetadataName (which performs cheap syntax-level filtering) or that pass non-IEquatable objects through the pipeline, defeating the incremental caching mechanism. Both APIs have performance pitfalls, but IIncrementalGenerator makes correct caching an explicit design concern rather than an afterthought. Scoping analyzer callbacks to specific SymbolKind values and enabling context.EnableConcurrentExecution() are the corresponding discipline for the analyzer side.

Is it possible to share logic between a Roslyn analyzer and a source generator?

Yes. Both live in the same analyzer assembly, referenced in consuming projects via <PackageReference> with the OutputItemType="Analyzer" and ReferenceOutputAssembly="false" attributes. You can share utility classes and attribute-parsing logic between them. A common pattern is a shared AttributeDataExtractor or ModelExtractor class that both the analyzer and the generator use to read data from the custom attribute. The analyzer uses that extracted data to validate constraints and report diagnostics; the generator uses it to synthesize code. Neither knows about the other -- they are just two independent consumers of shared infrastructure.

Conclusion

The incremental generator vs analyzer Roslyn split comes down to a single question: are you reading code or writing code? Analyzers read -- they inspect the compilation, report findings, and stay entirely out of the emission path. Incremental source generators write -- they synthesize new code and inject it into the compilation before the binary is produced.

The Roslyn compilation pipeline makes this division explicit. Analyzers participate in the analysis phase. Generators participate in a pre-emission phase. In practice, analyzers stay entirely in the analysis phase and generators stay in the pre-emission phase -- crossing those boundaries is technically possible in limited ways but is strongly discouraged by the Roslyn team, and pairing dedicated tools is always the cleaner design.

When both belong in the same package, the pattern is consistent: the analyzer enforces preconditions and reports diagnostics; the generator assumes those preconditions are met and produces clean output. This gives users the best of both -- meaningful error messages when something is wrong, and zero-overhead generated code when everything is right. Understanding that collaboration is the key insight that separates developers who build clean Roslyn tooling from developers who fight their own tools.

For a broader look at structuring a real analyzer from scratch, including packaging and distribution, see the Roslyn Analyzers complete guide.


Disclaimer: This article was written with AI assistance, by yours truly, Nick Cosentino, the Dev Leader. I have ensured that it represents my thoughts and perspectives on Roslyn analyzers vs incremental source generators.

How C# Source Generators Work: The Roslyn Compilation Pipeline Explained

Learn exactly how C# source generators work inside the Roslyn compilation pipeline. Understand the two-phase compilation model, syntax providers, and incremental execution.

Incremental Source Generators in C#: The Right Way to Build Them

Learn the incremental source generator API in C# -- covers pipeline design, caching with IEquatable, performance tips, and best practices for modern .NET.

Roslyn Analyzers in C#: The Complete Guide

Learn what roslyn analyzers are, how they work in the .NET 10 compiler pipeline, why teams build custom diagnostics, and how to write your first rule in C#.

An error has occurred. This application may no longer respond until reloaded. Reload