diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.Helpers.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.Helpers.cs
index 2602ff39c660b..13922de8bb3da 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.Helpers.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.Helpers.cs
@@ -37,6 +37,28 @@ internal static string StripGenericArity(string typeName)
return baseName.Length == typeName.Length ? typeName : baseName.ToString();
}
+ ///
+ /// Returns the name of the component type a descriptor belongs to. A component descriptor's owning
+ /// type is itself, but descriptors derived from a component -- child content and bind, for example --
+ /// carry the owning component's namespace and identifier while their own
+ /// is suffixed (e.g. Ns.Card.Header for the Header child content of Ns.Card).
+ /// Reconstructing the owning name from the namespace and identifier lets ownership filtering keep or
+ /// exclude a fallback component and all its derived descriptors together, rather than only the
+ /// component descriptor whose matches exactly.
+ ///
+ internal static string GetOwningTypeName(TagHelperDescriptor descriptor)
+ {
+ var identifier = descriptor.TypeNameIdentifier;
+ if (identifier is null)
+ {
+ return descriptor.TypeName;
+ }
+
+ return descriptor.TypeNamespace is { Length: > 0 } typeNamespace
+ ? typeNamespace + "." + identifier
+ : identifier;
+ }
+
///
/// Returns the hint name for the decl half of a Razor component generated source given
/// the impl half's hint name. The decl file substitutes .decl.g.cs for the
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.cs
index 273ad807cf1bc..26dbc6f6160e2 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.cs
@@ -222,7 +222,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
return TagHelperCollection.Empty;
}
- return all.Where(fallbackTypeNames, static (descriptor, names) => names.Contains(StripGenericArity(descriptor.TypeName)));
+ return all.Where(fallbackTypeNames, static (descriptor, names) => names.Contains(StripGenericArity(GetOwningTypeName(descriptor))));
})
.WithLambdaComparer(static (a, b) => a!.SequenceEqual(b!))
.WithTrackingName("SlowTagHelpers");
@@ -239,7 +239,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
var ((fast, slow), fallbackTypeNames) = pair;
var fastOwned = fallbackTypeNames.IsEmpty
? fast
- : fast.Where(fallbackTypeNames, static (descriptor, names) => !names.Contains(StripGenericArity(descriptor.TypeName)));
+ : fast.Where(fallbackTypeNames, static (descriptor, names) => !names.Contains(StripGenericArity(GetOwningTypeName(descriptor))));
return TagHelperCollection.Merge(fastOwned, slow);
})
.WithTrackingName("TagHelpersFromCompilation");
diff --git a/src/Razor/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.UnitTests/RazorSourceGeneratorComponentTests.cs b/src/Razor/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.UnitTests/RazorSourceGeneratorComponentTests.cs
index 23e2aacbc5057..be0276d7b3638 100644
--- a/src/Razor/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.UnitTests/RazorSourceGeneratorComponentTests.cs
+++ b/src/Razor/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.UnitTests/RazorSourceGeneratorComponentTests.cs
@@ -1329,6 +1329,52 @@ public class MyGrid : ComponentBase
Assert.Equal(2, result.GeneratedSources.Length);
}
+ [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/84817")]
+ public async Task Component_FallbackComponent_ChildContentParameters()
+ {
+ // A fallback component (unsplittable via @inherits) exposes RenderFragment child-content
+ // parameters. A consumer referencing them as child elements must resolve them, i.e. no
+ // RZ10012 "unexpected name" for Header/ChildContent.
+ var project = CreateTestProject(new()
+ {
+ ["Shared/Card.razor"] = """
+ @inherits EmptyBase
+
+
+ @ChildContent
+
+ @code {
+ [Parameter]
+ public RenderFragment? Header { get; set; }
+
+ [Parameter]
+ public RenderFragment? ChildContent { get; set; }
+ }
+ """,
+ ["Shared/Consumer.razor"] = """
+
+
+ Expected content
+
+ """,
+ }, new()
+ {
+ ["EmptyBase.cs"] = """
+ using Microsoft.AspNetCore.Components;
+
+ public abstract class EmptyBase : ComponentBase;
+ """,
+ });
+ var compilation = await project.GetCompilationAsync();
+ var driver = await GetDriverAsync(project);
+
+ // Act
+ var result = RunGenerator(compilation!, ref driver);
+
+ // Assert -- no RZ10012 for the fallback component's child-content parameters
+ result.Diagnostics.Verify();
+ }
+
[Fact]
public async Task Component_WithImplicitContext_NestedInWrapper()
{