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)