Skip to content

Proposal: Two-Phase Incremental Generators for Cross-Generator Dependencies #81395

@StephaneDelcroix

Description

@StephaneDelcroix

Two-Phase Incremental Generators

Summary

This proposal extends the existing incremental generator infrastructure to support a two-phase compilation model. In this model, generators can emit type declarations (signatures) in phase one, and full implementations in phase two. This enables generators to reference types produced by other generators, solving cross-generator dependency issues that currently require complex workarounds.

Motivation

Modern .NET applications commonly use multiple source generators that need to reference types created by other generators. The current limitation where generators cannot see each other's outputs creates significant problems for framework and library authors.

Real-World Problem: .NET MAUI

A typical .NET MAUI application uses multiple generators:

  • XAML compiler - generates code-behind
  • CommunityToolkit.Mvvm - generates observable properties and commands
  • MAUI Community Toolkit - generates behaviors and converters
  • Blazor - generates component code

When XAML code-behind needs to reference an MVVM-generated property, compilation fails because the XAML generator runs without visibility into the MVVM generator's output:

// CommunityToolkit.Mvvm generates this property
public partial class MainViewModel : ObservableObject
{
    [ObservableProperty]
    private string _title;
    // Generates: public string Title { get; set; }
}

// XAML generator tries to reference it - FAILS
public partial class MainPage : ContentPage
{
    public MainPage()
    {
        BindingContext = new MainViewModel();
        BindingContext.Title = "Hello";  // ERROR: Title doesn't exist yet
    }
}

Current Workarounds

  1. Nested Generator Execution - Invoke one generator from within another

    • Only works for generators you control
    • Violates independence principles
    • Creates tight coupling
  2. Heuristic Type Guessing - Assume type shapes and allow errors

    • Poor developer experience
    • Cryptic error messages
    • Breaks on version changes
  3. MSBuild Ordering - Use Before/After properties

    • Requires knowing all generators
    • Breaks with transitive dependencies
    • Forces full recompilation
    • No circular dependency resolution

Proposed Solution

Introduce a two-phase compilation model where generators declare types in phase one and implement them in phase two.

API Design

public void Initialize(IncrementalGeneratorInitializationContext context)
{
    // Phase 1: Declare types and signatures
    // These are visible to other generators in Phase 2
    context.RegisterDeclarationOutput(
        source: ...,
        action: (context, source) => 
        {
            // Emit partial types, method signatures, property declarations
            // No implementation bodies
        });
    
    // Phase 2: Provide implementations
    // Can see all declarations from Phase 1
    context.RegisterImplementationSourceOutput(
        source: ...,
        action: (context, source) => 
        {
            // Emit method bodies, complete implementations
            // Has access to enriched compilation including all declarations
        });
}

Execution Model

Phase 1: Declaration Phase

  1. Create initial Compilation from user code
  2. Execute all RegisterDeclarationOutput actions across all generators
  3. Collect generated declaration sources (partial types, signatures)
  4. Create EnrichedCompilation = initial compilation + declaration sources
  5. Provide EnrichedCompilation to Phase 2

Phase 2: Implementation Phase

  1. Use EnrichedCompilation that includes all generator declarations
  2. Execute all RegisterImplementationSourceOutput and RegisterSourceOutput actions
  3. Each generator can now see types declared by other generators
  4. Produce final compilation output

Concrete Example

Phase 1 - CommunityToolkit.Mvvm declares property:

context.RegisterDeclarationOutput(source, (ctx, data) => {
    ctx.AddSource("MainViewModel.g.cs", @"
    public partial class MainViewModel : ObservableObject
    {
        public string Title { get; set; } // Declaration only
    }");
});

Phase 2 - MAUI XAML generator sees and uses it:

context.RegisterImplementationSourceOutput(source, (ctx, data) => {
    // Semantic model now includes Title property from Phase 1
    var viewModel = compilation.GetTypeByMetadataName("MainViewModel");
    var titleProp = viewModel.GetMembers("Title").FirstOrDefault();
    
    ctx.AddSource("MainPage.g.cs", @"
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            BindingContext = new MainViewModel();
            BindingContext.Title = ""Hello""; // ✓ Compiles successfully
        }
    }");
});

Phase 2 - CommunityToolkit.Mvvm implements property:

context.RegisterImplementationSourceOutput(source, (ctx, data) => {
    ctx.AddSource("MainViewModel.g.cs", @"
    public partial class MainViewModel : ObservableObject
    {
        private string _title;
        public string Title 
        { 
            get => _title;
            set => SetProperty(ref _title, value); // Implementation
        }
    }");
});

Design Principles

Determinism: Phase 1 cannot access other generators' declarations, preventing circular dependencies and non-deterministic behavior.

Performance: Only declaration changes trigger Phase 2 invalidation. Implementation-only changes don't cascade to dependent generators.

Compatibility: Existing single-phase generators continue to work. The two-phase model is opt-in via new registration methods.

Scalability: Generators don't need to know about each other. The runtime manages visibility automatically.

Benefits

  1. No explicit ordering required - Eliminates MSBuild Before/After complexity
  2. Better incremental compilation - Implementation changes don't cascade unnecessarily
  3. Improved developer experience - Fewer cryptic compilation errors
  4. Framework interoperability - Enables rich ecosystem of composable generators
  5. Backward compatible - Existing generators continue to work unchanged
  6. Prevents circular dependencies - Phase 1 isolation ensures deterministic compilation

Comparison with Existing APIs

API Phase Visibility Current Behavior
RegisterPostInitializationOutput Pre-compilation N/A Global usings, attributes
RegisterDeclarationOutput (new) Phase 1 Initial compilation only Type signatures, partial declarations
RegisterImplementationSourceOutput (enhanced) Phase 2 Declarations from all generators Complete implementations
RegisterSourceOutput Phase 2 Declarations from all generators Both declaration and implementation

Open Questions

  1. Naming: RegisterDeclarationOutput vs RegisterSignatureOutput?
  2. Granularity: Should declaration vs implementation be per-generator or per-output?
  3. Diagnostics: How should errors in Phase 1 affect Phase 2 execution?
  4. Incremental behavior: What level of caching between phases?
  5. Strictness: Should the limitation to produce just declarations in Phase 1 be enforced or recommended?
  6. Existing RegisterSourceOutput: Should it continue to work in Phase 2, or be deprecated in favor of explicit declaration/implementation split?

Prior Art

Implementation Considerations

This proposal would require:

  1. Extensions to IncrementalGeneratorInitializationContext API
  2. New compilation pipeline stages in the generator driver
  3. Updates to incremental compilation caching strategy to track declaration vs implementation changes
  4. Generator testing infrastructure updates to support two-phase execution
  5. Documentation and migration guidance for generator authors
  6. Performance analysis to ensure phase separation doesn't introduce overhead

Related Issues

  • Cross-generator dependencies in .NET MAUI
  • Razor/Blazor component generation challenges
  • Third-party generator ecosystem limitations

Authors: .NET MAUI Team
Date: November 2025
Status: Proposal

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions