diff --git a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs index 0a10d83538ca..0836828dc949 100644 --- a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs +++ b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs @@ -71,6 +71,7 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection services.TryAddScoped(); services.AddSupplyValueFromQueryProvider(); services.TryAddCascadingValue(sp => sp.GetRequiredService().HttpContext); + services.TryAddScoped(); services.TryAddScoped(); diff --git a/src/Components/Endpoints/src/DependencyInjection/WebAssemblySettingsEmitter.cs b/src/Components/Endpoints/src/DependencyInjection/WebAssemblySettingsEmitter.cs new file mode 100644 index 000000000000..87ae37d74f7f --- /dev/null +++ b/src/Components/Endpoints/src/DependencyInjection/WebAssemblySettingsEmitter.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Hosting; + +namespace Microsoft.AspNetCore.Components.Endpoints; + +internal record WebAssemblySettings(string EnvironmentName, Dictionary EnvironmentVariables); + +internal class WebAssemblySettingsEmitter(IHostEnvironment hostEnvironment) +{ + private bool wasEmittedAlready; + + private const string dotnetModifiableAssembliesName = "DOTNET_MODIFIABLE_ASSEMBLIES"; + private const string aspnetcoreBrowserToolsName = "__ASPNETCORE_BROWSER_TOOLS"; + + private static readonly string? s_dotnetModifiableAssemblies = GetNonEmptyEnvironmentVariableValue(dotnetModifiableAssembliesName); + private static readonly string? s_aspnetcoreBrowserTools = GetNonEmptyEnvironmentVariableValue(aspnetcoreBrowserToolsName); + + private static string? GetNonEmptyEnvironmentVariableValue(string name) + => Environment.GetEnvironmentVariable(name) is { Length: > 0 } value ? value : null; + + public bool TryGetSettingsOnce([NotNullWhen(true)] out WebAssemblySettings? settings) + { + if (wasEmittedAlready) + { + settings = default; + return false; + } + + var environmentVariables = new Dictionary(); + + // DOTNET_MODIFIABLE_ASSEMBLIES is used by the runtime to initialize hot-reload specific environment variables and is configured + // by the launching process (dotnet-watch / Visual Studio). + // Always add the header if the environment variable is set, regardless of the kind of environment. + if (s_dotnetModifiableAssemblies != null) + { + environmentVariables[dotnetModifiableAssembliesName] = s_dotnetModifiableAssemblies; + } + + // See https://github.com/dotnet/aspnetcore/issues/37357#issuecomment-941237000 + // Translate the _ASPNETCORE_BROWSER_TOOLS environment configured by the browser tools agent in to a HTTP response header. + if (s_aspnetcoreBrowserTools != null) + { + environmentVariables[aspnetcoreBrowserToolsName] = s_aspnetcoreBrowserTools; + } + + wasEmittedAlready = true; + settings = new (hostEnvironment.EnvironmentName, environmentVariables); + return true; + } +} diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs index f7fd6a8860f8..be1b910c6f6c 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs @@ -271,6 +271,15 @@ private void WriteComponentHtml(int componentId, TextWriter output, bool allowBo _httpContext.Response.Headers.CacheControl = "no-cache, no-store, max-age=0"; } + if (marker.Type is ComponentMarker.WebAssemblyMarkerType or ComponentMarker.AutoMarkerType) + { + if (_httpContext.RequestServices.GetRequiredService().TryGetSettingsOnce(out var settings)) + { + var settingsJson = JsonSerializer.Serialize(settings, ServerComponentSerializationSettings.JsonSerializationOptions); + output.Write($""); + } + } + var serializedStartRecord = JsonSerializer.Serialize(marker, ServerComponentSerializationSettings.JsonSerializationOptions); output.Write("(?.+?)$"; + private const string WebAssemblyOptionsPattern = "^"; private const string ComponentPattern = "^$"; private static readonly IDataProtectionProvider _dataprotectorProvider = new EphemeralDataProtectionProvider(); @@ -57,6 +58,7 @@ public async Task CanRender_ParameterlessComponent_ClientMode() var result = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), new InteractiveWebAssemblyRenderMode(prerender: false), ParameterView.Empty); await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default)); var content = writer.ToString(); + content = AssertAndStripWebAssemblyOptions(content); var match = Regex.Match(content, ComponentPattern); // Assert @@ -80,6 +82,7 @@ public async Task CanPrerender_ParameterlessComponent_ClientMode() var result = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), RenderMode.InteractiveWebAssembly, ParameterView.Empty); await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default)); var content = writer.ToString(); + content = AssertAndStripWebAssemblyOptions(content); var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline); // Assert @@ -123,6 +126,7 @@ public async Task CanRender_ComponentWithParameters_ClientMode() })); await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default)); var content = writer.ToString(); + content = AssertAndStripWebAssemblyOptions(content); var match = Regex.Match(content, ComponentPattern); // Assert @@ -160,6 +164,7 @@ public async Task CanRender_ComponentWithNullParameters_ClientMode() })); await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default)); var content = writer.ToString(); + content = AssertAndStripWebAssemblyOptions(content); var match = Regex.Match(content, ComponentPattern); // Assert @@ -195,6 +200,7 @@ public async Task CanPrerender_ComponentWithParameters_ClientMode() })); await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default)); var content = writer.ToString(); + content = AssertAndStripWebAssemblyOptions(content); var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline); // Assert @@ -244,6 +250,7 @@ public async Task CanPrerender_ComponentWithNullParameters_ClientMode() })); await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default)); var content = writer.ToString(); + content = AssertAndStripWebAssemblyOptions(content); var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline); // Assert @@ -1063,6 +1070,7 @@ public async Task RenderMode_CanRenderInteractiveComponents() var lines = content.Replace("\r\n", "\n").Split('\n'); var serverMarkerMatch = Regex.Match(lines[0], PrerenderedComponentPattern); var serverNonPrerenderedMarkerMatch = Regex.Match(lines[1], ComponentPattern); + lines[2] = AssertAndStripWebAssemblyOptions(lines[2]); var webAssemblyMarkerMatch = Regex.Match(lines[2], PrerenderedComponentPattern); var webAssemblyNonPrerenderedMarkerMatch = Regex.Match(lines[3], ComponentPattern); @@ -1167,6 +1175,8 @@ public async Task DoesNotEmitNestedRenderModeBoundaries() var numMarkers = Regex.Matches(content, MarkerPrefix).Count; Assert.Equal(2, numMarkers); // A start and an end marker + content = AssertAndStripWebAssemblyOptions(content); + var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Singleline); Assert.True(match.Success); var preamble = match.Groups["preamble"].Value; @@ -1498,6 +1508,14 @@ await renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync( } } + private string AssertAndStripWebAssemblyOptions(string content) + { + var wasmOptionsMatch = Regex.Match(content, WebAssemblyOptionsPattern); + Assert.True(wasmOptionsMatch.Success); + content = content.Substring(wasmOptionsMatch.Groups[0].Length); + return content; + } + private class NamedEventHandlerComponent : ComponentBase { [Parameter] @@ -1681,6 +1699,7 @@ private static ServiceCollection CreateDefaultServiceCollection() services.AddSingleton(); services.AddSingleton(_ => new SupplyParameterFromFormValueProvider(null, "")); services.AddScoped(); + services.AddSingleton(new WebAssemblySettingsEmitter(new TestEnvironment(Environments.Development))); return services; } diff --git a/src/Components/Web.JS/src/Boot.WebAssembly.Common.ts b/src/Components/Web.JS/src/Boot.WebAssembly.Common.ts index b31a1c4b356f..c653c789c810 100644 --- a/src/Components/Web.JS/src/Boot.WebAssembly.Common.ts +++ b/src/Components/Web.JS/src/Boot.WebAssembly.Common.ts @@ -11,7 +11,7 @@ import { SharedMemoryRenderBatch } from './Rendering/RenderBatch/SharedMemoryRen import { Pointer } from './Platform/Platform'; import { WebAssemblyStartOptions } from './Platform/WebAssemblyStartOptions'; import { addDispatchEventMiddleware } from './Rendering/WebRendererInteropMethods'; -import { WebAssemblyComponentDescriptor, discoverWebAssemblyPersistedState } from './Services/ComponentDescriptorDiscovery'; +import { WebAssemblyComponentDescriptor, WebAssemblyServerOptions, discoverWebAssemblyPersistedState } from './Services/ComponentDescriptorDiscovery'; import { receiveDotNetDataStream } from './StreamingInterop'; import { WebAssemblyComponentAttacher } from './Platform/WebAssemblyComponentAttacher'; import { MonoConfig } from '@microsoft/dotnet-runtime'; @@ -68,23 +68,23 @@ export function setWebAssemblyOptions(initializersReady: Promise): Promise { +export function startWebAssembly(components: RootComponentManager, options: WebAssemblyServerOptions | undefined): Promise { if (startPromise !== undefined) { throw new Error('Blazor WebAssembly has already started.'); } - startPromise = new Promise(startCore.bind(null, components)); + startPromise = new Promise(startCore.bind(null, components, options)); return startPromise; } -async function startCore(components: RootComponentManager, resolve, _) { +async function startCore(components: RootComponentManager, options: WebAssemblyServerOptions | undefined, resolve, _) { if (inAuthRedirectIframe()) { // eslint-disable-next-line @typescript-eslint/no-empty-function await new Promise(() => { }); // See inAuthRedirectIframe for explanation } - const platformLoadPromise = loadWebAssemblyPlatformIfNotStarted(); + const platformLoadPromise = loadWebAssemblyPlatformIfNotStarted(options); addDispatchEventMiddleware((browserRendererId, eventHandlerId, continuation) => { // It's extremely unusual, but an event can be raised while we're in the middle of synchronously applying a @@ -206,13 +206,19 @@ export function waitForBootConfigLoaded(): Promise { return bootConfigPromise; } -export function loadWebAssemblyPlatformIfNotStarted(): Promise { +export function loadWebAssemblyPlatformIfNotStarted(serverOptions: WebAssemblyServerOptions | undefined): Promise { platformLoadPromise ??= (async () => { await initializersPromise; const finalOptions = options ?? {}; + if (!finalOptions.environment) { + finalOptions.environment = serverOptions?.environmentName ?? undefined; + } const existingConfig = options?.configureRuntime; finalOptions.configureRuntime = (config) => { existingConfig?.(config); + if (serverOptions?.environmentVariables) { + config.withEnvironmentVariables(serverOptions.environmentVariables); + } if (waitForRootComponents) { config.withEnvironmentVariable('__BLAZOR_WEBASSEMBLY_WAIT_FOR_ROOT_COMPONENTS', 'true'); } diff --git a/src/Components/Web.JS/src/Boot.WebAssembly.ts b/src/Components/Web.JS/src/Boot.WebAssembly.ts index ca1523f60f1c..a1b106b924c2 100644 --- a/src/Components/Web.JS/src/Boot.WebAssembly.ts +++ b/src/Components/Web.JS/src/Boot.WebAssembly.ts @@ -6,7 +6,7 @@ import { Blazor } from './GlobalExports'; import { shouldAutoStart } from './BootCommon'; import { WebAssemblyStartOptions } from './Platform/WebAssemblyStartOptions'; import { setWebAssemblyOptions, startWebAssembly } from './Boot.WebAssembly.Common'; -import { WebAssemblyComponentDescriptor, discoverComponents } from './Services/ComponentDescriptorDiscovery'; +import { WebAssemblyComponentDescriptor, discoverComponents, discoverWebAssemblyOptions } from './Services/ComponentDescriptorDiscovery'; import { DotNet } from '@microsoft/dotnet-js-interop'; import { InitialRootComponentsList } from './Services/InitialRootComponentsList'; import { JSEventRegistry } from './Services/JSEventRegistry'; @@ -24,8 +24,10 @@ async function boot(options?: Partial): Promise { JSEventRegistry.create(Blazor); const webAssemblyComponents = discoverComponents(document, 'webassembly') as WebAssemblyComponentDescriptor[]; + const webAssemblyOptions = discoverWebAssemblyOptions(document); + const components = new InitialRootComponentsList(webAssemblyComponents); - await startWebAssembly(components); + await startWebAssembly(components, webAssemblyOptions); } Blazor.start = boot; diff --git a/src/Components/Web.JS/src/Rendering/DomMerging/DomSync.ts b/src/Components/Web.JS/src/Rendering/DomMerging/DomSync.ts index 49585f8b03e9..b86ff3bd8d03 100644 --- a/src/Components/Web.JS/src/Rendering/DomMerging/DomSync.ts +++ b/src/Components/Web.JS/src/Rendering/DomMerging/DomSync.ts @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import { AutoComponentDescriptor, ComponentDescriptor, ServerComponentDescriptor, WebAssemblyComponentDescriptor, canMergeDescriptors, discoverComponents, mergeDescriptors } from '../../Services/ComponentDescriptorDiscovery'; +import { AutoComponentDescriptor, ComponentDescriptor, ServerComponentDescriptor, WebAssemblyComponentDescriptor, WebAssemblyServerOptions, canMergeDescriptors, discoverComponents, discoverWebAssemblyOptions, mergeDescriptors } from '../../Services/ComponentDescriptorDiscovery'; import { isInteractiveRootComponentElement } from '../BrowserRenderer'; import { applyAnyDeferredValue } from '../DomSpecialPropertyUtil'; import { LogicalElement, getLogicalChildrenArray, getLogicalNextSibling, getLogicalParent, getLogicalRootDescriptor, insertLogicalChild, insertLogicalChildBefore, isLogicalElement, toLogicalElement, toLogicalRootCommentElement } from '../LogicalElements'; @@ -13,6 +13,7 @@ let descriptorHandler: DescriptorHandler | null = null; export interface DescriptorHandler { registerComponent(descriptor: ComponentDescriptor): void; + setWebAssemblyOptions(options: WebAssemblyServerOptions | undefined): void; } export function attachComponentDescriptorHandler(handler: DescriptorHandler) { @@ -21,6 +22,8 @@ export function attachComponentDescriptorHandler(handler: DescriptorHandler) { export function registerAllComponentDescriptors(root: Node) { const descriptors = upgradeComponentCommentsToLogicalRootComments(root); + const webAssemblyOptions = discoverWebAssemblyOptions(root); + descriptorHandler?.setWebAssemblyOptions(webAssemblyOptions); for (const descriptor of descriptors) { descriptorHandler?.registerComponent(descriptor); @@ -168,7 +171,7 @@ function treatAsMatch(destination: Node, source: Node) { } if (destinationRootDescriptor) { - // Update the existing descriptor with hte new descriptor's data + // Update the existing descriptor with the new descriptor's data mergeDescriptors(destinationRootDescriptor, sourceRootDescriptor); const isDestinationInteractive = isInteractiveRootComponentElement(destinationAsLogicalElement); diff --git a/src/Components/Web.JS/src/Services/ComponentDescriptorDiscovery.ts b/src/Components/Web.JS/src/Services/ComponentDescriptorDiscovery.ts index f306b676a0d4..4fb4e39d8fc2 100644 --- a/src/Components/Web.JS/src/Services/ComponentDescriptorDiscovery.ts +++ b/src/Components/Web.JS/src/Services/ComponentDescriptorDiscovery.ts @@ -15,6 +15,16 @@ export function discoverComponents(root: Node, type: 'webassembly' | 'server' | const blazorServerStateCommentRegularExpression = /^\s*Blazor-Server-Component-State:(?[a-zA-Z0-9+/=]+)$/; const blazorWebAssemblyStateCommentRegularExpression = /^\s*Blazor-WebAssembly-Component-State:(?[a-zA-Z0-9+/=]+)$/; const blazorWebInitializerCommentRegularExpression = /^\s*Blazor-Web-Initializers:(?[a-zA-Z0-9+/=]+)$/; +const blazorWebAssemblyOptionsCommentRegularExpression = /^\s*Blazor-WebAssembly:[^{]*(?.*)$/; + +export function discoverWebAssemblyOptions(root: Node): WebAssemblyServerOptions | undefined { + const optionsJson = discoverBlazorComment(root, blazorWebAssemblyOptionsCommentRegularExpression, 'options'); + if (!optionsJson) { + return undefined; + } + const options = JSON.parse(optionsJson); + return options; +} export function discoverServerPersistedState(node: Node): string | null | undefined { return discoverBlazorComment(node, blazorServerStateCommentRegularExpression); @@ -339,6 +349,11 @@ export type ServerComponentDescriptor = ServerComponentMarker & DescriptorData; export type WebAssemblyComponentDescriptor = WebAssemblyComponentMarker & DescriptorData; export type AutoComponentDescriptor = AutoComponentMarker & DescriptorData; +export type WebAssemblyServerOptions = { + environmentName: string, + environmentVariables: { [i: string]: string; } +}; + type DescriptorData = { uniqueId: number; start: Comment; diff --git a/src/Components/Web.JS/src/Services/WebRootComponentManager.ts b/src/Components/Web.JS/src/Services/WebRootComponentManager.ts index 0ee177244139..097a38c3e057 100644 --- a/src/Components/Web.JS/src/Services/WebRootComponentManager.ts +++ b/src/Components/Web.JS/src/Services/WebRootComponentManager.ts @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import { ComponentDescriptor, ComponentMarker, descriptorToMarker } from './ComponentDescriptorDiscovery'; +import { ComponentDescriptor, ComponentMarker, descriptorToMarker, WebAssemblyServerOptions } from './ComponentDescriptorDiscovery'; import { isRendererAttached, registerRendererAttachedListener } from '../Rendering/WebRendererInteropMethods'; import { WebRendererId } from '../Rendering/WebRendererId'; import { DescriptorHandler } from '../Rendering/DomMerging/DomSync'; @@ -63,6 +63,8 @@ export class WebRootComponentManager implements DescriptorHandler, RootComponent private _circuitInactivityTimeoutId: any; + private _webAssemblyOptions: WebAssemblyServerOptions | undefined; + // Implements RootComponentManager. // An empty array becuase all root components managed // by WebRootComponentManager are added and removed dynamically. @@ -94,6 +96,10 @@ export class WebRootComponentManager implements DescriptorHandler, RootComponent this.rootComponentsMayRequireRefresh(); } + public setWebAssemblyOptions(webAssemblyOptions: WebAssemblyServerOptions | undefined): void { + this._webAssemblyOptions = webAssemblyOptions; + } + public registerComponent(descriptor: ComponentDescriptor) { if (this._seenDescriptors.has(descriptor)) { return; @@ -132,7 +138,7 @@ export class WebRootComponentManager implements DescriptorHandler, RootComponent setWaitForRootComponents(); - const loadWebAssemblyPromise = loadWebAssemblyPlatformIfNotStarted(); + const loadWebAssemblyPromise = loadWebAssemblyPlatformIfNotStarted(this._webAssemblyOptions); const bootConfig = await waitForBootConfigLoaded(); if (maxParallelDownloadsOverride !== undefined) { @@ -182,7 +188,7 @@ export class WebRootComponentManager implements DescriptorHandler, RootComponent this.startLoadingWebAssemblyIfNotStarted(); if (!hasStartedWebAssembly()) { - await startWebAssembly(this); + await startWebAssembly(this, this._webAssemblyOptions); } } diff --git a/src/Components/WebAssembly/Server/src/Builder/WebAssemblyRazorComponentsEndpointConventionBuilderExtensions.cs b/src/Components/WebAssembly/Server/src/Builder/WebAssemblyRazorComponentsEndpointConventionBuilderExtensions.cs index 8c075b4769a8..b35b43bc39c1 100644 --- a/src/Components/WebAssembly/Server/src/Builder/WebAssemblyRazorComponentsEndpointConventionBuilderExtensions.cs +++ b/src/Components/WebAssembly/Server/src/Builder/WebAssemblyRazorComponentsEndpointConventionBuilderExtensions.cs @@ -59,7 +59,6 @@ public static RazorComponentsEndpointConventionBuilder AddInteractiveWebAssembly var descriptors = StaticAssetsEndpointDataSourceHelper.ResolveStaticAssetDescriptors(endpointBuilder, options.StaticAssetsManifestPath); if (descriptors != null && descriptors.Count > 0) { - ComponentWebAssemblyConventions.AddBlazorWebAssemblyConventions(descriptors, environment); return builder; } diff --git a/src/Components/WebAssembly/Server/src/ComponentWebAssemblyConventions.cs b/src/Components/WebAssembly/Server/src/ComponentWebAssemblyConventions.cs deleted file mode 100644 index 427b7135fd57..000000000000 --- a/src/Components/WebAssembly/Server/src/ComponentWebAssemblyConventions.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.StaticAssets; - -namespace Microsoft.AspNetCore.Components.WebAssembly.Server; - -internal static class ComponentWebAssemblyConventions -{ - private static readonly string? s_dotnetModifiableAssemblies = GetNonEmptyEnvironmentVariableValue("DOTNET_MODIFIABLE_ASSEMBLIES"); - private static readonly string? s_aspnetcoreBrowserTools = GetNonEmptyEnvironmentVariableValue("__ASPNETCORE_BROWSER_TOOLS"); - - private static string? GetNonEmptyEnvironmentVariableValue(string name) - => Environment.GetEnvironmentVariable(name) is { Length: > 0 } value ? value : null; - - internal static void AddBlazorWebAssemblyConventions( - IReadOnlyList descriptors, - IWebHostEnvironment webHostEnvironment) - { - var headers = new List - { - new("Blazor-Environment", webHostEnvironment.EnvironmentName) - }; - - // DOTNET_MODIFIABLE_ASSEMBLIES is used by the runtime to initialize hot-reload specific environment variables and is configured - // by the launching process (dotnet-watch / Visual Studio). - // Always add the header if the environment variable is set, regardless of the kind of environment. - if (s_dotnetModifiableAssemblies != null) - { - headers.Add(new("DOTNET-MODIFIABLE-ASSEMBLIES", s_dotnetModifiableAssemblies)); - } - - // See https://github.com/dotnet/aspnetcore/issues/37357#issuecomment-941237000 - // Translate the _ASPNETCORE_BROWSER_TOOLS environment configured by the browser tools agent in to a HTTP response header. - if (s_aspnetcoreBrowserTools != null) - { - headers.Add(new("ASPNETCORE-BROWSER-TOOLS", s_aspnetcoreBrowserTools)); - } - - for (var i = 0; i < descriptors.Count; i++) - { - var descriptor = descriptors[i]; - if (descriptor.AssetPath.StartsWith("_framework/", StringComparison.OrdinalIgnoreCase)) - { - descriptor.ResponseHeaders = [ - ..descriptor.ResponseHeaders, - ..headers]; - } - } - } -}