diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperDiscoverer.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperDiscoverer.cs
index 9fc6f5ec5fd2a..495190409a306 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperDiscoverer.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperDiscoverer.cs
@@ -168,4 +168,59 @@ public TagHelperCollection GetTagHelpers(IAssemblySymbol assembly, CancellationT
builder.Dispose();
}
}
+
+ ///
+ /// Discovers tag helpers for a known set of types rather than walking an entire assembly.
+ ///
+ ///
+ /// Only type producers run here; assembly-level static tag helpers are intentionally skipped because a
+ /// caller with a specific type set already owns discovery of the rest of the assembly. Producers examine
+ /// each type independently, so restricting the input to a subset yields the same descriptors those types
+ /// would produce during a full assembly walk. Results are not assembly-cached because the input is a
+ /// per-request slice rather than a whole assembly.
+ ///
+ public TagHelperCollection GetTagHelpers(ImmutableArray types, CancellationToken cancellationToken = default)
+ {
+ if (producers.IsDefaultOrEmpty || types.IsDefaultOrEmpty)
+ {
+ return TagHelperCollection.Empty;
+ }
+
+ var builder = new TagHelperCollection.RefBuilder();
+ try
+ {
+ using var _ = ArrayPool.Shared.GetPooledArraySpan(
+ minimumLength: producers.Length, clearOnReturn: true, out var typeProducers);
+
+ var index = 0;
+ foreach (var producer in producers)
+ {
+ if (producer.SupportsTypes)
+ {
+ typeProducers[index++] = producer;
+ }
+ }
+
+ typeProducers = typeProducers[..index];
+
+ foreach (var type in types)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ foreach (var producer in typeProducers)
+ {
+ if (producer.IsCandidateType(type))
+ {
+ producer.AddTagHelpersForType(type, ref builder, cancellationToken);
+ }
+ }
+ }
+
+ return builder.ToCollection();
+ }
+ finally
+ {
+ builder.Dispose();
+ }
+ }
}
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..0d02b3b964ad9 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
@@ -2,8 +2,10 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using System.Collections.Generic;
using System.Collections.Immutable;
using System.Text;
+using System.Threading;
using Microsoft.AspNetCore.Mvc.Razor.Extensions;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.PooledObjects;
@@ -132,6 +134,44 @@ private static StaticCompilationTagHelperFeature GetStaticTagHelperFeature(Compi
return tagHelperFeature;
}
+ ///
+ /// Resolves the fallback component type symbols so the slow discovery path can target just those
+ /// types instead of walking the whole augmented assembly. Uses the compilation's declaration table
+ /// (no semantic models): a fallback type name is namespace-qualified, so the fast predicate keys off
+ /// its final segment and over-selects; the caller's descriptor-name filter trims any collisions.
+ ///
+ private static ImmutableArray ResolveFallbackTypes(
+ Compilation compilation,
+ ImmutableHashSet fallbackTypeNames,
+ CancellationToken cancellationToken)
+ {
+ if (fallbackTypeNames.IsEmpty)
+ {
+ return [];
+ }
+
+ var shortNames = new HashSet(StringComparer.Ordinal);
+ foreach (var name in fallbackTypeNames)
+ {
+ var lastDot = name.LastIndexOf('.');
+ shortNames.Add(lastDot >= 0 ? name.Substring(lastDot + 1) : name);
+ }
+
+ using var builder = new PooledArrayBuilder();
+
+ foreach (var symbol in compilation.GetSymbolsWithName(shortNames.Contains, SymbolFilter.Type, cancellationToken))
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (symbol is INamedTypeSymbol typeSymbol)
+ {
+ builder.Add(typeSymbol);
+ }
+ }
+
+ return builder.ToImmutable();
+ }
+
private static SourceGeneratorProjectEngine GetGenerationProjectEngine(
SourceGeneratorProjectItem item,
ImmutableArray imports,
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..195e7500ffb44 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
@@ -215,14 +215,29 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
}
var augmented = compilation.AddSyntaxTrees(fallbackTrees);
+
+ // Discover only the fallback components' own types rather than re-walking the whole
+ // augmented assembly. fastDiscovery already covered every splittable component, so the
+ // full walk here would rediscover all of them just to keep the few fallback types. The
+ // fallback types are exactly the ones declared by the discovery-only decl trees we just
+ // added, and tag-helper producers examine each type independently, so discovering just
+ // those yields the same descriptors the full walk would for them.
+ var fallbackTypes = ResolveFallbackTypes(augmented, fallbackTypeNames, cancellationToken);
+ if (fallbackTypes.IsDefaultOrEmpty)
+ {
+ return TagHelperCollection.Empty;
+ }
+
var tagHelperFeature = GetStaticTagHelperFeature(augmented);
- var all = tagHelperFeature.GetTagHelpers(augmented.Assembly, cancellationToken);
- if (all.IsEmpty)
+ var discovered = tagHelperFeature.GetTagHelpers(fallbackTypes, cancellationToken);
+ if (discovered.IsEmpty)
{
return TagHelperCollection.Empty;
}
- return all.Where(fallbackTypeNames, static (descriptor, names) => names.Contains(StripGenericArity(descriptor.TypeName)));
+ // A resolved type can be a partial that also produces a non-fallback descriptor name;
+ // keep only the fallback components' types, matching the ownership split with fastDiscovery.
+ return discovered.Where(fallbackTypeNames, static (descriptor, names) => names.Contains(StripGenericArity(descriptor.TypeName)));
})
.WithLambdaComparer(static (a, b) => a!.SequenceEqual(b!))
.WithTrackingName("SlowTagHelpers");
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/StaticCompilationTagHelperFeature.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/StaticCompilationTagHelperFeature.cs
index 79d08eaad3d1c..86419b86217c3 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/StaticCompilationTagHelperFeature.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/StaticCompilationTagHelperFeature.cs
@@ -31,6 +31,22 @@ public TagHelperCollection GetTagHelpers(IAssemblySymbol assembly, CancellationT
return _discoverer.GetTagHelpers(assembly, cancellationToken);
}
+ public TagHelperCollection GetTagHelpers(ImmutableArray types, CancellationToken cancellationToken)
+ {
+ if (_discoveryService is null)
+ {
+ return [];
+ }
+
+ if (_discoverer is null &&
+ !_discoveryService.TryGetDiscoverer(compilation, out _discoverer))
+ {
+ return [];
+ }
+
+ return _discoverer.GetTagHelpers(types, cancellationToken);
+ }
+
TagHelperCollection ITagHelperFeature.GetTagHelpers(CancellationToken cancellationToken)
{
if (_discoveryService is null)