diff --git a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/IntegrationTests/ComponentCodeGenerationTestBase.cs b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/IntegrationTests/ComponentCodeGenerationTestBase.cs
index cb969d332a5..dee594f9851 100644
--- a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/IntegrationTests/ComponentCodeGenerationTestBase.cs
+++ b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/IntegrationTests/ComponentCodeGenerationTestBase.cs
@@ -10097,6 +10097,122 @@ @namespace X
CompileToAssembly(generated, expectedDiagnostics);
}
+ [IntegrationTestFact]
+ public void Component_WithRef_AutoGeneratedField()
+ {
+ // Arrange
+ AdditionalSyntaxTrees.Add(Parse(@"
+using Microsoft.AspNetCore.Components;
+
+namespace Test
+{
+ public class MyComponent : ComponentBase
+ {
+ }
+}
+"));
+
+ // Arrange/Act - No manual field declaration required
+ var generated = CompileToCSharp(@"
+
+
+@code {
+ public void Foo() { System.GC.KeepAlive(myInstance); }
+}
+");
+
+ // Assert
+ AssertDocumentNodeMatchesBaseline(generated.CodeDocument);
+ AssertCSharpDocumentMatchesBaseline(generated.CodeDocument);
+ CompileToAssembly(generated);
+ }
+
+ [IntegrationTestFact]
+ public void Element_WithRef_AutoGeneratedField()
+ {
+ // Arrange/Act - No manual field declaration required
+ var generated = CompileToCSharp(@"
+Hello
+
+@code {
+ public void Foo() { System.GC.KeepAlive(myElem); }
+}
+");
+
+ // Assert
+ AssertDocumentNodeMatchesBaseline(generated.CodeDocument);
+ AssertCSharpDocumentMatchesBaseline(generated.CodeDocument);
+ CompileToAssembly(generated);
+ }
+
+ [IntegrationTestFact]
+ public void Component_WithRef_AutoGeneratedField_MultipleRefs()
+ {
+ // Arrange
+ AdditionalSyntaxTrees.Add(Parse(@"
+using Microsoft.AspNetCore.Components;
+
+namespace Test
+{
+ public class MyComponent : ComponentBase
+ {
+ }
+}
+"));
+
+ // Arrange/Act - Multiple refs should all be auto-generated
+ var generated = CompileToCSharp(@"
+
+
+
+
+@code {
+ public void Foo()
+ {
+ System.GC.KeepAlive(comp1);
+ System.GC.KeepAlive(comp2);
+ System.GC.KeepAlive(elem1);
+ }
+}
+");
+
+ // Assert
+ AssertDocumentNodeMatchesBaseline(generated.CodeDocument);
+ AssertCSharpDocumentMatchesBaseline(generated.CodeDocument);
+ CompileToAssembly(generated);
+ }
+
+ [IntegrationTestFact]
+ public void Component_WithRef_AutoGeneratedField_SkipsExistingField()
+ {
+ // Arrange
+ AdditionalSyntaxTrees.Add(Parse(@"
+using Microsoft.AspNetCore.Components;
+
+namespace Test
+{
+ public class MyComponent : ComponentBase
+ {
+ }
+}
+"));
+
+ // Arrange/Act - If field already exists, don't generate it
+ var generated = CompileToCSharp(@"
+
+
+@code {
+ private Test.MyComponent myInstance = null!;
+ public void Foo() { System.GC.KeepAlive(myInstance); }
+}
+");
+
+ // Assert - should not duplicate the field
+ AssertDocumentNodeMatchesBaseline(generated.CodeDocument);
+ AssertCSharpDocumentMatchesBaseline(generated.CodeDocument);
+ CompileToAssembly(generated);
+ }
+
#endregion
#region Templates
diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentReferenceCaptureLoweringPass.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentReferenceCaptureLoweringPass.cs
index 694a0170602..b758e279d2c 100644
--- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentReferenceCaptureLoweringPass.cs
+++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentReferenceCaptureLoweringPass.cs
@@ -2,7 +2,10 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
+using System.Collections.Generic;
+using System.Linq;
using System.Threading;
+using Microsoft.AspNetCore.Razor.Language.Extensions;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
namespace Microsoft.AspNetCore.Razor.Language.Components;
@@ -31,17 +34,23 @@ protected override void ExecuteCore(
}
var references = documentNode.FindDescendantReferences();
+
+ // Track field names to avoid generating duplicates
+ var generatedFields = new HashSet(StringComparer.Ordinal);
foreach (var reference in references)
{
if (reference.Node.TagHelper.Kind == TagHelperKind.Ref)
{
- RewriteUsage(reference);
+ RewriteUsage(classNode, reference, generatedFields);
}
}
}
- private static void RewriteUsage(IntermediateNodeReference reference)
+ private static void RewriteUsage(
+ ClassDeclarationIntermediateNode classNode,
+ IntermediateNodeReference reference,
+ HashSet generatedFields)
{
var (node, parent) = reference;
@@ -59,9 +68,65 @@ private static void RewriteUsage(IntermediateNodeReference().Any(t => t.Content.Contains("#pragma") || t.Content.Contains("__o")))))
+ {
+ index++;
+ }
+
+ // Skip past any existing field declarations to maintain ordering
+ while (index < children.Count && children[index] is FieldDeclarationIntermediateNode)
+ {
+ index++;
+ }
+
+ children.Insert(index, new FieldDeclarationIntermediateNode()
+ {
+ Modifiers = CommonModifiers.Private,
+ Name = fieldName,
+ Type = fieldType,
+ });
+ }
+
private static IntermediateToken? DetermineIdentifierToken(TagHelperDirectiveAttributeIntermediateNode attributeNode)
{
var foundToken = attributeNode.Children switch