From 513d10a17ce324c4b3b14ed98ae4ec4e1cd8882d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fi=C5=A1era?= Date: Wed, 26 Mar 2025 12:29:52 +0100 Subject: [PATCH 01/11] Generate Link headers based on StaticWebAssets manifest properties --- .../EndpointHtmlRenderer.Streaming.cs | 66 +++++++++++++++++++ .../Components/ResourceCollectionResolver.cs | 57 +++++++++++++++- 2 files changed, 120 insertions(+), 3 deletions(-) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs index be1b910c6f6c..1c3ab2eae96e 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Components.Endpoints; @@ -275,6 +276,12 @@ private void WriteComponentHtml(int componentId, TextWriter output, bool allowBo { if (_httpContext.RequestServices.GetRequiredService().TryGetSettingsOnce(out var settings)) { + if (marker.Type is ComponentMarker.WebAssemblyMarkerType) + { + // Preload WebAssembly assets when using WebAssembly (not Auto) mode + AppendWebAssemblyPreloadHeaders(); + } + var settingsJson = JsonSerializer.Serialize(settings, ServerComponentSerializationSettings.JsonSerializationOptions); output.Write($""); } @@ -311,6 +318,65 @@ private void WriteComponentHtml(int componentId, TextWriter output, bool allowBo } } + private void AppendWebAssemblyPreloadHeaders() + { + var assets = _httpContext.GetEndpoint()?.Metadata.GetMetadata(); + if (assets != null) + { + var headers = new List(); + foreach (var asset in assets) + { + if (asset.Properties == null) + { + continue; + } + + // Use preloadrel to identify assets that should to be preloaded + string? header = null; + foreach (var property in asset.Properties) + { + if (property.Name.Equals("preloadrel", StringComparison.OrdinalIgnoreCase)) + { + header = String.Concat($"<{asset.Url}>", "; rel=", property.Value); + break; + } + } + + if (header == null) + { + continue; + } + + foreach (var property in asset.Properties) + { + if (property.Name.Equals("preloadas", StringComparison.OrdinalIgnoreCase)) + { + header = String.Concat(header, "; as=", property.Value); + } + else if (property.Name.Equals("preloadpriority", StringComparison.OrdinalIgnoreCase)) + { + header = String.Concat(header, "; fetchpriority=", property.Value); + } + else if (property.Name.Equals("preloadcrossorigin", StringComparison.OrdinalIgnoreCase)) + { + header = String.Concat(header, "; crossorigin=", property.Value); + } + else if (property.Name.Equals("integrity", StringComparison.OrdinalIgnoreCase)) + { + header = String.Concat(header, "; integrity=\"", property.Value, "\""); + } + } + + if (header != null) + { + headers.Add(header); + } + } + + _httpContext.Response.Headers.Link = StringValues.Concat(_httpContext.Response.Headers.Link, headers.ToArray()); + } + } + private static bool IsProgressivelyEnhancedNavigation(HttpRequest request) { // For enhanced nav, the Blazor JS code controls the "accept" header precisely, so we can be very specific about the format diff --git a/src/Shared/Components/ResourceCollectionResolver.cs b/src/Shared/Components/ResourceCollectionResolver.cs index a0b39aa8208e..c4d8b7775758 100644 --- a/src/Shared/Components/ResourceCollectionResolver.cs +++ b/src/Shared/Components/ResourceCollectionResolver.cs @@ -42,9 +42,17 @@ public ResourceAssetCollection ResolveResourceCollection() #if !MVC_VIEWFEATURES string? label = null; string? integrity = null; + string? preloadRel = null; + string? preloadAs = null; + string? preloadPriority = null; + string? preloadCrossorigin = null; #else string label = null; string integrity = null; + string preloadRel = null; + string preloadAs = null; + string preloadPriority = null; + string preloadCrossorigin = null; #endif // If there's a selector this means that this is an alternative representation for a resource, so skip it. @@ -59,15 +67,34 @@ public ResourceAssetCollection ResolveResourceCollection() label = property.Value; foundProperties++; } - else if (property.Name.Equals("integrity", StringComparison.OrdinalIgnoreCase)) { integrity = property.Value; foundProperties++; } + else if (property.Name.Equals("preloadrel", StringComparison.OrdinalIgnoreCase)) + { + preloadRel = property.Value; + foundProperties++; + } + else if (property.Name.Equals("preloadas", StringComparison.OrdinalIgnoreCase)) + { + preloadAs = property.Value; + foundProperties++; + } + else if (property.Name.Equals("preloadpriority", StringComparison.OrdinalIgnoreCase)) + { + preloadPriority = property.Value; + foundProperties++; + } + else if (property.Name.Equals("preloadcrossorigin", StringComparison.OrdinalIgnoreCase)) + { + preloadCrossorigin = property.Value; + foundProperties++; + } } - AddResource(resources, descriptor, label, integrity, foundProperties); + AddResource(resources, descriptor, label, integrity, preloadRel, preloadAs, preloadPriority, preloadCrossorigin, foundProperties); } } @@ -97,11 +124,19 @@ private static void AddResource( #if !MVC_VIEWFEATURES string? label, string? integrity, + string? preloadRel, + string? preloadAs, + string? preloadPriority, + string? preloadCrossorigin, #else string label, string integrity, + string preloadRel, + string preloadAs, + string preloadPriority, + string preloadCrossorigin, #endif - int foundProperties) + int foundProperties) { if (label != null || integrity != null) { @@ -115,6 +150,22 @@ private static void AddResource( { properties[index++] = new ResourceAssetProperty("integrity", integrity); } + if (preloadRel != null) + { + properties[index++] = new ResourceAssetProperty("preloadrel", preloadRel); + } + if (preloadAs != null) + { + properties[index++] = new ResourceAssetProperty("preloadas", preloadAs); + } + if (preloadPriority != null) + { + properties[index++] = new ResourceAssetProperty("preloadpriority", preloadPriority); + } + if (preloadCrossorigin != null) + { + properties[index++] = new ResourceAssetProperty("preloadcrossorigin", preloadCrossorigin); + } resources.Add(new ResourceAsset(descriptor.Route, properties)); } From 8768690fb901c476e7f1f235008980cf0cb72292 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fi=C5=A1era?= Date: Wed, 26 Mar 2025 14:26:10 +0100 Subject: [PATCH 02/11] Add preload order property and sort headers --- .../Rendering/EndpointHtmlRenderer.Streaming.cs | 13 ++++++++++--- .../Components/ResourceCollectionResolver.cs | 15 ++++++++++++++- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs index 1c3ab2eae96e..b95b230912fa 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Text.Encodings.Web; @@ -323,7 +324,7 @@ private void AppendWebAssemblyPreloadHeaders() var assets = _httpContext.GetEndpoint()?.Metadata.GetMetadata(); if (assets != null) { - var headers = new List(); + var headers = new List<(string? Order, string Value)>(); foreach (var asset in assets) { if (asset.Properties == null) @@ -347,6 +348,7 @@ private void AppendWebAssemblyPreloadHeaders() continue; } + string? order = null; foreach (var property in asset.Properties) { if (property.Name.Equals("preloadas", StringComparison.OrdinalIgnoreCase)) @@ -365,15 +367,20 @@ private void AppendWebAssemblyPreloadHeaders() { header = String.Concat(header, "; integrity=\"", property.Value, "\""); } + else if (property.Name.Equals("preloadorder", StringComparison.OrdinalIgnoreCase)) + { + order = property.Value; + } } if (header != null) { - headers.Add(header); + headers.Add((order, header)); } } - _httpContext.Response.Headers.Link = StringValues.Concat(_httpContext.Response.Headers.Link, headers.ToArray()); + headers.Sort((a, b) => string.Compare(a.Order, b.Order, StringComparison.InvariantCulture)); + _httpContext.Response.Headers.Link = StringValues.Concat(_httpContext.Response.Headers.Link, headers.Select(h => h.Value).ToArray()); } } diff --git a/src/Shared/Components/ResourceCollectionResolver.cs b/src/Shared/Components/ResourceCollectionResolver.cs index c4d8b7775758..bda3cb5372b6 100644 --- a/src/Shared/Components/ResourceCollectionResolver.cs +++ b/src/Shared/Components/ResourceCollectionResolver.cs @@ -46,6 +46,7 @@ public ResourceAssetCollection ResolveResourceCollection() string? preloadAs = null; string? preloadPriority = null; string? preloadCrossorigin = null; + string? preloadOrder = null; #else string label = null; string integrity = null; @@ -53,6 +54,7 @@ public ResourceAssetCollection ResolveResourceCollection() string preloadAs = null; string preloadPriority = null; string preloadCrossorigin = null; + string preloadOrder = null; #endif // If there's a selector this means that this is an alternative representation for a resource, so skip it. @@ -92,9 +94,14 @@ public ResourceAssetCollection ResolveResourceCollection() preloadCrossorigin = property.Value; foundProperties++; } + else if (property.Name.Equals("preloadorder", StringComparison.OrdinalIgnoreCase)) + { + preloadOrder = property.Value; + foundProperties++; + } } - AddResource(resources, descriptor, label, integrity, preloadRel, preloadAs, preloadPriority, preloadCrossorigin, foundProperties); + AddResource(resources, descriptor, label, integrity, preloadRel, preloadAs, preloadPriority, preloadCrossorigin, preloadOrder, foundProperties); } } @@ -128,6 +135,7 @@ private static void AddResource( string? preloadAs, string? preloadPriority, string? preloadCrossorigin, + string? preloadOrder, #else string label, string integrity, @@ -135,6 +143,7 @@ private static void AddResource( string preloadAs, string preloadPriority, string preloadCrossorigin, + string preloadOrder, #endif int foundProperties) { @@ -166,6 +175,10 @@ private static void AddResource( { properties[index++] = new ResourceAssetProperty("preloadcrossorigin", preloadCrossorigin); } + if (preloadOrder != null) + { + properties[index++] = new ResourceAssetProperty("preloadorder", preloadOrder); + } resources.Add(new ResourceAsset(descriptor.Route, properties)); } From 5c260be6f8436fc654d12c207ae97d80de5197a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fi=C5=A1era?= Date: Thu, 27 Mar 2025 12:49:19 +0100 Subject: [PATCH 03/11] Add unit test --- .../test/EndpointHtmlRendererTest.cs | 60 ++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs b/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs index f77d6f38ac5c..ce4a9593b831 100644 --- a/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs +++ b/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Net.Http; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.RegularExpressions; @@ -71,6 +70,65 @@ public async Task CanRender_ParameterlessComponent_ClientMode() Assert.Empty(httpContext.Items); } + [Fact] + public async Task CanPreload_WebAssembly_ResourceAssets() + { + // Arrange + var httpContext = GetHttpContext(); + var writer = new StringWriter(); + + httpContext.SetEndpoint( + new Endpoint( + ctx => Task.CompletedTask, + new EndpointMetadataCollection([ + new ResourceAssetCollection([ + new ResourceAsset("second.js", [ + new ResourceAssetProperty("preloadrel", "preload"), + new ResourceAssetProperty("preloadas", "script"), + new ResourceAssetProperty("preloadpriority", "high"), + new ResourceAssetProperty("preloadcrossorigin", "anonymous"), + new ResourceAssetProperty("integrity", "abcd"), + new ResourceAssetProperty("preloadorder", "2") + ]), + new ResourceAsset("first.js", [ + new ResourceAssetProperty("preloadrel", "preload"), + new ResourceAssetProperty("preloadas", "script"), + new ResourceAssetProperty("preloadpriority", "high"), + new ResourceAssetProperty("preloadcrossorigin", "anonymous"), + new ResourceAssetProperty("integrity", "abcd"), + new ResourceAssetProperty("preloadorder", "1") + ]), + new ResourceAsset("nopreload.js", [ + new ResourceAssetProperty("integrity", "abcd") + ]) + ]) + ]), + "TestEndpoint" + ) + ); + + // Act + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), new InteractiveWebAssemblyRenderMode(prerender: false), ParameterView.Empty); + await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default)); + + // Assert + Assert.Equal(2, httpContext.Response.Headers.Link.Count); + + var firstPreloadLink = httpContext.Response.Headers.Link[0]; + Assert.Contains("", firstPreloadLink); + Assert.Contains("rel=preload", firstPreloadLink); + Assert.Contains("as=script", firstPreloadLink); + Assert.Contains("fetchpriority=high", firstPreloadLink); + Assert.Contains("integrity=\"abcd\"", firstPreloadLink); + + var secondPreloadLink = httpContext.Response.Headers.Link[1]; + Assert.Contains("", secondPreloadLink); + Assert.Contains("rel=preload", secondPreloadLink); + Assert.Contains("as=script", secondPreloadLink); + Assert.Contains("fetchpriority=high", secondPreloadLink); + Assert.Contains("integrity=\"abcd\"", secondPreloadLink); + } + [Fact] public async Task CanPrerender_ParameterlessComponent_ClientMode() { From bc09678f51a6020e9ac55a631bbba3ca57e7af07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fi=C5=A1era?= Date: Thu, 27 Mar 2025 13:07:50 +0100 Subject: [PATCH 04/11] Introduce groups for preloading --- .../Rendering/EndpointHtmlRenderer.Streaming.cs | 12 ++++++++---- .../Endpoints/test/EndpointHtmlRendererTest.cs | 15 +++++++++++++-- .../Components/ResourceCollectionResolver.cs | 15 ++++++++++++++- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs index b95b230912fa..3a12ad3e4b60 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs @@ -332,13 +332,13 @@ private void AppendWebAssemblyPreloadHeaders() continue; } - // Use preloadrel to identify assets that should to be preloaded + // Use preloadgroup=webassembly to identify assets that should to be preloaded string? header = null; foreach (var property in asset.Properties) { - if (property.Name.Equals("preloadrel", StringComparison.OrdinalIgnoreCase)) + if (property.Name.Equals("preloadgroup", StringComparison.OrdinalIgnoreCase) && property.Value.Equals("webassembly", StringComparison.OrdinalIgnoreCase)) { - header = String.Concat($"<{asset.Url}>", "; rel=", property.Value); + header = $"<{asset.Url}>"; break; } } @@ -351,7 +351,11 @@ private void AppendWebAssemblyPreloadHeaders() string? order = null; foreach (var property in asset.Properties) { - if (property.Name.Equals("preloadas", StringComparison.OrdinalIgnoreCase)) + if (property.Name.Equals("preloadrel", StringComparison.OrdinalIgnoreCase)) + { + header = String.Concat(header, "; rel=", property.Value); + } + else if (property.Name.Equals("preloadas", StringComparison.OrdinalIgnoreCase)) { header = String.Concat(header, "; as=", property.Value); } diff --git a/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs b/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs index ce4a9593b831..e35759c9ae0d 100644 --- a/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs +++ b/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs @@ -88,7 +88,8 @@ public async Task CanPreload_WebAssembly_ResourceAssets() new ResourceAssetProperty("preloadpriority", "high"), new ResourceAssetProperty("preloadcrossorigin", "anonymous"), new ResourceAssetProperty("integrity", "abcd"), - new ResourceAssetProperty("preloadorder", "2") + new ResourceAssetProperty("preloadorder", "2"), + new ResourceAssetProperty("preloadgroup", "webassembly") ]), new ResourceAsset("first.js", [ new ResourceAssetProperty("preloadrel", "preload"), @@ -96,7 +97,17 @@ public async Task CanPreload_WebAssembly_ResourceAssets() new ResourceAssetProperty("preloadpriority", "high"), new ResourceAssetProperty("preloadcrossorigin", "anonymous"), new ResourceAssetProperty("integrity", "abcd"), - new ResourceAssetProperty("preloadorder", "1") + new ResourceAssetProperty("preloadorder", "1"), + new ResourceAssetProperty("preloadgroup", "webassembly") + ]), + new ResourceAsset("preload-nowebassembly.js", [ + new ResourceAssetProperty("preloadrel", "preload"), + new ResourceAssetProperty("preloadas", "script"), + new ResourceAssetProperty("preloadpriority", "high"), + new ResourceAssetProperty("preloadcrossorigin", "anonymous"), + new ResourceAssetProperty("integrity", "abcd"), + new ResourceAssetProperty("preloadorder", "1"), + new ResourceAssetProperty("preloadgroup", "abcd") ]), new ResourceAsset("nopreload.js", [ new ResourceAssetProperty("integrity", "abcd") diff --git a/src/Shared/Components/ResourceCollectionResolver.cs b/src/Shared/Components/ResourceCollectionResolver.cs index bda3cb5372b6..f5a56cbdb20a 100644 --- a/src/Shared/Components/ResourceCollectionResolver.cs +++ b/src/Shared/Components/ResourceCollectionResolver.cs @@ -47,6 +47,7 @@ public ResourceAssetCollection ResolveResourceCollection() string? preloadPriority = null; string? preloadCrossorigin = null; string? preloadOrder = null; + string? preloadGroup = null; #else string label = null; string integrity = null; @@ -55,6 +56,7 @@ public ResourceAssetCollection ResolveResourceCollection() string preloadPriority = null; string preloadCrossorigin = null; string preloadOrder = null; + string preloadGroup = null; #endif // If there's a selector this means that this is an alternative representation for a resource, so skip it. @@ -99,9 +101,14 @@ public ResourceAssetCollection ResolveResourceCollection() preloadOrder = property.Value; foundProperties++; } + else if (property.Name.Equals("preloadgroup", StringComparison.OrdinalIgnoreCase)) + { + preloadGroup = property.Value; + foundProperties++; + } } - AddResource(resources, descriptor, label, integrity, preloadRel, preloadAs, preloadPriority, preloadCrossorigin, preloadOrder, foundProperties); + AddResource(resources, descriptor, label, integrity, preloadRel, preloadAs, preloadPriority, preloadCrossorigin, preloadOrder, preloadGroup, foundProperties); } } @@ -136,6 +143,7 @@ private static void AddResource( string? preloadPriority, string? preloadCrossorigin, string? preloadOrder, + string? preloadGroup, #else string label, string integrity, @@ -144,6 +152,7 @@ private static void AddResource( string preloadPriority, string preloadCrossorigin, string preloadOrder, + string preloadGroup, #endif int foundProperties) { @@ -179,6 +188,10 @@ private static void AddResource( { properties[index++] = new ResourceAssetProperty("preloadorder", preloadOrder); } + if (preloadGroup != null) + { + properties[index++] = new ResourceAssetProperty("preloadgroup", preloadGroup); + } resources.Add(new ResourceAsset(descriptor.Route, properties)); } From efd9c3552792c744cc0e26398f15c21c500fc496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fi=C5=A1era?= Date: Fri, 28 Mar 2025 15:42:41 +0100 Subject: [PATCH 05/11] wip --- .../src/Builder/ResourcePreloadCollection.cs | 84 +++++++++++++++++++ .../EndpointHtmlRenderer.Streaming.cs | 67 +-------------- 2 files changed, 88 insertions(+), 63 deletions(-) create mode 100644 src/Components/Endpoints/src/Builder/ResourcePreloadCollection.cs diff --git a/src/Components/Endpoints/src/Builder/ResourcePreloadCollection.cs b/src/Components/Endpoints/src/Builder/ResourcePreloadCollection.cs new file mode 100644 index 000000000000..e1aad76b280f --- /dev/null +++ b/src/Components/Endpoints/src/Builder/ResourcePreloadCollection.cs @@ -0,0 +1,84 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Components.Endpoints; + +internal class ResourcePreloadCollection +{ + private readonly Dictionary _storage = new(); + + public ResourcePreloadCollection(ResourceAssetCollection assets) + { + if (assets != null) + { + var headers = new List<(string? Order, string Value)>(); + foreach (var asset in assets) + { + if (asset.Properties == null) + { + continue; + } + + // Use preloadgroup=webassembly to identify assets that should to be preloaded + string? header = null; + string? group = null; + foreach (var property in asset.Properties) + { + if (property.Name.Equals("preloadgroup", StringComparison.OrdinalIgnoreCase)) + { + group = property.Value; + header = $"<{asset.Url}>"; + break; + } + } + + if (header == null) + { + continue; + } + + string? order = null; + foreach (var property in asset.Properties) + { + if (property.Name.Equals("preloadrel", StringComparison.OrdinalIgnoreCase)) + { + header = String.Concat(header, "; rel=", property.Value); + } + else if (property.Name.Equals("preloadas", StringComparison.OrdinalIgnoreCase)) + { + header = String.Concat(header, "; as=", property.Value); + } + else if (property.Name.Equals("preloadpriority", StringComparison.OrdinalIgnoreCase)) + { + header = String.Concat(header, "; fetchpriority=", property.Value); + } + else if (property.Name.Equals("preloadcrossorigin", StringComparison.OrdinalIgnoreCase)) + { + header = String.Concat(header, "; crossorigin=", property.Value); + } + else if (property.Name.Equals("integrity", StringComparison.OrdinalIgnoreCase)) + { + header = String.Concat(header, "; integrity=\"", property.Value, "\""); + } + else if (property.Name.Equals("preloadorder", StringComparison.OrdinalIgnoreCase)) + { + order = property.Value; + } + } + + if (header != null) + { + headers.Add((order, header)); + } + } + + headers.Sort((a, b) => string.Compare(a.Order, b.Order, StringComparison.InvariantCulture)); + } + } + + public bool TryGetLinkHeaders(string group, out StringValues linkHeaders) + => _storage.TryGetValue(group, out linkHeaders); +} diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs index 3a12ad3e4b60..a018f3071b9d 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Linq; +using System.Reflection.PortableExecutable; using System.Runtime.InteropServices; using System.Text; using System.Text.Encodings.Web; @@ -321,70 +322,10 @@ private void WriteComponentHtml(int componentId, TextWriter output, bool allowBo private void AppendWebAssemblyPreloadHeaders() { - var assets = _httpContext.GetEndpoint()?.Metadata.GetMetadata(); - if (assets != null) + var preloads = _httpContext.GetEndpoint()?.Metadata.GetMetadata(); + if (preloads != null && preloads.TryGetLinkHeaders("webassembly", out var linkHeaders)) { - var headers = new List<(string? Order, string Value)>(); - foreach (var asset in assets) - { - if (asset.Properties == null) - { - continue; - } - - // Use preloadgroup=webassembly to identify assets that should to be preloaded - string? header = null; - foreach (var property in asset.Properties) - { - if (property.Name.Equals("preloadgroup", StringComparison.OrdinalIgnoreCase) && property.Value.Equals("webassembly", StringComparison.OrdinalIgnoreCase)) - { - header = $"<{asset.Url}>"; - break; - } - } - - if (header == null) - { - continue; - } - - string? order = null; - foreach (var property in asset.Properties) - { - if (property.Name.Equals("preloadrel", StringComparison.OrdinalIgnoreCase)) - { - header = String.Concat(header, "; rel=", property.Value); - } - else if (property.Name.Equals("preloadas", StringComparison.OrdinalIgnoreCase)) - { - header = String.Concat(header, "; as=", property.Value); - } - else if (property.Name.Equals("preloadpriority", StringComparison.OrdinalIgnoreCase)) - { - header = String.Concat(header, "; fetchpriority=", property.Value); - } - else if (property.Name.Equals("preloadcrossorigin", StringComparison.OrdinalIgnoreCase)) - { - header = String.Concat(header, "; crossorigin=", property.Value); - } - else if (property.Name.Equals("integrity", StringComparison.OrdinalIgnoreCase)) - { - header = String.Concat(header, "; integrity=\"", property.Value, "\""); - } - else if (property.Name.Equals("preloadorder", StringComparison.OrdinalIgnoreCase)) - { - order = property.Value; - } - } - - if (header != null) - { - headers.Add((order, header)); - } - } - - headers.Sort((a, b) => string.Compare(a.Order, b.Order, StringComparison.InvariantCulture)); - _httpContext.Response.Headers.Link = StringValues.Concat(_httpContext.Response.Headers.Link, headers.Select(h => h.Value).ToArray()); + _httpContext.Response.Headers.Link = StringValues.Concat(_httpContext.Response.Headers.Link, linkHeaders); } } From 28b12637f1aa8e96fdc4113727e8d95f6a8851b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fi=C5=A1era?= Date: Tue, 1 Apr 2025 13:48:50 +0200 Subject: [PATCH 06/11] wip --- .../src/Builder/ResourceCollectionConvention.cs | 1 + .../src/Builder/ResourcePreloadCollection.cs | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Components/Endpoints/src/Builder/ResourceCollectionConvention.cs b/src/Components/Endpoints/src/Builder/ResourceCollectionConvention.cs index 2415687031ec..7606ff07abd7 100644 --- a/src/Components/Endpoints/src/Builder/ResourceCollectionConvention.cs +++ b/src/Components/Endpoints/src/Builder/ResourceCollectionConvention.cs @@ -49,6 +49,7 @@ public void ApplyConvention(EndpointBuilder eb) if (_collection != null && _collectionImportMap != null) { eb.Metadata.Add(_collection); + eb.Metadata.Add(new ResourcePreloadCollection(_collection)); if (_collectionUrl != null) { diff --git a/src/Components/Endpoints/src/Builder/ResourcePreloadCollection.cs b/src/Components/Endpoints/src/Builder/ResourcePreloadCollection.cs index e1aad76b280f..b34d2925ff13 100644 --- a/src/Components/Endpoints/src/Builder/ResourcePreloadCollection.cs +++ b/src/Components/Endpoints/src/Builder/ResourcePreloadCollection.cs @@ -8,13 +8,13 @@ namespace Microsoft.AspNetCore.Components.Endpoints; internal class ResourcePreloadCollection { - private readonly Dictionary _storage = new(); + private readonly Dictionary _storage = new(); public ResourcePreloadCollection(ResourceAssetCollection assets) { if (assets != null) { - var headers = new List<(string? Order, string Value)>(); + var headers = new List<(string? Group, string? Order, string Value)>(); foreach (var asset in assets) { if (asset.Properties == null) @@ -71,11 +71,14 @@ public ResourcePreloadCollection(ResourceAssetCollection assets) if (header != null) { - headers.Add((order, header)); + headers.Add((group, order, header)); } } - headers.Sort((a, b) => string.Compare(a.Order, b.Order, StringComparison.InvariantCulture)); + foreach (var group in headers.GroupBy(h => h.Group)) + { + _storage[group.Key ?? string.Empty] = group.OrderBy(h => h.Order).Select(h => h.Value).ToArray(); + } } } From c25094224c5b397bb342444d36f346a13c3377b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fi=C5=A1era?= Date: Tue, 1 Apr 2025 13:56:10 +0200 Subject: [PATCH 07/11] wip --- .../src/Builder/ResourcePreloadCollection.cs | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/Components/Endpoints/src/Builder/ResourcePreloadCollection.cs b/src/Components/Endpoints/src/Builder/ResourcePreloadCollection.cs index b34d2925ff13..36b01c32b1fa 100644 --- a/src/Components/Endpoints/src/Builder/ResourcePreloadCollection.cs +++ b/src/Components/Endpoints/src/Builder/ResourcePreloadCollection.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Linq; +using System.Text; using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Components.Endpoints; @@ -23,45 +24,48 @@ public ResourcePreloadCollection(ResourceAssetCollection assets) } // Use preloadgroup=webassembly to identify assets that should to be preloaded - string? header = null; string? group = null; foreach (var property in asset.Properties) { if (property.Name.Equals("preloadgroup", StringComparison.OrdinalIgnoreCase)) { - group = property.Value; - header = $"<{asset.Url}>"; + group = property.Value ?? string.Empty; break; } } - if (header == null) + if (group == null) { continue; } + var header = new StringBuilder(); + header.Append('<'); + header.Append(asset.Url); + header.Append('>'); + string? order = null; foreach (var property in asset.Properties) { if (property.Name.Equals("preloadrel", StringComparison.OrdinalIgnoreCase)) { - header = String.Concat(header, "; rel=", property.Value); + header.Append("; rel=").Append(property.Value); } else if (property.Name.Equals("preloadas", StringComparison.OrdinalIgnoreCase)) { - header = String.Concat(header, "; as=", property.Value); + header.Append("; as=").Append(property.Value); } else if (property.Name.Equals("preloadpriority", StringComparison.OrdinalIgnoreCase)) { - header = String.Concat(header, "; fetchpriority=", property.Value); + header.Append("; fetchpriority=").Append(property.Value); } else if (property.Name.Equals("preloadcrossorigin", StringComparison.OrdinalIgnoreCase)) { - header = String.Concat(header, "; crossorigin=", property.Value); + header.Append("; crossorigin=").Append(property.Value); } else if (property.Name.Equals("integrity", StringComparison.OrdinalIgnoreCase)) { - header = String.Concat(header, "; integrity=\"", property.Value, "\""); + header.Append("; integrity=\"").Append(property.Value).Append('"'); } else if (property.Name.Equals("preloadorder", StringComparison.OrdinalIgnoreCase)) { @@ -71,7 +75,7 @@ public ResourcePreloadCollection(ResourceAssetCollection assets) if (header != null) { - headers.Add((group, order, header)); + headers.Add((group, order, header.ToString())); } } From e845ad72ce5098d1191c8d7b31290706caf8c84a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fi=C5=A1era?= Date: Tue, 1 Apr 2025 14:08:03 +0200 Subject: [PATCH 08/11] Fix style rules --- .../Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs index a018f3071b9d..79239c6d6db2 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; -using System.Linq; -using System.Reflection.PortableExecutable; using System.Runtime.InteropServices; using System.Text; using System.Text.Encodings.Web; From 145cc1884d2fb7934eeb45942171e9665a1c04d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fi=C5=A1era?= Date: Wed, 2 Apr 2025 10:42:05 +0200 Subject: [PATCH 09/11] Update test --- .../test/EndpointHtmlRendererTest.cs | 64 ++++++++++--------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs b/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs index e35759c9ae0d..e54298c42208 100644 --- a/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs +++ b/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs @@ -81,38 +81,40 @@ public async Task CanPreload_WebAssembly_ResourceAssets() new Endpoint( ctx => Task.CompletedTask, new EndpointMetadataCollection([ - new ResourceAssetCollection([ - new ResourceAsset("second.js", [ - new ResourceAssetProperty("preloadrel", "preload"), - new ResourceAssetProperty("preloadas", "script"), - new ResourceAssetProperty("preloadpriority", "high"), - new ResourceAssetProperty("preloadcrossorigin", "anonymous"), - new ResourceAssetProperty("integrity", "abcd"), - new ResourceAssetProperty("preloadorder", "2"), - new ResourceAssetProperty("preloadgroup", "webassembly") - ]), - new ResourceAsset("first.js", [ - new ResourceAssetProperty("preloadrel", "preload"), - new ResourceAssetProperty("preloadas", "script"), - new ResourceAssetProperty("preloadpriority", "high"), - new ResourceAssetProperty("preloadcrossorigin", "anonymous"), - new ResourceAssetProperty("integrity", "abcd"), - new ResourceAssetProperty("preloadorder", "1"), - new ResourceAssetProperty("preloadgroup", "webassembly") - ]), - new ResourceAsset("preload-nowebassembly.js", [ - new ResourceAssetProperty("preloadrel", "preload"), - new ResourceAssetProperty("preloadas", "script"), - new ResourceAssetProperty("preloadpriority", "high"), - new ResourceAssetProperty("preloadcrossorigin", "anonymous"), - new ResourceAssetProperty("integrity", "abcd"), - new ResourceAssetProperty("preloadorder", "1"), - new ResourceAssetProperty("preloadgroup", "abcd") - ]), - new ResourceAsset("nopreload.js", [ - new ResourceAssetProperty("integrity", "abcd") + new ResourcePreloadCollection( + new ResourceAssetCollection([ + new ResourceAsset("second.js", [ + new ResourceAssetProperty("preloadrel", "preload"), + new ResourceAssetProperty("preloadas", "script"), + new ResourceAssetProperty("preloadpriority", "high"), + new ResourceAssetProperty("preloadcrossorigin", "anonymous"), + new ResourceAssetProperty("integrity", "abcd"), + new ResourceAssetProperty("preloadorder", "2"), + new ResourceAssetProperty("preloadgroup", "webassembly") + ]), + new ResourceAsset("first.js", [ + new ResourceAssetProperty("preloadrel", "preload"), + new ResourceAssetProperty("preloadas", "script"), + new ResourceAssetProperty("preloadpriority", "high"), + new ResourceAssetProperty("preloadcrossorigin", "anonymous"), + new ResourceAssetProperty("integrity", "abcd"), + new ResourceAssetProperty("preloadorder", "1"), + new ResourceAssetProperty("preloadgroup", "webassembly") + ]), + new ResourceAsset("preload-nowebassembly.js", [ + new ResourceAssetProperty("preloadrel", "preload"), + new ResourceAssetProperty("preloadas", "script"), + new ResourceAssetProperty("preloadpriority", "high"), + new ResourceAssetProperty("preloadcrossorigin", "anonymous"), + new ResourceAssetProperty("integrity", "abcd"), + new ResourceAssetProperty("preloadorder", "1"), + new ResourceAssetProperty("preloadgroup", "abcd") + ]), + new ResourceAsset("nopreload.js", [ + new ResourceAssetProperty("integrity", "abcd") + ]) ]) - ]) + ) ]), "TestEndpoint" ) From ac7d33baadc97f0ca2049a11086c5c8d58ed74d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fi=C5=A1era?= Date: Wed, 2 Apr 2025 11:04:56 +0200 Subject: [PATCH 10/11] Consider order to be a number --- .../Endpoints/src/Builder/ResourcePreloadCollection.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Components/Endpoints/src/Builder/ResourcePreloadCollection.cs b/src/Components/Endpoints/src/Builder/ResourcePreloadCollection.cs index 36b01c32b1fa..5692915a2c59 100644 --- a/src/Components/Endpoints/src/Builder/ResourcePreloadCollection.cs +++ b/src/Components/Endpoints/src/Builder/ResourcePreloadCollection.cs @@ -15,7 +15,7 @@ public ResourcePreloadCollection(ResourceAssetCollection assets) { if (assets != null) { - var headers = new List<(string? Group, string? Order, string Value)>(); + var headers = new List<(string? Group, int Order, string Value)>(); foreach (var asset in assets) { if (asset.Properties == null) @@ -44,7 +44,7 @@ public ResourcePreloadCollection(ResourceAssetCollection assets) header.Append(asset.Url); header.Append('>'); - string? order = null; + int order = 0; foreach (var property in asset.Properties) { if (property.Name.Equals("preloadrel", StringComparison.OrdinalIgnoreCase)) @@ -69,7 +69,10 @@ public ResourcePreloadCollection(ResourceAssetCollection assets) } else if (property.Name.Equals("preloadorder", StringComparison.OrdinalIgnoreCase)) { - order = property.Value; + if (!int.TryParse(property.Value, out order)) + { + order = 0; + } } } From 836e2597c04c51811acdc04ebb2c756ac9b94ba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fi=C5=A1era?= Date: Thu, 3 Apr 2025 15:16:53 +0200 Subject: [PATCH 11/11] Feedback --- .../src/Builder/ResourcePreloadCollection.cs | 128 +++++++++--------- 1 file changed, 67 insertions(+), 61 deletions(-) diff --git a/src/Components/Endpoints/src/Builder/ResourcePreloadCollection.cs b/src/Components/Endpoints/src/Builder/ResourcePreloadCollection.cs index 5692915a2c59..083deac91a3d 100644 --- a/src/Components/Endpoints/src/Builder/ResourcePreloadCollection.cs +++ b/src/Components/Endpoints/src/Builder/ResourcePreloadCollection.cs @@ -13,80 +13,86 @@ internal class ResourcePreloadCollection public ResourcePreloadCollection(ResourceAssetCollection assets) { - if (assets != null) + var headerBuilder = new StringBuilder(); + var headers = new Dictionary>(); + foreach (var asset in assets) { - var headers = new List<(string? Group, int Order, string Value)>(); - foreach (var asset in assets) + if (asset.Properties == null) { - if (asset.Properties == null) - { - continue; - } + continue; + } - // Use preloadgroup=webassembly to identify assets that should to be preloaded - string? group = null; - foreach (var property in asset.Properties) + // Use preloadgroup property to identify assets that should be preloaded + string? group = null; + foreach (var property in asset.Properties) + { + if (property.Name.Equals("preloadgroup", StringComparison.OrdinalIgnoreCase)) { - if (property.Name.Equals("preloadgroup", StringComparison.OrdinalIgnoreCase)) - { - group = property.Value ?? string.Empty; - break; - } + group = property.Value ?? string.Empty; + break; } + } - if (group == null) - { - continue; - } + if (group == null) + { + continue; + } - var header = new StringBuilder(); - header.Append('<'); - header.Append(asset.Url); - header.Append('>'); + var header = CreateHeader(headerBuilder, asset.Url, asset.Properties); + if (!headers.TryGetValue(group, out var groupHeaders)) + { + groupHeaders = headers[group] = new List<(int Order, string Value)>(); + } - int order = 0; - foreach (var property in asset.Properties) - { - if (property.Name.Equals("preloadrel", StringComparison.OrdinalIgnoreCase)) - { - header.Append("; rel=").Append(property.Value); - } - else if (property.Name.Equals("preloadas", StringComparison.OrdinalIgnoreCase)) - { - header.Append("; as=").Append(property.Value); - } - else if (property.Name.Equals("preloadpriority", StringComparison.OrdinalIgnoreCase)) - { - header.Append("; fetchpriority=").Append(property.Value); - } - else if (property.Name.Equals("preloadcrossorigin", StringComparison.OrdinalIgnoreCase)) - { - header.Append("; crossorigin=").Append(property.Value); - } - else if (property.Name.Equals("integrity", StringComparison.OrdinalIgnoreCase)) - { - header.Append("; integrity=\"").Append(property.Value).Append('"'); - } - else if (property.Name.Equals("preloadorder", StringComparison.OrdinalIgnoreCase)) - { - if (!int.TryParse(property.Value, out order)) - { - order = 0; - } - } - } + groupHeaders.Add(header); + } - if (header != null) - { - headers.Add((group, order, header.ToString())); - } - } + foreach (var group in headers) + { + _storage[group.Key ?? string.Empty] = group.Value.OrderBy(h => h.Order).Select(h => h.Value).ToArray(); + } + } - foreach (var group in headers.GroupBy(h => h.Group)) + private static (int order, string header) CreateHeader(StringBuilder headerBuilder, string url, IEnumerable properties) + { + headerBuilder.Clear(); + headerBuilder.Append('<'); + headerBuilder.Append(url); + headerBuilder.Append('>'); + + int order = 0; + foreach (var property in properties) + { + if (property.Name.Equals("preloadrel", StringComparison.OrdinalIgnoreCase)) + { + headerBuilder.Append("; rel=").Append(property.Value); + } + else if (property.Name.Equals("preloadas", StringComparison.OrdinalIgnoreCase)) + { + headerBuilder.Append("; as=").Append(property.Value); + } + else if (property.Name.Equals("preloadpriority", StringComparison.OrdinalIgnoreCase)) { - _storage[group.Key ?? string.Empty] = group.OrderBy(h => h.Order).Select(h => h.Value).ToArray(); + headerBuilder.Append("; fetchpriority=").Append(property.Value); + } + else if (property.Name.Equals("preloadcrossorigin", StringComparison.OrdinalIgnoreCase)) + { + headerBuilder.Append("; crossorigin=").Append(property.Value); + } + else if (property.Name.Equals("integrity", StringComparison.OrdinalIgnoreCase)) + { + headerBuilder.Append("; integrity=\"").Append(property.Value).Append('"'); + } + else if (property.Name.Equals("preloadorder", StringComparison.OrdinalIgnoreCase)) + { + if (!int.TryParse(property.Value, out order)) + { + order = 0; + } } } + + return (order, headerBuilder.ToString()); } public bool TryGetLinkHeaders(string group, out StringValues linkHeaders)