-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Proposal: Two-Phase Incremental Generators for Cross-Generator Dependencies #81395
Description
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
-
Nested Generator Execution - Invoke one generator from within another
- Only works for generators you control
- Violates independence principles
- Creates tight coupling
-
Heuristic Type Guessing - Assume type shapes and allow errors
- Poor developer experience
- Cryptic error messages
- Breaks on version changes
-
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
- Create initial
Compilationfrom user code - Execute all
RegisterDeclarationOutputactions across all generators - Collect generated declaration sources (partial types, signatures)
- Create
EnrichedCompilation= initial compilation + declaration sources - Provide
EnrichedCompilationto Phase 2
Phase 2: Implementation Phase
- Use
EnrichedCompilationthat includes all generator declarations - Execute all
RegisterImplementationSourceOutputandRegisterSourceOutputactions - Each generator can now see types declared by other generators
- 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
- No explicit ordering required - Eliminates MSBuild Before/After complexity
- Better incremental compilation - Implementation changes don't cascade unnecessarily
- Improved developer experience - Fewer cryptic compilation errors
- Framework interoperability - Enables rich ecosystem of composable generators
- Backward compatible - Existing generators continue to work unchanged
- 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
- Naming:
RegisterDeclarationOutputvsRegisterSignatureOutput? - Granularity: Should declaration vs implementation be per-generator or per-output?
- Diagnostics: How should errors in Phase 1 affect Phase 2 execution?
- Incremental behavior: What level of caching between phases?
- Strictness: Should the limitation to produce just declarations in Phase 1 be enforced or recommended?
- Existing RegisterSourceOutput: Should it continue to work in Phase 2, or be deprecated in favor of explicit declaration/implementation split?
Prior Art
- Roslyn Issue #57589 - Earlier two-phase proposal discussion (October 2020)
- Current
RegisterImplementationSourceOutput- Already distinguishes implementation from other outputs - Source Generators Cookbook
- Incremental Generators
Implementation Considerations
This proposal would require:
- Extensions to
IncrementalGeneratorInitializationContextAPI - New compilation pipeline stages in the generator driver
- Updates to incremental compilation caching strategy to track declaration vs implementation changes
- Generator testing infrastructure updates to support two-phase execution
- Documentation and migration guidance for generator authors
- 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