From cb992ca9383ef21635047164aaff1a2a2a7b81c7 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Tue, 4 Jun 2024 20:26:49 +0200 Subject: [PATCH 01/30] tmp --- .../StaticAssetDevelopmentRuntimeHandler.cs | 6 +- .../StaticAssetsEndpointDataSourceHelper.cs | 47 ++++++++--- .../Microsoft.AspNetCore.StaticAssets.csproj | 1 + src/StaticAssets/src/PublicAPI.Unshipped.txt | 28 ++++++- src/StaticAssets/src/StaticAssetDescriptor.cs | 79 ++++++++++++++++--- .../src/StaticAssetEndpointDataSource.cs | 11 ++- .../src/StaticAssetEndpointFactory.cs | 4 +- ...ointProperty.cs => StaticAssetProperty.cs} | 13 ++- ...Header.cs => StaticAssetResponseHeader.cs} | 15 +++- src/StaticAssets/src/StaticAssetSelector.cs | 21 ++++- src/StaticAssets/src/StaticAssetsInvoker.cs | 4 +- src/StaticAssets/src/StaticAssetsManifest.cs | 1 + .../test/StaticAssetsIntegrationTests.cs | 46 +++++------ 13 files changed, 217 insertions(+), 59 deletions(-) rename src/StaticAssets/src/{EndpointProperty.cs => StaticAssetProperty.cs} (60%) rename src/StaticAssets/src/{ResponseHeader.cs => StaticAssetResponseHeader.cs} (50%) diff --git a/src/StaticAssets/src/Development/StaticAssetDevelopmentRuntimeHandler.cs b/src/StaticAssets/src/Development/StaticAssetDevelopmentRuntimeHandler.cs index 95d59b6437e3..1f7b73d87dec 100644 --- a/src/StaticAssets/src/Development/StaticAssetDevelopmentRuntimeHandler.cs +++ b/src/StaticAssets/src/Development/StaticAssetDevelopmentRuntimeHandler.cs @@ -42,7 +42,7 @@ public void AttachRuntimePatching(EndpointBuilder builder) builder.RequestDelegate = async context => { var originalFeature = context.Features.GetRequiredFeature(); - var fileInfo = context.RequestServices.GetRequiredService().WebRootFileProvider.GetFileInfo(asset.AssetFile); + var fileInfo = context.RequestServices.GetRequiredService().WebRootFileProvider.GetFileInfo(asset.AssetPath); if (fileInfo.Length != asset.GetContentLength() || fileInfo.LastModified != asset.GetLastModified()) { // At this point, we know that the file has changed from what was generated at build time. @@ -93,12 +93,12 @@ public void DisableBuffering() public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellationToken = default) { - var fileInfo = _context.RequestServices.GetRequiredService().WebRootFileProvider.GetFileInfo(_asset.AssetFile); + var fileInfo = _context.RequestServices.GetRequiredService().WebRootFileProvider.GetFileInfo(_asset.AssetPath); var endpoint = _context.GetEndpoint()!; var assetDescriptor = endpoint.Metadata.OfType().Single(); _context.Response.Headers.ETag = ""; - if (assetDescriptor.AssetFile != _asset.AssetFile) + if (assetDescriptor.AssetPath != _asset.AssetPath) { // This was a compressed asset, asset contains the path to the original file, we'll re-compress the asset on the fly and replace the body // and the content length. diff --git a/src/StaticAssets/src/Infrastructure/StaticAssetsEndpointDataSourceHelper.cs b/src/StaticAssets/src/Infrastructure/StaticAssetsEndpointDataSourceHelper.cs index a89d556ac496..a72bda103a35 100644 --- a/src/StaticAssets/src/Infrastructure/StaticAssetsEndpointDataSourceHelper.cs +++ b/src/StaticAssets/src/Infrastructure/StaticAssetsEndpointDataSourceHelper.cs @@ -15,22 +15,51 @@ public static class StaticAssetsEndpointDataSourceHelper /// /// For internal framework use only. /// - public static bool IsStaticAssetsDataSource(EndpointDataSource dataSource, string? staticAssetsManifestPath = null) + public static bool HasStaticAssetsDataSource(IEndpointRouteBuilder builder, string? staticAssetsManifestPath = null) { - if (dataSource is StaticAssetsEndpointDataSource staticAssetsDataSource) + staticAssetsManifestPath = ApplyStaticAssetManifestPathConventions(staticAssetsManifestPath, builder.ServiceProvider); + foreach (var dataSource in builder.DataSources) { - if (staticAssetsManifestPath is null) + if (dataSource is StaticAssetsEndpointDataSource staticAssetsDataSource) { - var serviceProvider = staticAssetsDataSource.ServiceProvider; - var environment = serviceProvider.GetRequiredService(); - staticAssetsManifestPath = Path.Combine(AppContext.BaseDirectory, $"{environment.ApplicationName}.staticwebassets.endpoints.json"); + if (string.Equals(staticAssetsDataSource.ManifestPath, staticAssetsManifestPath, StringComparison.Ordinal)) + { + return true; + } } + } - staticAssetsManifestPath = Path.IsPathRooted(staticAssetsManifestPath) ? staticAssetsManifestPath : Path.Combine(AppContext.BaseDirectory, staticAssetsManifestPath); + return false; + } - return string.Equals(staticAssetsDataSource.ManifestPath, staticAssetsManifestPath, StringComparison.Ordinal); + /// + /// For internal framework use only. + /// + public static IReadOnlyList ResolveStaticAssetDescriptors( + IEndpointRouteBuilder endpointRouteBuilder, + string? manifestPath) + { + manifestPath = ApplyStaticAssetManifestPathConventions(manifestPath, endpointRouteBuilder.ServiceProvider); + foreach (var dataSource in endpointRouteBuilder.DataSources) + { + if (dataSource is StaticAssetsEndpointDataSource staticAssetsDataSource && + string.Equals(staticAssetsDataSource.ManifestPath, manifestPath, StringComparison.Ordinal)) + { + return staticAssetsDataSource.Descriptors; + } } - return false; + return []; + } + + internal static string ApplyStaticAssetManifestPathConventions(string? staticAssetsManifestPath, IServiceProvider services) + { + if (staticAssetsManifestPath is null) + { + var environment = services.GetRequiredService(); + return Path.Combine(AppContext.BaseDirectory, $"{environment.ApplicationName}.staticwebassets.endpoints.json"); + } + + return Path.IsPathRooted(staticAssetsManifestPath) ? staticAssetsManifestPath : Path.Combine(AppContext.BaseDirectory, staticAssetsManifestPath); } } diff --git a/src/StaticAssets/src/Microsoft.AspNetCore.StaticAssets.csproj b/src/StaticAssets/src/Microsoft.AspNetCore.StaticAssets.csproj index c385473e7147..c5a47fce08f1 100644 --- a/src/StaticAssets/src/Microsoft.AspNetCore.StaticAssets.csproj +++ b/src/StaticAssets/src/Microsoft.AspNetCore.StaticAssets.csproj @@ -24,4 +24,5 @@ + diff --git a/src/StaticAssets/src/PublicAPI.Unshipped.txt b/src/StaticAssets/src/PublicAPI.Unshipped.txt index 8cf8331aa2ad..ded8dc823703 100644 --- a/src/StaticAssets/src/PublicAPI.Unshipped.txt +++ b/src/StaticAssets/src/PublicAPI.Unshipped.txt @@ -1,7 +1,33 @@ Microsoft.AspNetCore.Builder.StaticAssetsEndpointRouteBuilderExtensions Microsoft.AspNetCore.StaticAssets.Infrastructure.StaticAssetsEndpointDataSourceHelper +Microsoft.AspNetCore.StaticAssets.StaticAssetDescriptor +Microsoft.AspNetCore.StaticAssets.StaticAssetDescriptor.AssetPath.get -> string! +Microsoft.AspNetCore.StaticAssets.StaticAssetDescriptor.AssetPath.set -> void +Microsoft.AspNetCore.StaticAssets.StaticAssetDescriptor.Properties.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.AspNetCore.StaticAssets.StaticAssetDescriptor.Properties.set -> void +Microsoft.AspNetCore.StaticAssets.StaticAssetDescriptor.ResponseHeaders.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.AspNetCore.StaticAssets.StaticAssetDescriptor.ResponseHeaders.set -> void +Microsoft.AspNetCore.StaticAssets.StaticAssetDescriptor.Route.get -> string! +Microsoft.AspNetCore.StaticAssets.StaticAssetDescriptor.Route.set -> void +Microsoft.AspNetCore.StaticAssets.StaticAssetDescriptor.Selectors.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.AspNetCore.StaticAssets.StaticAssetDescriptor.Selectors.set -> void +Microsoft.AspNetCore.StaticAssets.StaticAssetDescriptor.StaticAssetDescriptor() -> void +Microsoft.AspNetCore.StaticAssets.StaticAssetProperty +Microsoft.AspNetCore.StaticAssets.StaticAssetProperty.Name.get -> string! +Microsoft.AspNetCore.StaticAssets.StaticAssetProperty.StaticAssetProperty(string! name, string! value) -> void +Microsoft.AspNetCore.StaticAssets.StaticAssetProperty.Value.get -> string! +Microsoft.AspNetCore.StaticAssets.StaticAssetResponseHeader +Microsoft.AspNetCore.StaticAssets.StaticAssetResponseHeader.Name.get -> string! +Microsoft.AspNetCore.StaticAssets.StaticAssetResponseHeader.StaticAssetResponseHeader(string! name, string! value) -> void +Microsoft.AspNetCore.StaticAssets.StaticAssetResponseHeader.Value.get -> string! +Microsoft.AspNetCore.StaticAssets.StaticAssetSelector +Microsoft.AspNetCore.StaticAssets.StaticAssetSelector.Name.get -> string! +Microsoft.AspNetCore.StaticAssets.StaticAssetSelector.Quality.get -> string! +Microsoft.AspNetCore.StaticAssets.StaticAssetSelector.StaticAssetSelector(string! name, string! value, string! quality) -> void +Microsoft.AspNetCore.StaticAssets.StaticAssetSelector.Value.get -> string! Microsoft.AspNetCore.StaticAssets.StaticAssetsEndpointConventionBuilder Microsoft.AspNetCore.StaticAssets.StaticAssetsEndpointConventionBuilder.Add(System.Action! convention) -> void Microsoft.AspNetCore.StaticAssets.StaticAssetsEndpointConventionBuilder.Finally(System.Action! convention) -> void static Microsoft.AspNetCore.Builder.StaticAssetsEndpointRouteBuilderExtensions.MapStaticAssets(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string? staticAssetsManifestPath = null) -> Microsoft.AspNetCore.StaticAssets.StaticAssetsEndpointConventionBuilder! -static Microsoft.AspNetCore.StaticAssets.Infrastructure.StaticAssetsEndpointDataSourceHelper.IsStaticAssetsDataSource(Microsoft.AspNetCore.Routing.EndpointDataSource! dataSource, string? staticAssetsManifestPath = null) -> bool +static Microsoft.AspNetCore.StaticAssets.Infrastructure.StaticAssetsEndpointDataSourceHelper.HasStaticAssetsDataSource(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! builder, string? staticAssetsManifestPath = null) -> bool +static Microsoft.AspNetCore.StaticAssets.Infrastructure.StaticAssetsEndpointDataSourceHelper.ResolveStaticAssetDescriptors(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpointRouteBuilder, string? manifestPath) -> System.Collections.Generic.IReadOnlyList! diff --git a/src/StaticAssets/src/StaticAssetDescriptor.cs b/src/StaticAssets/src/StaticAssetDescriptor.cs index c34150117c82..11fb3e2b709d 100644 --- a/src/StaticAssets/src/StaticAssetDescriptor.cs +++ b/src/StaticAssets/src/StaticAssetDescriptor.cs @@ -2,26 +2,79 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Text.Json.Serialization; namespace Microsoft.AspNetCore.StaticAssets; -// Represents a static resource. +/// +/// The description of a static asset that was generated during the build process. +/// [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")] -internal sealed class StaticAssetDescriptor( - string route, - string assetFile, - StaticAssetSelector[] selectors, - EndpointProperty[] endpointProperties, - ResponseHeader[] responseHeaders) +public sealed class StaticAssetDescriptor { - public string Route { get; } = route; - public string AssetFile { get; } = assetFile; - public StaticAssetSelector[] Selectors { get; } = selectors; - public EndpointProperty[] EndpointProperties { get; } = endpointProperties; - public ResponseHeader[] ResponseHeaders { get; } = responseHeaders; + bool _isFrozen; + private string? _route; + private string? _assetFile; + private IReadOnlyList _selectors = []; + private IReadOnlyList _endpointProperties = []; + private IReadOnlyList _responseHeaders = []; + + /// + /// The route that the asset is served from. + /// + public required string Route + { + get => _route ?? throw new InvalidOperationException("Route is required"); + set => _route = !_isFrozen ? value : throw new InvalidOperationException("StaticAssetDescriptor is frozen and doesn't accept further changes"); + } + + /// + /// The path to the asset file from the wwwroot folder. + /// + [JsonPropertyName("AssetFile")] + public required string AssetPath + { + get => _assetFile ?? throw new InvalidOperationException("AssetPath is required"); + set => _assetFile = !_isFrozen ? value : throw new InvalidOperationException("StaticAssetDescriptor is frozen and doesn't accept further changes"); + } + + /// + /// A list of selectors that are used to discriminate between two or more assets with the same route. + /// + [JsonPropertyName("Selectors")] + public IReadOnlyList Selectors + { + get => _selectors; + set => _selectors = !_isFrozen ? value : throw new InvalidOperationException("StaticAssetDescriptor is frozen and doesn't accept further changes"); + } + + /// + /// A list of properties that are associated with the endpoint. + /// + [JsonPropertyName("EndpointProperties")] + public IReadOnlyList Properties + { + get => _endpointProperties; + set => _endpointProperties = !_isFrozen ? value : throw new InvalidOperationException("StaticAssetDescriptor is frozen and doesn't accept further changes"); + } + + /// + /// A list of headers to apply to the response when this resource is served. + /// + [JsonPropertyName("ResponseHeaders")] + public IReadOnlyList ResponseHeaders + { + get => _responseHeaders; + set => _responseHeaders = !_isFrozen ? value : throw new InvalidOperationException("StaticAssetDescriptor is frozen and doesn't accept further changes"); + } private string GetDebuggerDisplay() { - return $"Route: {Route} Path: {AssetFile}"; + return $"Route: {Route} Path: {AssetPath}"; + } + + internal void Freeze() + { + _isFrozen = true; } } diff --git a/src/StaticAssets/src/StaticAssetEndpointDataSource.cs b/src/StaticAssets/src/StaticAssetEndpointDataSource.cs index 8c8968dc19ed..644293976045 100644 --- a/src/StaticAssets/src/StaticAssetEndpointDataSource.cs +++ b/src/StaticAssets/src/StaticAssetEndpointDataSource.cs @@ -17,6 +17,7 @@ internal class StaticAssetsEndpointDataSource : EndpointDataSource { private readonly object _lock = new(); private readonly StaticAssetsManifest _manifest; + private readonly List _descriptors; private readonly StaticAssetEndpointFactory _endpointFactory; private readonly List> _conventions = []; private readonly List> _finallyConventions = []; @@ -24,10 +25,16 @@ internal class StaticAssetsEndpointDataSource : EndpointDataSource private CancellationTokenSource _cancellationTokenSource; private CancellationChangeToken _changeToken; - internal StaticAssetsEndpointDataSource(IServiceProvider serviceProvider, StaticAssetsManifest manifest, StaticAssetEndpointFactory endpointFactory, string manifestName, List descriptors) + internal StaticAssetsEndpointDataSource( + IServiceProvider serviceProvider, + StaticAssetsManifest manifest, + StaticAssetEndpointFactory endpointFactory, + string manifestName, + List descriptors) { ServiceProvider = serviceProvider; _manifest = manifest; + _descriptors = descriptors; ManifestPath = manifestName; _endpointFactory = endpointFactory; _cancellationTokenSource = new CancellationTokenSource(); @@ -45,6 +52,8 @@ internal StaticAssetsEndpointDataSource(IServiceProvider serviceProvider, Static /// public string ManifestPath { get; } + internal IReadOnlyList Descriptors => _descriptors; + /// internal StaticAssetsEndpointConventionBuilder DefaultBuilder { get; set; } diff --git a/src/StaticAssets/src/StaticAssetEndpointFactory.cs b/src/StaticAssets/src/StaticAssetEndpointFactory.cs index 9340fe9cf33a..fdc39cb66874 100644 --- a/src/StaticAssets/src/StaticAssetEndpointFactory.cs +++ b/src/StaticAssets/src/StaticAssetEndpointFactory.cs @@ -39,8 +39,8 @@ public Endpoint Create(StaticAssetDescriptor resource, List>(); - var fileInfo = serviceProvider.GetRequiredService().WebRootFileProvider.GetFileInfo(resource.AssetFile) ?? - throw new InvalidOperationException($"The file '{resource.AssetFile}' could not be found."); + var fileInfo = serviceProvider.GetRequiredService().WebRootFileProvider.GetFileInfo(resource.AssetPath) ?? + throw new InvalidOperationException($"The file '{resource.AssetPath}' could not be found."); var invoker = new StaticAssetsInvoker(resource, fileInfo, logger); diff --git a/src/StaticAssets/src/EndpointProperty.cs b/src/StaticAssets/src/StaticAssetProperty.cs similarity index 60% rename from src/StaticAssets/src/EndpointProperty.cs rename to src/StaticAssets/src/StaticAssetProperty.cs index 944dcd61ca6c..e25157c0abf8 100644 --- a/src/StaticAssets/src/EndpointProperty.cs +++ b/src/StaticAssets/src/StaticAssetProperty.cs @@ -5,11 +5,20 @@ namespace Microsoft.AspNetCore.StaticAssets; -// Represents a property of an endpoint. +/// +/// A property associated with a static asset. +/// [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")] -internal sealed class EndpointProperty(string name, string value) +public sealed class StaticAssetProperty(string name, string value) { + /// + /// The name of the property. + /// public string Name { get; } = name; + + /// + /// The value of the property. + /// public string Value { get; } = value; private string GetDebuggerDisplay() => $"Name: {Name} Value:{Value}"; diff --git a/src/StaticAssets/src/ResponseHeader.cs b/src/StaticAssets/src/StaticAssetResponseHeader.cs similarity index 50% rename from src/StaticAssets/src/ResponseHeader.cs rename to src/StaticAssets/src/StaticAssetResponseHeader.cs index 0c57a463331c..3a1c9b7b0410 100644 --- a/src/StaticAssets/src/ResponseHeader.cs +++ b/src/StaticAssets/src/StaticAssetResponseHeader.cs @@ -5,11 +5,22 @@ namespace Microsoft.AspNetCore.StaticAssets; -// Represents a response header for a static resource. +/// +/// A response header to apply to the response when a static asset is served. +/// +/// The name of the header. +/// The value of the header. [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")] -internal sealed class ResponseHeader(string name, string value) +public sealed class StaticAssetResponseHeader(string name, string value) { + /// + /// The name of the header. + /// public string Name { get; } = name; + + /// + /// The value of the header. + /// public string Value { get; } = value; private string GetDebuggerDisplay() => $"Name: {Name} Value: {Value}"; diff --git a/src/StaticAssets/src/StaticAssetSelector.cs b/src/StaticAssets/src/StaticAssetSelector.cs index e155e387fbf9..b3218fb67863 100644 --- a/src/StaticAssets/src/StaticAssetSelector.cs +++ b/src/StaticAssets/src/StaticAssetSelector.cs @@ -5,12 +5,29 @@ namespace Microsoft.AspNetCore.StaticAssets; -// Represents a selector for a static resource. +/// +/// A static asset selector. Selectors are used to discriminate between two or more assets with the same route. +/// +/// The name associated to the selector. +/// The value associated to the selector and used to match against incoming requests. +/// The static server quality associated to this selector. [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")] -internal sealed class StaticAssetSelector(string name, string value, string quality) +public sealed class StaticAssetSelector(string name, string value, string quality) { + /// + /// The name associated to the selector. + /// public string Name { get; } = name; + + /// + /// The value associated to the selector and used to match against incoming requests. + /// public string Value { get; } = value; + + /// + /// The static server quality associated to this selector. Used to break ties when a request matches multiple values + /// with the same degree of specificity. + /// public string Quality { get; } = quality; private string GetDebuggerDisplay() => $"Name: {Name} Value: {Value} Quality: {Quality}"; diff --git a/src/StaticAssets/src/StaticAssetsInvoker.cs b/src/StaticAssets/src/StaticAssetsInvoker.cs index e43ed498c6c4..13265e16a12f 100644 --- a/src/StaticAssets/src/StaticAssetsInvoker.cs +++ b/src/StaticAssets/src/StaticAssetsInvoker.cs @@ -24,7 +24,7 @@ internal class StaticAssetsInvoker private readonly EntityTagHeaderValue _etag; private readonly long _length; private readonly DateTimeOffset _lastModified; - private readonly List _remainingHeaders; + private readonly List _remainingHeaders; public StaticAssetsInvoker(StaticAssetDescriptor resource, IFileInfo fileInfo, ILogger logger) { @@ -46,7 +46,7 @@ public StaticAssetsInvoker(StaticAssetDescriptor resource, IFileInfo fileInfo, I { if (_etag != null) { - _remainingHeaders.Add(new ResponseHeader("ETag", _etag.ToString())); + _remainingHeaders.Add(new StaticAssetResponseHeader("ETag", _etag.ToString())); } _etag = EntityTagHeaderValue.Parse(etag); diff --git a/src/StaticAssets/src/StaticAssetsManifest.cs b/src/StaticAssets/src/StaticAssetsManifest.cs index 0bb70b10246e..b107aa46e384 100644 --- a/src/StaticAssets/src/StaticAssetsManifest.cs +++ b/src/StaticAssets/src/StaticAssetsManifest.cs @@ -43,5 +43,6 @@ internal StaticAssetsEndpointDataSource CreateDataSource(IEndpointRouteBuilder e } public int Version { get; set; } + public List Endpoints { get; set; } = []; } diff --git a/src/StaticAssets/test/StaticAssetsIntegrationTests.cs b/src/StaticAssets/test/StaticAssetsIntegrationTests.cs index 9314865bd764..0e35425d9029 100644 --- a/src/StaticAssets/test/StaticAssetsIntegrationTests.cs +++ b/src/StaticAssets/test/StaticAssetsIntegrationTests.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; -using System.IO; using System.IO.Compression; using System.Net; using System.Net.Http; @@ -276,31 +275,33 @@ private static void CreateTestManifest(string appName, string webRoot, params Sp var lastModified = DateTimeOffset.UtcNow; File.WriteAllText(filePath, resource.Content); - manifest.Endpoints.Add(new StaticAssetDescriptor( - resource.Path, - resource.Path, - [], - [], - [ + manifest.Endpoints.Add(new StaticAssetDescriptor + { + Route = resource.Path, + AssetPath = resource.Path, + Selectors = [], + Properties = [], + ResponseHeaders = [ new ("Accept-Ranges", "bytes"), new("Content-Length", resource.Content.Length.ToString(CultureInfo.InvariantCulture)), new("Content-Type", GetContentType(filePath)), new ("ETag", $"\"{GetEtag(resource.Content)}\""), new("Last-Modified", lastModified.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture)) ] - )); + }); if (resource.IncludeCompressedVersion) { var compressedFilePath = Path.Combine(webRoot, resource.Path + ".gz"); var length = CreateCompressedFile(compressedFilePath, resource); - manifest.Endpoints.Add(new StaticAssetDescriptor( - resource.Path, - $"{resource.Path}.gz", - [new StaticAssetSelector("Content-Encoding", "gzip", "1.0")], - [], - [ + manifest.Endpoints.Add(new StaticAssetDescriptor + { + Route = resource.Path, + AssetPath = $"{resource.Path}.gz", + Selectors = [new StaticAssetSelector("Content-Encoding", "gzip", "1.0")], + Properties = [], + ResponseHeaders = [ new ("Accept-Ranges", "bytes"), new ("Content-Type", GetContentType(filePath)), @@ -312,7 +313,7 @@ [new StaticAssetSelector("Content-Encoding", "gzip", "1.0")], new ("Content-Encoding", "gzip"), new ("Vary", "Accept-Encoding"), ] - )); + }); } } using var stream = File.Create(manifestPath); @@ -352,19 +353,20 @@ private static async Task CreateClient() { Version = 1 }; - manifest.Endpoints.Add(new StaticAssetDescriptor( - "sample.txt", - "sample.txt", - [], - [], - [ + manifest.Endpoints.Add(new StaticAssetDescriptor + { + Route = "sample.txt", + AssetPath = "sample.txt", + Selectors = [], + Properties = [], + ResponseHeaders = [ new ("Accept-Ranges", "bytes"), new("Content-Length", "Hello, World!".Length.ToString(CultureInfo.InvariantCulture)), new("Content-Type", GetContentType("sample.txt")), new ("ETag", $"\"{GetEtag("Hello, World!")}\""), new("Last-Modified", new DateTimeOffset(2023,03,03,0,0,0,TimeSpan.Zero).ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture)) ] - )); + }); var builder = WebApplication.CreateEmptyBuilder(new WebApplicationOptions { From eef9307b89ba93b109b3dca62519bc32609750d7 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Tue, 4 Jun 2024 20:35:49 +0200 Subject: [PATCH 02/30] Update samples --- .../BlazorUnitedApp.Client.csproj | 19 ++++++++ .../BlazorUnitedApp.Client/HelloWorld.razor | 3 ++ .../Samples/BlazorUnitedApp.Client/Program.cs | 8 ++++ .../Properties/launchSettings.json | 41 ++++++++++++++++++ .../BlazorUnitedApp.Client/_Imports.razor | 9 ++++ .../wwwroot/blazor-logo.png | Bin 0 -> 13226 bytes .../Samples/BlazorUnitedApp/App.razor | 6 ++- .../BlazorUnitedApp/BlazorUnitedApp.csproj | 5 +++ .../Pages/WebAssemblyComponent.razor | 4 ++ .../Samples/BlazorUnitedApp/Program.cs | 4 +- .../Properties/launchSettings.json | 3 ++ .../BlazorUnitedApp/Shared/NavMenu.razor | 5 +++ src/Mvc/samples/MvcSandbox/Startup.cs | 9 ++-- .../MvcSandbox/Views/Shared/_Layout.cshtml | 1 + 14 files changed, 111 insertions(+), 6 deletions(-) create mode 100644 src/Components/Samples/BlazorUnitedApp.Client/BlazorUnitedApp.Client.csproj create mode 100644 src/Components/Samples/BlazorUnitedApp.Client/HelloWorld.razor create mode 100644 src/Components/Samples/BlazorUnitedApp.Client/Program.cs create mode 100644 src/Components/Samples/BlazorUnitedApp.Client/Properties/launchSettings.json create mode 100644 src/Components/Samples/BlazorUnitedApp.Client/_Imports.razor create mode 100644 src/Components/Samples/BlazorUnitedApp.Client/wwwroot/blazor-logo.png create mode 100644 src/Components/Samples/BlazorUnitedApp/Pages/WebAssemblyComponent.razor diff --git a/src/Components/Samples/BlazorUnitedApp.Client/BlazorUnitedApp.Client.csproj b/src/Components/Samples/BlazorUnitedApp.Client/BlazorUnitedApp.Client.csproj new file mode 100644 index 000000000000..a9a7bc04f7ad --- /dev/null +++ b/src/Components/Samples/BlazorUnitedApp.Client/BlazorUnitedApp.Client.csproj @@ -0,0 +1,19 @@ + + + + $(DefaultNetCoreTargetFramework) + false + enable + Default + + + + + + + + + + + + diff --git a/src/Components/Samples/BlazorUnitedApp.Client/HelloWorld.razor b/src/Components/Samples/BlazorUnitedApp.Client/HelloWorld.razor new file mode 100644 index 000000000000..65954dbb3bc4 --- /dev/null +++ b/src/Components/Samples/BlazorUnitedApp.Client/HelloWorld.razor @@ -0,0 +1,3 @@ +

Hello assets!

+ +Blazor Logo diff --git a/src/Components/Samples/BlazorUnitedApp.Client/Program.cs b/src/Components/Samples/BlazorUnitedApp.Client/Program.cs new file mode 100644 index 000000000000..d380861a8e68 --- /dev/null +++ b/src/Components/Samples/BlazorUnitedApp.Client/Program.cs @@ -0,0 +1,8 @@ +// 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.Components.WebAssembly.Hosting; + +var builder = WebAssemblyHostBuilder.CreateDefault(args); + +await builder.Build().RunAsync(); diff --git a/src/Components/Samples/BlazorUnitedApp.Client/Properties/launchSettings.json b/src/Components/Samples/BlazorUnitedApp.Client/Properties/launchSettings.json new file mode 100644 index 000000000000..e7eeef644670 --- /dev/null +++ b/src/Components/Samples/BlazorUnitedApp.Client/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:59453", + "sslPort": 44337 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "http://localhost:5023", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "https://localhost:7110;http://localhost:5023", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Components/Samples/BlazorUnitedApp.Client/_Imports.razor b/src/Components/Samples/BlazorUnitedApp.Client/_Imports.razor new file mode 100644 index 000000000000..5087a9eb772a --- /dev/null +++ b/src/Components/Samples/BlazorUnitedApp.Client/_Imports.razor @@ -0,0 +1,9 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.AspNetCore.Components.WebAssembly.Http +@using Microsoft.JSInterop +@using BlazorUnitedApp.Client diff --git a/src/Components/Samples/BlazorUnitedApp.Client/wwwroot/blazor-logo.png b/src/Components/Samples/BlazorUnitedApp.Client/wwwroot/blazor-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..af3641ce89b18015bdd3e36c76c14e20017082e0 GIT binary patch literal 13226 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 - + + + - + diff --git a/src/Components/Samples/BlazorUnitedApp/BlazorUnitedApp.csproj b/src/Components/Samples/BlazorUnitedApp/BlazorUnitedApp.csproj index 5f540ab86959..62460261002b 100644 --- a/src/Components/Samples/BlazorUnitedApp/BlazorUnitedApp.csproj +++ b/src/Components/Samples/BlazorUnitedApp/BlazorUnitedApp.csproj @@ -6,10 +6,15 @@ enable + + + + + diff --git a/src/Components/Samples/BlazorUnitedApp/Pages/WebAssemblyComponent.razor b/src/Components/Samples/BlazorUnitedApp/Pages/WebAssemblyComponent.razor new file mode 100644 index 000000000000..1e78f8e001c2 --- /dev/null +++ b/src/Components/Samples/BlazorUnitedApp/Pages/WebAssemblyComponent.razor @@ -0,0 +1,4 @@ +@page "/webassembly" +@using BlazorUnitedApp.Client + + diff --git a/src/Components/Samples/BlazorUnitedApp/Program.cs b/src/Components/Samples/BlazorUnitedApp/Program.cs index aebb156ed98d..6492f3fb3e50 100644 --- a/src/Components/Samples/BlazorUnitedApp/Program.cs +++ b/src/Components/Samples/BlazorUnitedApp/Program.cs @@ -8,6 +8,7 @@ // Add services to the container. builder.Services.AddRazorComponents() + .AddInteractiveWebAssemblyComponents() .AddInteractiveServerComponents(); builder.Services.AddSingleton(); @@ -29,6 +30,7 @@ app.MapStaticAssets(); app.MapRazorComponents() - .AddInteractiveServerRenderMode(); + .AddInteractiveServerRenderMode() + .AddInteractiveWebAssemblyRenderMode(); app.Run(); diff --git a/src/Components/Samples/BlazorUnitedApp/Properties/launchSettings.json b/src/Components/Samples/BlazorUnitedApp/Properties/launchSettings.json index e9f3bc751ea5..fc639e6af0ed 100644 --- a/src/Components/Samples/BlazorUnitedApp/Properties/launchSettings.json +++ b/src/Components/Samples/BlazorUnitedApp/Properties/launchSettings.json @@ -13,6 +13,7 @@ "dotnetRunMessages": true, "launchBrowser": true, "applicationUrl": "http://localhost:5265", + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -22,6 +23,7 @@ "dotnetRunMessages": true, "launchBrowser": true, "applicationUrl": "https://localhost:7247;http://localhost:5265", + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -29,6 +31,7 @@ "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/src/Components/Samples/BlazorUnitedApp/Shared/NavMenu.razor b/src/Components/Samples/BlazorUnitedApp/Shared/NavMenu.razor index bcebada388d5..3801dfc18932 100644 --- a/src/Components/Samples/BlazorUnitedApp/Shared/NavMenu.razor +++ b/src/Components/Samples/BlazorUnitedApp/Shared/NavMenu.razor @@ -24,6 +24,11 @@ Fetch data + diff --git a/src/Mvc/samples/MvcSandbox/Startup.cs b/src/Mvc/samples/MvcSandbox/Startup.cs index 2708a0c0b47c..3f5a178d3c92 100644 --- a/src/Mvc/samples/MvcSandbox/Startup.cs +++ b/src/Mvc/samples/MvcSandbox/Startup.cs @@ -16,20 +16,23 @@ public void ConfigureServices(IServiceCollection services) public void Configure(IApplicationBuilder app) { app.UseDeveloperExceptionPage(); - app.UseStaticFiles(); app.UseRouting(); static void ConfigureEndpoints(IEndpointRouteBuilder endpoints) { + endpoints.MapStaticAssets(); endpoints.MapGet("/MapGet", () => "MapGet"); - endpoints.MapControllers(); + endpoints.MapControllers() + .WithResourceCollection(); + endpoints.MapControllerRoute( Guid.NewGuid().ToString(), "{controller=Home}/{action=Index}/{id?}"); - endpoints.MapRazorPages(); + endpoints.MapRazorPages() + .WithResourceCollection(); } app.UseEndpoints(builder => diff --git a/src/Mvc/samples/MvcSandbox/Views/Shared/_Layout.cshtml b/src/Mvc/samples/MvcSandbox/Views/Shared/_Layout.cshtml index 60569523afd5..08d9c383b09d 100644 --- a/src/Mvc/samples/MvcSandbox/Views/Shared/_Layout.cshtml +++ b/src/Mvc/samples/MvcSandbox/Views/Shared/_Layout.cshtml @@ -5,6 +5,7 @@ @ViewData["Title"] - MvcSandbox +
From acc4e0c60e4253b26f97c8a71a1e7c545b7c4993 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Tue, 4 Jun 2024 20:40:27 +0200 Subject: [PATCH 03/30] Code changes --- AspNetCore.sln | 19 ++ src/Components/Components.slnf | 1 + .../Components/src/ComponentBase.cs | 5 + .../Components/src/ParameterView.cs | 2 + .../Components/src/PublicAPI.Unshipped.txt | 15 + src/Components/Components/src/RenderHandle.cs | 11 + .../Components/src/RenderTree/Renderer.cs | 5 + .../Components/src/ResourceAsset.cs | 22 ++ .../Components/src/ResourceAssetCollection.cs | 63 ++++ .../Components/src/ResourceAssetProperty.cs | 22 ++ .../Endpoints/src/Assets/ImportMap.cs | 63 ++++ .../src/Assets/ImportMapDefinition.cs | 208 +++++++++++++ .../src/Assets/ImportMapSerializerContext.cs | 22 ++ .../RazorComponentDataSourceOptions.cs | 2 + .../RazorComponentEndpointDataSource.cs | 39 ++- ...RazorComponentEndpointDataSourceFactory.cs | 21 +- .../Builder/RazorComponentEndpointFactory.cs | 4 +- ...azorComponentsEndpointConventionBuilder.cs | 29 +- ...entsEndpointConventionBuilderExtensions.cs | 31 ++ ...omponentsEndpointRouteBuilderExtensions.cs | 13 +- .../src/Builder/RenderModeEndpointProvider.cs | 1 - .../Builder/ResourceCollectionConvention.cs | 63 ++++ .../Builder/ResourceCollectionUrlEndpoint.cs | 291 ++++++++++++++++++ .../Builder/ResourceCollectionUrlMetadata.cs | 9 + ...orComponentsServiceCollectionExtensions.cs | 2 + ...oft.AspNetCore.Components.Endpoints.csproj | 3 + .../Endpoints/src/PublicAPI.Unshipped.txt | 15 + .../src/Rendering/EndpointHtmlRenderer.cs | 31 +- .../test/EndpointHtmlRendererTest.cs | 2 +- .../Endpoints/test/HotReloadServiceTests.cs | 1 + .../test/RazorComponentEndpointFactoryTest.cs | 4 +- .../Server/src/Circuits/CircuitFactory.cs | 6 +- .../Server/src/Circuits/ICircuitFactory.cs | 3 +- .../Server/src/Circuits/RemoteRenderer.cs | 7 +- src/Components/Server/src/ComponentHub.cs | 5 +- .../Server/test/Circuits/ComponentHubTest.cs | 3 +- .../Shared/src/ResourceCollectionProvider.cs | 62 ++++ ...entsEndpointConventionBuilderExtensions.cs | 17 +- .../Server/src/WebAssemblyEndpointProvider.cs | 83 +++++ ...ssemblyRazorComponentsBuilderExtensions.cs | 82 ----- .../src/Hosting/WebAssemblyHost.cs | 5 +- .../src/Hosting/WebAssemblyHostBuilder.cs | 1 + ...t.AspNetCore.Components.WebAssembly.csproj | 1 + .../src/Rendering/WebAssemblyRenderer.cs | 7 +- .../App.Ref/src/CompatibilitySuppressions.xml | 2 +- .../src/CompatibilitySuppressions.xml | 2 +- .../WebUtilities/src/PublicAPI.Unshipped.txt | 1 + ...ntrollerActionEndpointConventionBuilder.cs | 2 + ...ontrollerEndpointRouteBuilderExtensions.cs | 4 +- .../src/TagHelpers/UrlResolutionTagHelper.cs | 36 +++ .../PageActionEndpointConventionBuilder.cs | 2 + ...tionBuilderResourceCollectionExtensions.cs | 47 +++ ...azorPagesEndpointRouteBuilderExtensions.cs | 7 +- .../PageActionEndpointDataSourceFactory.cs | 6 +- .../src/PublicAPI.Unshipped.txt | 2 + src/Mvc/Mvc.TagHelpers/src/ImageTagHelper.cs | 42 ++- src/Mvc/Mvc.TagHelpers/src/LinkTagHelper.cs | 48 ++- .../src/PublicAPI.Unshipped.txt | 4 + src/Mvc/Mvc.TagHelpers/src/ScriptTagHelper.cs | 73 ++++- ...tionBuilderResourceCollectionExtensions.cs | 55 ++++ ...crosoft.AspNetCore.Mvc.ViewFeatures.csproj | 2 + .../src/PublicAPI.Unshipped.txt | 2 + .../Components/ResourceCollectionResolver.cs | 135 ++++++++ src/Shared/WebEncoders/WebEncoders.cs | 10 + 64 files changed, 1618 insertions(+), 165 deletions(-) create mode 100644 src/Components/Components/src/ResourceAsset.cs create mode 100644 src/Components/Components/src/ResourceAssetCollection.cs create mode 100644 src/Components/Components/src/ResourceAssetProperty.cs create mode 100644 src/Components/Endpoints/src/Assets/ImportMap.cs create mode 100644 src/Components/Endpoints/src/Assets/ImportMapDefinition.cs create mode 100644 src/Components/Endpoints/src/Assets/ImportMapSerializerContext.cs create mode 100644 src/Components/Endpoints/src/Builder/ResourceCollectionConvention.cs create mode 100644 src/Components/Endpoints/src/Builder/ResourceCollectionUrlEndpoint.cs create mode 100644 src/Components/Endpoints/src/Builder/ResourceCollectionUrlMetadata.cs create mode 100644 src/Components/Shared/src/ResourceCollectionProvider.cs create mode 100644 src/Components/WebAssembly/Server/src/WebAssemblyEndpointProvider.cs create mode 100644 src/Mvc/Mvc.RazorPages/src/Builder/PageActionEndpointConventionBuilderResourceCollectionExtensions.cs create mode 100644 src/Mvc/Mvc.ViewFeatures/src/Builder/ControllerActionEndpointConventionBuilderResourceCollectionExtensions.cs create mode 100644 src/Shared/Components/ResourceCollectionResolver.cs diff --git a/AspNetCore.sln b/AspNetCore.sln index bf6010cef399..d9d83a1961f7 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1816,6 +1816,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GetDocumentInsider.Tests", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GetDocumentSample", "src\Tools\GetDocumentInsider\sample\GetDocumentSample.csproj", "{D8F7091E-A2D1-4E81-BA7C-97EAE392D683}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazorUnitedApp.Client", "src\Components\Samples\BlazorUnitedApp.Client\BlazorUnitedApp.Client.csproj", "{757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -10977,6 +10979,22 @@ Global {D8F7091E-A2D1-4E81-BA7C-97EAE392D683}.Release|x64.Build.0 = Release|Any CPU {D8F7091E-A2D1-4E81-BA7C-97EAE392D683}.Release|x86.ActiveCfg = Release|Any CPU {D8F7091E-A2D1-4E81-BA7C-97EAE392D683}.Release|x86.Build.0 = Release|Any CPU + {757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}.Debug|Any CPU.Build.0 = Debug|Any CPU + {757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}.Debug|arm64.ActiveCfg = Debug|Any CPU + {757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}.Debug|arm64.Build.0 = Debug|Any CPU + {757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}.Debug|x64.ActiveCfg = Debug|Any CPU + {757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}.Debug|x64.Build.0 = Debug|Any CPU + {757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}.Debug|x86.ActiveCfg = Debug|Any CPU + {757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}.Debug|x86.Build.0 = Debug|Any CPU + {757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}.Release|Any CPU.ActiveCfg = Release|Any CPU + {757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}.Release|Any CPU.Build.0 = Release|Any CPU + {757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}.Release|arm64.ActiveCfg = Release|Any CPU + {757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}.Release|arm64.Build.0 = Release|Any CPU + {757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}.Release|x64.ActiveCfg = Release|Any CPU + {757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}.Release|x64.Build.0 = Release|Any CPU + {757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}.Release|x86.ActiveCfg = Release|Any CPU + {757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -11874,6 +11892,7 @@ Global {9536C284-65B4-4884-BB50-06D629095C3E} = {274100A5-5B2D-4EA2-AC42-A62257FC6BDC} {6A19D94D-2BC6-4198-BE2E-342688FDBA4B} = {A1B75FC7-A777-4412-A635-D0C9ED8FE7A0} {D8F7091E-A2D1-4E81-BA7C-97EAE392D683} = {A1B75FC7-A777-4412-A635-D0C9ED8FE7A0} + {757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63} = {5FE1FBC1-8CE3-4355-9866-44FE1307C5F1} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F} diff --git a/src/Components/Components.slnf b/src/Components/Components.slnf index 6da9c7c7e04d..b3975358cac3 100644 --- a/src/Components/Components.slnf +++ b/src/Components/Components.slnf @@ -19,6 +19,7 @@ "src\\Components\\QuickGrid\\Microsoft.AspNetCore.Components.QuickGrid.EntityFrameworkAdapter\\src\\Microsoft.AspNetCore.Components.QuickGrid.EntityFrameworkAdapter.csproj", "src\\Components\\QuickGrid\\Microsoft.AspNetCore.Components.QuickGrid\\src\\Microsoft.AspNetCore.Components.QuickGrid.csproj", "src\\Components\\Samples\\BlazorServerApp\\BlazorServerApp.csproj", + "src\\Components\\Samples\\BlazorUnitedApp.Client\\BlazorUnitedApp.Client.csproj", "src\\Components\\Samples\\BlazorUnitedApp\\BlazorUnitedApp.csproj", "src\\Components\\Server\\src\\Microsoft.AspNetCore.Components.Server.csproj", "src\\Components\\Server\\test\\Microsoft.AspNetCore.Components.Server.Tests.csproj", diff --git a/src/Components/Components/src/ComponentBase.cs b/src/Components/Components/src/ComponentBase.cs index 86b5c32fcea8..d9c0f346b0ca 100644 --- a/src/Components/Components/src/ComponentBase.cs +++ b/src/Components/Components/src/ComponentBase.cs @@ -47,6 +47,11 @@ public ComponentBase() /// protected ComponentPlatform Platform => _renderHandle.Platform; + /// + /// Gets the for the application. + /// + protected ResourceAssetCollection Assets => _renderHandle.Assets; + /// /// Gets the assigned to this component. /// diff --git a/src/Components/Components/src/ParameterView.cs b/src/Components/Components/src/ParameterView.cs index 01252270fbe5..517cb058024b 100644 --- a/src/Components/Components/src/ParameterView.cs +++ b/src/Components/Components/src/ParameterView.cs @@ -46,6 +46,8 @@ private ParameterView(in ParameterViewLifetime lifetime, RenderTreeFrame[] frame internal ParameterViewLifetime Lifetime => _lifetime; + internal int Count => _frames?.Length ?? 0; + /// /// Returns an enumerator that iterates through the . /// diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index aa9e6551abe9..c669b0dc567c 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -1,4 +1,5 @@ #nullable enable +Microsoft.AspNetCore.Components.ComponentBase.Assets.get -> Microsoft.AspNetCore.Components.ResourceAssetCollection! Microsoft.AspNetCore.Components.ComponentBase.AssignedRenderMode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode? Microsoft.AspNetCore.Components.ComponentBase.Platform.get -> Microsoft.AspNetCore.Components.ComponentPlatform! Microsoft.AspNetCore.Components.ComponentPlatform @@ -7,6 +8,20 @@ Microsoft.AspNetCore.Components.ComponentPlatform.IsInteractive.get -> bool Microsoft.AspNetCore.Components.ComponentPlatform.Name.get -> string! Microsoft.AspNetCore.Components.ExcludeFromInteractiveRoutingAttribute Microsoft.AspNetCore.Components.ExcludeFromInteractiveRoutingAttribute.ExcludeFromInteractiveRoutingAttribute() -> void +Microsoft.AspNetCore.Components.RenderHandle.Assets.get -> Microsoft.AspNetCore.Components.ResourceAssetCollection! Microsoft.AspNetCore.Components.RenderHandle.Platform.get -> Microsoft.AspNetCore.Components.ComponentPlatform! Microsoft.AspNetCore.Components.RenderHandle.RenderMode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode? +Microsoft.AspNetCore.Components.ResourceAsset +Microsoft.AspNetCore.Components.ResourceAsset.Properties.get -> System.Collections.Generic.IReadOnlyList? +Microsoft.AspNetCore.Components.ResourceAsset.ResourceAsset(string! url, System.Collections.Generic.IReadOnlyList? properties) -> void +Microsoft.AspNetCore.Components.ResourceAsset.Url.get -> string! +Microsoft.AspNetCore.Components.ResourceAssetCollection +Microsoft.AspNetCore.Components.ResourceAssetCollection.ResourceAssetCollection(System.Collections.Generic.IReadOnlyList! resources) -> void +Microsoft.AspNetCore.Components.ResourceAssetCollection.this[string! key].get -> string! +Microsoft.AspNetCore.Components.ResourceAssetProperty +Microsoft.AspNetCore.Components.ResourceAssetProperty.Name.get -> string! +Microsoft.AspNetCore.Components.ResourceAssetProperty.ResourceAssetProperty(string! name, string! value) -> void +Microsoft.AspNetCore.Components.ResourceAssetProperty.Value.get -> string! +static readonly Microsoft.AspNetCore.Components.ResourceAssetCollection.Empty -> Microsoft.AspNetCore.Components.ResourceAssetCollection! +virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.Assets.get -> Microsoft.AspNetCore.Components.ResourceAssetCollection! virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.ComponentPlatform.get -> Microsoft.AspNetCore.Components.ComponentPlatform! diff --git a/src/Components/Components/src/RenderHandle.cs b/src/Components/Components/src/RenderHandle.cs index e26639e83b02..4af99006811a 100644 --- a/src/Components/Components/src/RenderHandle.cs +++ b/src/Components/Components/src/RenderHandle.cs @@ -73,6 +73,17 @@ public IComponentRenderMode? RenderMode } } + /// + /// Gets the associated with the . + /// + public ResourceAssetCollection Assets + { + get + { + return _renderer?.Assets ?? throw new InvalidOperationException("No renderer has been initialized."); + } + } + /// /// Notifies the renderer that the component should be rendered. /// diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index 1e4ef05d30b4..20be6ba7f07f 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -158,6 +158,11 @@ protected internal ComponentState GetComponentState(IComponent component) /// protected internal virtual ComponentPlatform ComponentPlatform { get; } + /// + /// Gets the associated with this . + /// + protected internal virtual ResourceAssetCollection Assets { get; } = ResourceAssetCollection.Empty; + private async void RenderRootComponentsOnHotReload() { // Before re-rendering the root component, also clear any well-known caches in the framework diff --git a/src/Components/Components/src/ResourceAsset.cs b/src/Components/Components/src/ResourceAsset.cs new file mode 100644 index 000000000000..b42a7dd80ef7 --- /dev/null +++ b/src/Components/Components/src/ResourceAsset.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components; + +/// +/// A resource of the component application, such as a script, stylesheet or image. +/// +/// The URL of the resource. +/// The properties associated to this resource. +public class ResourceAsset(string url, IReadOnlyList? properties) +{ + /// + /// Gets the URL that identifies this resource. + /// + public string Url { get; } = url; + + /// + /// Gets a list of properties associated to this resource. + /// + public IReadOnlyList? Properties { get; } = properties; +} diff --git a/src/Components/Components/src/ResourceAssetCollection.cs b/src/Components/Components/src/ResourceAssetCollection.cs new file mode 100644 index 000000000000..dea6ffd56a86 --- /dev/null +++ b/src/Components/Components/src/ResourceAssetCollection.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; + +namespace Microsoft.AspNetCore.Components; + +/// +/// Describes a mapping of static assets to their corresponding unique URLs. +/// +public class ResourceAssetCollection : IReadOnlyList +{ + /// + /// An empty . + /// + public static readonly ResourceAssetCollection Empty = new([]); + + private readonly Dictionary _uniqueUrlMappings; + private readonly IReadOnlyList _resources; + + /// + /// Initializes a new instance of + /// + /// The list of resources available. + public ResourceAssetCollection(IReadOnlyList resources) + { + _uniqueUrlMappings = new Dictionary(StringComparer.OrdinalIgnoreCase); + _resources = resources; + foreach (var resource in resources) + { + foreach (var property in resource.Properties ?? []) + { + if (property.Name.Equals("label", StringComparison.OrdinalIgnoreCase)) + { + _uniqueUrlMappings[property.Value] = resource; + } + } + } + } + + /// + /// Gets the unique content-based URL for the specified static asset. + /// + /// The asset name. + /// The unique URL if availabe, the same if not available. + public string this[string key] + { + get + { + if (_uniqueUrlMappings.TryGetValue(key, out var value)) + { + return value.Url; + } + + return key; + } + } + + ResourceAsset IReadOnlyList.this[int index] => _resources[index]; + int IReadOnlyCollection.Count => _resources.Count; + IEnumerator IEnumerable.GetEnumerator() => _resources.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => _resources.GetEnumerator(); +} diff --git a/src/Components/Components/src/ResourceAssetProperty.cs b/src/Components/Components/src/ResourceAssetProperty.cs new file mode 100644 index 000000000000..1ec70700fcfd --- /dev/null +++ b/src/Components/Components/src/ResourceAssetProperty.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components; + +/// +/// A resource proeperty. +/// +/// The name of the property. +/// The value of the property. +public class ResourceAssetProperty(string name, string value) +{ + /// + /// Gets the name of the property. + /// + public string Name { get; } = name; + + /// + /// Gets the value of the property. + /// + public string Value { get; } = value; +} diff --git a/src/Components/Endpoints/src/Assets/ImportMap.cs b/src/Components/Endpoints/src/Assets/ImportMap.cs new file mode 100644 index 000000000000..8cc73bfb4880 --- /dev/null +++ b/src/Components/Endpoints/src/Assets/ImportMap.cs @@ -0,0 +1,63 @@ +// 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.Components.Rendering; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Components; + +/// +/// Represents an element that defines the import map for module scripts +/// in the application. +/// +public class ImportMap : IComponent +{ + private RenderHandle _renderHandle; + private bool _firstRender = true; + private ImportMapDefinition? _computedImportMapDefinition; + + /// + /// Gets or sets the for the component. + /// + [CascadingParameter] public HttpContext? HttpContext { get; set; } = null; + + /// + /// Gets or sets the import map definition to use for the component. If not set + /// the component will generte the import map based on the assets defined for this + /// application. + /// + [Parameter] + public ImportMapDefinition? ImportMapDefinition { get; set; } + + void IComponent.Attach(RenderHandle renderHandle) + { + _renderHandle = renderHandle; + } + + Task IComponent.SetParametersAsync(ParameterView parameters) + { + parameters.SetParameterProperties(this); + if (!_firstRender && ReferenceEquals(ImportMapDefinition, _computedImportMapDefinition)) + { + return Task.CompletedTask; + } + else + { + _firstRender = false; + _computedImportMapDefinition = ImportMapDefinition ?? HttpContext?.GetEndpoint()?.Metadata.GetMetadata(); + if (_computedImportMapDefinition != null) + { + _renderHandle.Render(RenderImportMap); + } + return Task.CompletedTask; + } + } + + private void RenderImportMap(RenderTreeBuilder builder) + { + builder.OpenElement(0, "script"); + builder.AddAttribute(1, "type", "importmap"); + builder.AddMarkupContent(2, _computedImportMapDefinition!.ToJson()); + builder.CloseElement(); + } +} diff --git a/src/Components/Endpoints/src/Assets/ImportMapDefinition.cs b/src/Components/Endpoints/src/Assets/ImportMapDefinition.cs new file mode 100644 index 000000000000..276264001469 --- /dev/null +++ b/src/Components/Endpoints/src/Assets/ImportMapDefinition.cs @@ -0,0 +1,208 @@ +// 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 System.Text.Json; + +namespace Microsoft.AspNetCore.Components; + +/// +/// Represents the contents of a element that defines the import map +/// for module scripts in the application. +/// +/// +/// The import map is a JSON object that defines the mapping of module import specifiers to URLs. +/// instances are expensive to create, so it is recommended to cache them if +/// you are creating an additional instance. +/// +public class ImportMapDefinition +{ + private Dictionary? _imports; + private Dictionary>? _scopes; + private Dictionary? _integrity; + private string? _json; + + /// + /// Initializes a new instance of ."/> with the specified imports, scopes, and integrity. + /// + /// The unscoped imports defined in the import map. + /// The scoped imports defined in the import map. + /// The integrity for the imports defined in the import map. + /// + /// The , , and parameters + /// will be copied into the new instance. The original collections will not be modified. + /// + public ImportMapDefinition( + IReadOnlyDictionary? imports, + IReadOnlyDictionary>? scopes, + IReadOnlyDictionary? integrity) + { + _imports = imports?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + _integrity = integrity?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + _scopes = scopes?.ToDictionary( + kvp => kvp.Key, + kvp => kvp.Value.ToDictionary(scopeKvp => scopeKvp.Key, scopeKvp => scopeKvp.Value) as IReadOnlyDictionary); + } + + private ImportMapDefinition() + { + } + + /// + /// Creates an import map from a . + /// + /// The collection of assets to create the import map from. + /// The import map. + public static ImportMapDefinition FromResourceCollection(ResourceAssetCollection assets) + { + var importMap = new ImportMapDefinition(); + foreach (var asset in assets) + { + if (!(asset.Url.EndsWith(".mjs", StringComparison.OrdinalIgnoreCase) || + asset.Url.EndsWith(".js", StringComparison.OrdinalIgnoreCase)) || + asset.Properties == null) + { + continue; + } + + var (integrity, label) = GetAssetProperties(asset); + if (integrity != null) + { + importMap._integrity ??= []; + importMap._integrity[asset.Url] = integrity; + } + + if (label != null) + { + importMap._imports ??= []; + importMap._imports[label] = asset.Url; + } + } + + return importMap; + } + + private static (string? integrity, string? label) GetAssetProperties(ResourceAsset asset) + { + string? integrity = null; + string? label = null; + for (var i = 0; i < asset.Properties!.Count; i++) + { + var property = asset.Properties[i]; + if (string.Equals(property.Name, "integrity", StringComparison.OrdinalIgnoreCase)) + { + integrity = property.Value; + } + else if (string.Equals(property.Name, "label", StringComparison.OrdinalIgnoreCase)) + { + label = property.Name; + } + + if (integrity != null && label != null) + { + return (integrity, label); + } + } + + return (integrity, label); + } + + /// + /// Combines one or more import maps into a single import map. + /// + /// The list of import maps to combine. + /// + /// A new import map that is the combination of all the input import maps with their + /// entries applied in order. + /// + public static ImportMapDefinition Combine(params ImportMapDefinition[] sources) + { + var importMap = new ImportMapDefinition(); + foreach (var item in sources) + { + if (item.Imports != null) + { + importMap._imports ??= []; + foreach (var (key, value) in item.Imports) + { + importMap._imports[key] = value; + } + } + + if (item.Scopes != null) + { + importMap._scopes ??= []; + foreach (var (key, value) in item.Scopes) + { + foreach (var (scopeKey, scopeValue) in value) + { + importMap._scopes[key] ??= new Dictionary(); + ((Dictionary)importMap._scopes[key])[scopeKey] = scopeValue; + } + } + } + + if (item.Integrity != null) + { + importMap._integrity ??= []; + foreach (var (key, value) in item.Integrity) + { + importMap._integrity[key] = value; + } + } + } + + return importMap; + } + + // Example: + // "imports": { + // "triangle": "./module/shapes/triangle.js", + // "pentagram": "https://example.com/shapes/pentagram.js" + // } + /// + /// Gets the unscoped imports defined in the import map. + /// + public IReadOnlyDictionary? Imports { get => _imports; } + + // Example: + // { + // "imports": { + // "triangle": "./module/shapes/triangle.js" + // }, + // "scopes": { + // "/modules/myshapes/": { + // "triangle": "https://example.com/modules/myshapes/triangle.js" + // } + // } + // } + /// + /// Gets the scoped imports defined in the import map. + /// + public IReadOnlyDictionary>? Scopes { get => _scopes; } + + // Example: + // + /// + /// Gets the integrity properties defined in the import map. + /// + public IReadOnlyDictionary? Integrity { get => _integrity; } + + internal string ToJson() + { + _json ??= JsonSerializer.Serialize(this, ImportMapSerializerContext.CustomEncoder.Options); + return _json; + } + + /// + public override string ToString() => ToJson(); +} diff --git a/src/Components/Endpoints/src/Assets/ImportMapSerializerContext.cs b/src/Components/Endpoints/src/Assets/ImportMapSerializerContext.cs new file mode 100644 index 000000000000..19db8d5c7bf2 --- /dev/null +++ b/src/Components/Endpoints/src/Assets/ImportMapSerializerContext.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.AspNetCore.Components; + +[JsonSerializable(typeof(ImportMapDefinition))] +internal partial class ImportMapSerializerContext : JsonSerializerContext +{ + private static ImportMapSerializerContext? _customEncoder; + + public static ImportMapSerializerContext CustomEncoder => _customEncoder ??= new(new JsonSerializerOptions + { + WriteIndented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }); +} diff --git a/src/Components/Endpoints/src/Builder/RazorComponentDataSourceOptions.cs b/src/Components/Endpoints/src/Builder/RazorComponentDataSourceOptions.cs index 89d9b9e4f43e..e7e319c544b5 100644 --- a/src/Components/Endpoints/src/Builder/RazorComponentDataSourceOptions.cs +++ b/src/Components/Endpoints/src/Builder/RazorComponentDataSourceOptions.cs @@ -28,5 +28,7 @@ internal class RazorComponentDataSourceOptions _ => throw new InvalidOperationException($"Unknown render mode: {obj}"), }); + public string? ManifestPath { get; set; } + internal ISet ConfiguredRenderModes { get; } = new HashSet(RenderModeComparer); } diff --git a/src/Components/Endpoints/src/Builder/RazorComponentEndpointDataSource.cs b/src/Components/Endpoints/src/Builder/RazorComponentEndpointDataSource.cs index d7ce3ca90bc9..807c69ad4f68 100644 --- a/src/Components/Endpoints/src/Builder/RazorComponentEndpointDataSource.cs +++ b/src/Components/Endpoints/src/Builder/RazorComponentEndpointDataSource.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -17,11 +18,12 @@ namespace Microsoft.AspNetCore.Components.Endpoints; internal class RazorComponentEndpointDataSource<[DynamicallyAccessedMembers(Component)] TRootComponent> : EndpointDataSource { private readonly object _lock = new(); - private readonly List> _conventions = new(); - private readonly List> _finallyConventions = new(); + private readonly List> _conventions = []; + private readonly List> _finallyConventions = []; private readonly RazorComponentDataSourceOptions _options = new(); private readonly ComponentApplicationBuilder _builder; - private readonly IApplicationBuilder _applicationBuilder; + private readonly IEndpointRouteBuilder _endpointRouteBuilder; + private readonly ResourceCollectionResolver _resourceCollectionResolver; private readonly RenderModeEndpointProvider[] _renderModeEndpointProviders; private readonly RazorComponentEndpointFactory _factory; private readonly HotReloadService? _hotReloadService; @@ -44,7 +46,8 @@ public RazorComponentEndpointDataSource( HotReloadService? hotReloadService = null) { _builder = builder; - _applicationBuilder = endpointRouteBuilder.CreateApplicationBuilder(); + _endpointRouteBuilder = endpointRouteBuilder; + _resourceCollectionResolver = new ResourceCollectionResolver(endpointRouteBuilder); _renderModeEndpointProviders = renderModeEndpointProviders.ToArray(); _factory = factory; _hotReloadService = hotReloadService; @@ -99,32 +102,48 @@ private void Initialize() private void UpdateEndpoints() { + const string ResourceCollectionKey = "__ResourceCollectionKey"; + lock (_lock) { var endpoints = new List(); var context = _builder.Build(); var configuredRenderModesMetadata = new ConfiguredRenderModesMetadata( - Options.ConfiguredRenderModes.ToArray()); + [.. Options.ConfiguredRenderModes]); + + var endpointContext = new RazorComponentEndpointUpdateContext(endpoints, _options); + + DefaultBuilder.OnBeforeCreateEndpoints(endpointContext); foreach (var definition in context.Pages) { - _factory.AddEndpoints(endpoints, typeof(TRootComponent), definition, _conventions, _finallyConventions, configuredRenderModesMetadata); + _factory.AddEndpoints( + endpoints, + typeof(TRootComponent), + definition, + _conventions, + _finallyConventions, + configuredRenderModesMetadata); } - ICollection renderModes = Options.ConfiguredRenderModes; + // Extract the endpoint collection from any of the endpoints + var resourceCollection = endpoints[^1].Metadata.GetMetadata(); + ICollection renderModes = Options.ConfiguredRenderModes; foreach (var renderMode in renderModes) { var found = false; foreach (var provider in _renderModeEndpointProviders) { + var builder = _endpointRouteBuilder.CreateApplicationBuilder(); + builder.Properties[ResourceCollectionKey] = resourceCollection; if (provider.Supports(renderMode)) { found = true; RenderModeEndpointProvider.AddEndpoints( endpoints, typeof(TRootComponent), - provider.GetEndpointBuilders(renderMode, _applicationBuilder.New()), + provider.GetEndpointBuilders(renderMode, builder), renderMode, _conventions, _finallyConventions); @@ -145,14 +164,14 @@ private void UpdateEndpoints() _changeToken = new CancellationChangeToken(_cancellationTokenSource.Token); oldCancellationTokenSource?.Cancel(); oldCancellationTokenSource?.Dispose(); - if (_hotReloadService is { MetadataUpdateSupported : true }) + if (_hotReloadService is { MetadataUpdateSupported: true }) { _disposableChangeToken?.Dispose(); _disposableChangeToken = SetDisposableChangeTokenAction(ChangeToken.OnChange(_hotReloadService.GetChangeToken, UpdateEndpoints)); } } } - + public void OnHotReloadClearCache(Type[]? types) { lock (_lock) diff --git a/src/Components/Endpoints/src/Builder/RazorComponentEndpointDataSourceFactory.cs b/src/Components/Endpoints/src/Builder/RazorComponentEndpointDataSourceFactory.cs index 462fc50a2268..84327f3f6d54 100644 --- a/src/Components/Endpoints/src/Builder/RazorComponentEndpointDataSourceFactory.cs +++ b/src/Components/Endpoints/src/Builder/RazorComponentEndpointDataSourceFactory.cs @@ -10,27 +10,16 @@ namespace Microsoft.AspNetCore.Components.Infrastructure; -internal class RazorComponentEndpointDataSourceFactory +internal class RazorComponentEndpointDataSourceFactory( + RazorComponentEndpointFactory factory, + IEnumerable providers, + HotReloadService? hotReloadService = null) { - private readonly RazorComponentEndpointFactory _factory; - private readonly IEnumerable _providers; - private readonly HotReloadService? _hotReloadService; - - public RazorComponentEndpointDataSourceFactory( - RazorComponentEndpointFactory factory, - IEnumerable providers, - HotReloadService? hotReloadService = null) - { - _factory = factory; - _providers = providers; - _hotReloadService = hotReloadService; - } - public RazorComponentEndpointDataSource CreateDataSource<[DynamicallyAccessedMembers(Component)] TRootComponent>(IEndpointRouteBuilder endpoints) { var builder = ComponentApplicationBuilder.GetBuilder() ?? DefaultRazorComponentApplication.Instance.GetBuilder(); - return new RazorComponentEndpointDataSource(builder, _providers, endpoints, _factory, _hotReloadService); + return new RazorComponentEndpointDataSource(builder, providers, endpoints, factory, hotReloadService); } } diff --git a/src/Components/Endpoints/src/Builder/RazorComponentEndpointFactory.cs b/src/Components/Endpoints/src/Builder/RazorComponentEndpointFactory.cs index 8f79f6c191b8..692f4c208cb7 100644 --- a/src/Components/Endpoints/src/Builder/RazorComponentEndpointFactory.cs +++ b/src/Components/Endpoints/src/Builder/RazorComponentEndpointFactory.cs @@ -13,9 +13,9 @@ namespace Microsoft.AspNetCore.Components.Endpoints; -internal class RazorComponentEndpointFactory +internal class RazorComponentEndpointFactory() { - private static readonly HttpMethodMetadata HttpMethodsMetadata = new(new[] { HttpMethods.Get, HttpMethods.Post }); + private static readonly HttpMethodMetadata HttpMethodsMetadata = new([HttpMethods.Get, HttpMethods.Post]); #pragma warning disable CA1822 // It's a singleton internal void AddEndpoints( diff --git a/src/Components/Endpoints/src/Builder/RazorComponentsEndpointConventionBuilder.cs b/src/Components/Endpoints/src/Builder/RazorComponentsEndpointConventionBuilder.cs index c37b9d348a9b..04b1daa9e952 100644 --- a/src/Components/Endpoints/src/Builder/RazorComponentsEndpointConventionBuilder.cs +++ b/src/Components/Endpoints/src/Builder/RazorComponentsEndpointConventionBuilder.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Discovery; using Microsoft.AspNetCore.Components.Endpoints; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; namespace Microsoft.AspNetCore.Builder; @@ -14,8 +15,6 @@ namespace Microsoft.AspNetCore.Builder; public sealed class RazorComponentsEndpointConventionBuilder : IEndpointConventionBuilder { private readonly object _lock; - private readonly ComponentApplicationBuilder _builder; - private readonly IEndpointRouteBuilder _endpointRouteBuilder; private readonly RazorComponentDataSourceOptions _options; private readonly List> _conventions; private readonly List> _finallyConventions; @@ -29,8 +28,8 @@ internal RazorComponentsEndpointConventionBuilder( List> finallyConventions) { _lock = @lock; - _builder = builder; - _endpointRouteBuilder = endpointRouteBuilder; + ApplicationBuilder = builder; + EndpointRouteBuilder = endpointRouteBuilder; _options = options; _conventions = conventions; _finallyConventions = finallyConventions; @@ -39,9 +38,15 @@ internal RazorComponentsEndpointConventionBuilder( /// /// Gets the that is used to build the endpoints. /// - internal ComponentApplicationBuilder ApplicationBuilder => _builder; + internal ComponentApplicationBuilder ApplicationBuilder { get; } - internal IEndpointRouteBuilder EndpointRouteBuilder => _endpointRouteBuilder; + internal string? ManifestPath { get => _options.ManifestPath; set => _options.ManifestPath = value; } + + internal bool ResourceCollectionConventionRegistered { get; set; } + + internal IEndpointRouteBuilder EndpointRouteBuilder { get; } + + internal event Action? BeforeCreateEndpoints; /// public void Add(Action convention) @@ -71,5 +76,17 @@ internal void AddRenderMode(IComponentRenderMode renderMode) { _options.ConfiguredRenderModes.Add(renderMode); } + + internal void OnBeforeCreateEndpoints(RazorComponentEndpointUpdateContext endpointContext) => + BeforeCreateEndpoints?.Invoke(endpointContext); +} + +internal class RazorComponentEndpointUpdateContext( + List endpoints, + RazorComponentDataSourceOptions options) +{ + public List Endpoints { get; } = endpoints; + + public RazorComponentDataSourceOptions Options { get; } = options; } diff --git a/src/Components/Endpoints/src/Builder/RazorComponentsEndpointConventionBuilderExtensions.cs b/src/Components/Endpoints/src/Builder/RazorComponentsEndpointConventionBuilderExtensions.cs index 821a16309b24..843fb30c61d5 100644 --- a/src/Components/Endpoints/src/Builder/RazorComponentsEndpointConventionBuilderExtensions.cs +++ b/src/Components/Endpoints/src/Builder/RazorComponentsEndpointConventionBuilderExtensions.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Reflection; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Endpoints; namespace Microsoft.AspNetCore.Builder; @@ -32,4 +34,33 @@ public static RazorComponentsEndpointConventionBuilder AddAdditionalAssemblies( } return builder; } + + /// + /// Sets a and metadata + /// for the component application. + /// + /// The . + /// The manifest associated with the assets. + /// The . + /// + /// The must match the path of the manifes file provided to + /// the + /// call. + /// + public static RazorComponentsEndpointConventionBuilder WithResourceCollection( + this RazorComponentsEndpointConventionBuilder builder, + string? manifestPath = null) + { + ArgumentNullException.ThrowIfNull(builder); + builder.ManifestPath = manifestPath; + if (!builder.ResourceCollectionConventionRegistered) + { + builder.ResourceCollectionConventionRegistered = true; + var convention = new ResourceCollectionConvention(new ResourceCollectionResolver(builder.EndpointRouteBuilder)); + builder.BeforeCreateEndpoints += convention.OnBeforeCreateEndpoints; + builder.Add(convention.ApplyConvention); + } + + return builder; + } } diff --git a/src/Components/Endpoints/src/Builder/RazorComponentsEndpointRouteBuilderExtensions.cs b/src/Components/Endpoints/src/Builder/RazorComponentsEndpointRouteBuilderExtensions.cs index d8cfbb5b0d2e..bcd072b5ee90 100644 --- a/src/Components/Endpoints/src/Builder/RazorComponentsEndpointRouteBuilderExtensions.cs +++ b/src/Components/Endpoints/src/Builder/RazorComponentsEndpointRouteBuilderExtensions.cs @@ -33,7 +33,15 @@ public static class RazorComponentsEndpointRouteBuilderExtensions AddBlazorWebJsEndpoint(endpoints); OpaqueRedirection.AddBlazorOpaqueRedirectionEndpoint(endpoints); - return GetOrCreateDataSource(endpoints).DefaultBuilder; + var result = GetOrCreateDataSource(endpoints).DefaultBuilder; + + // Setup the convention to find the list of descriptors in the endpoint builder and + // populate a resource collection out of them. + // The user can call WithResourceCollection with a manifest path to override the manifest + // to use for the resource collection in case more than one has been mapped. + result.WithResourceCollection(); + + return result; } private static void AddBlazorWebJsEndpoint(IEndpointRouteBuilder endpoints) @@ -67,7 +75,8 @@ private static void AddBlazorWebJsEndpoint(IEndpointRouteBuilder endpoints) #endif } - private static RazorComponentEndpointDataSource GetOrCreateDataSource<[DynamicallyAccessedMembers(Component)] TRootComponent>(IEndpointRouteBuilder endpoints) + private static RazorComponentEndpointDataSource GetOrCreateDataSource<[DynamicallyAccessedMembers(Component)] TRootComponent>( + IEndpointRouteBuilder endpoints) { var dataSource = endpoints.DataSources.OfType>().FirstOrDefault(); if (dataSource == null) diff --git a/src/Components/Endpoints/src/Builder/RenderModeEndpointProvider.cs b/src/Components/Endpoints/src/Builder/RenderModeEndpointProvider.cs index 40213e400f31..5f18e47505fe 100644 --- a/src/Components/Endpoints/src/Builder/RenderModeEndpointProvider.cs +++ b/src/Components/Endpoints/src/Builder/RenderModeEndpointProvider.cs @@ -43,7 +43,6 @@ internal static void AddEndpoints( { builder.Metadata.Add(new RootComponentMetadata(rootComponent)); builder.Metadata.Add(renderMode); - foreach (var convention in conventions) { convention(builder); diff --git a/src/Components/Endpoints/src/Builder/ResourceCollectionConvention.cs b/src/Components/Endpoints/src/Builder/ResourceCollectionConvention.cs new file mode 100644 index 000000000000..d4b13cc48f4e --- /dev/null +++ b/src/Components/Endpoints/src/Builder/ResourceCollectionConvention.cs @@ -0,0 +1,63 @@ +// 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.Components; +using Microsoft.AspNetCore.Components.Endpoints; +using Microsoft.AspNetCore.Components.Web; + +namespace Microsoft.AspNetCore.Builder; + +internal class ResourceCollectionConvention(ResourceCollectionResolver resolver) +{ + string? _collectionUrl; + ImportMapDefinition? _collectionEndpointImportMap; + ResourceAssetCollection? _collection; + ImportMapDefinition? _collectionImportMap; + + public void OnBeforeCreateEndpoints(RazorComponentEndpointUpdateContext context) + { + if (resolver.IsRegistered(context.Options.ManifestPath)) + { + _collection = resolver.ResolveResourceCollection(context.Options.ManifestPath); + _collectionImportMap = ImportMapDefinition.FromResourceCollection(_collection); + + string? url = null; + ImportMapDefinition? map = null; + foreach (var renderMode in context.Options.ConfiguredRenderModes) + { + if (renderMode is InteractiveWebAssemblyRenderMode or InteractiveAutoRenderMode) + { + (map, url) = ResourceCollectionUrlEndpoint.MapResourceCollectionEndpoints( + context.Endpoints, + "_framework/resource-collection{0}.js{1}", + _collection); + break; + } + } + + if (url != null && map != null) + { + _collectionUrl = url; + _collectionEndpointImportMap = map; + } + } + } + + public void ApplyConvention(EndpointBuilder eb) + { + // The user called MapStaticAssets + if (_collection != null && _collectionImportMap != null) + { + eb.Metadata.Add(_collection); + + if (_collectionUrl != null) + { + eb.Metadata.Add(new ResourceCollectionUrlMetadata(_collectionUrl)); + } + + var importMap = _collectionEndpointImportMap == null ? _collectionImportMap : + ImportMapDefinition.Combine(_collectionImportMap, _collectionEndpointImportMap); + eb.Metadata.Add(importMap); + } + } +} diff --git a/src/Components/Endpoints/src/Builder/ResourceCollectionUrlEndpoint.cs b/src/Components/Endpoints/src/Builder/ResourceCollectionUrlEndpoint.cs new file mode 100644 index 000000000000..edf77a8951b0 --- /dev/null +++ b/src/Components/Endpoints/src/Builder/ResourceCollectionUrlEndpoint.cs @@ -0,0 +1,291 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Globalization; +using System.IO.Compression; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Components.Endpoints; + +internal partial class ResourceCollectionUrlEndpoint +{ + internal static (ImportMapDefinition, string) MapResourceCollectionEndpoints( + List endpoints, + string resourceCollectionUrlFormat, + ResourceAssetCollection resourceCollection) + { + // We map an additional endpoint to serve the resources so webassembly can fetch the list of + // resources and use fingerprinted resources when running interactively. + // We expose the same endpoint in four different urls _framework/resource-collection(.)?.js(.gz)? and + // with appropriate caching headers in both cases. + // The fingerprinted URL allows us to cache the resource collection forever and avoid an additional request + // to fetch the resource collection on subsequent visits. + var fingerprintSuffix = ComputeFingerprintSuffix(resourceCollection)[0..6]; + // $"/_framework/resource-collection.{fingerprint}.js"; + var fingerprintedResourceCollectionUrl = string.Format(CultureInfo.InvariantCulture, resourceCollectionUrlFormat, fingerprintSuffix, ""); + // $"/_framework/resource-collection.{fingerprintSuffix}.js.gz"; + var fingerprintedGzResourceCollectionUrl = string.Format(CultureInfo.InvariantCulture, resourceCollectionUrlFormat, fingerprintSuffix, ".gz"); + // $"/_framework/resource-collection.js" + var resourceCollectionUrl = string.Format(CultureInfo.InvariantCulture, resourceCollectionUrlFormat, "", ""); + // $"/_framework/resource-collection.js.gz" + var gzResourceCollectionUrl = string.Format(CultureInfo.InvariantCulture, resourceCollectionUrlFormat, "", ".gz"); + + var bytes = CreateContent(resourceCollection); + var gzipBytes = CreateGzipBytes(bytes); + var integrity = ComputeIntegrity(bytes); + + var resourceCollectionEndpoints = new ResourceCollectionEndpointsBuilder(bytes, gzipBytes); + var builders = resourceCollectionEndpoints.CreateEndpoints( + fingerprintedResourceCollectionUrl, + fingerprintedGzResourceCollectionUrl, + resourceCollectionUrl, + gzResourceCollectionUrl); + + foreach (var builder in builders) + { + var endpoint = builder.Build(); + endpoints.Add(endpoint); + } + + var importMapDefinition = new ImportMapDefinition( + new Dictionary + { + [resourceCollectionUrl] = $"./{fingerprintedResourceCollectionUrl}", + [gzResourceCollectionUrl] = $"./{fingerprintedGzResourceCollectionUrl}", + }, + scopes: null, + new Dictionary + { + [$"./{fingerprintedResourceCollectionUrl}"] = integrity, + [$"./{fingerprintedGzResourceCollectionUrl}"] = integrity, + }); + + return (importMapDefinition, $"./{fingerprintedResourceCollectionUrl}"); + } + + private static string ComputeIntegrity(byte[] bytes) + { + Span hash = stackalloc byte[32]; + SHA256.HashData(bytes, hash); + return $"sha256-{Convert.ToBase64String(hash)}"; + } + + private static byte[] CreateGzipBytes(byte[] bytes) + { + using var gzipContent = new MemoryStream(); + using var gzipStream = new GZipStream(gzipContent, CompressionLevel.Optimal, leaveOpen: true); + gzipStream.Write(bytes); + gzipStream.Flush(); + return gzipContent.ToArray(); + } + + private static byte[] CreateContent(ResourceAssetCollection resourceCollection) + { + var content = new MemoryStream(); + var preamble = """ + export function get() { + return + """u8; + content.Write(preamble); + var utf8Writer = new Utf8JsonWriter(content); + JsonSerializer.Serialize>(utf8Writer, resourceCollection, ResourceCollectionSerializerContext.Default.Options); + var epilogue = """ + ; + } + """u8; + content.Write(epilogue); + content.Flush(); + return content.ToArray(); + } + + private static string ComputeFingerprintSuffix(ResourceAssetCollection resourceCollection) + { + var resources = (IReadOnlyList)resourceCollection; + var incrementalHash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256); + Span buffer = stackalloc byte[1024]; + byte[]? rented = null; + Span result = stackalloc byte[incrementalHash.HashLengthInBytes]; + foreach (var resource in resources) + { + var url = resource.Url; + AppendToHash(incrementalHash, buffer, ref rented, url); + } + incrementalHash.GetCurrentHash(result); + // Base64 encoding at most increases size by (4 * byteSize / 3 + 2), + // add an extra byte for the initial dot. + Span fingerprintSpan = stackalloc char[incrementalHash.HashLengthInBytes * 4 / 3 + 3]; + var length = WebUtilities.WebEncoders.Base64UrlEncode(result, fingerprintSpan[1..]); + fingerprintSpan[0] = '.'; + return fingerprintSpan[..(length + 1)].ToString(); + } + + private static void AppendToHash(IncrementalHash incrementalHash, Span buffer, ref byte[]? rented, string value) + { + if (Encoding.UTF8.TryGetBytes(value, buffer, out var written)) + { + incrementalHash.AppendData(buffer[..written]); + } + else + { + var length = Encoding.UTF8.GetByteCount(value); + if (rented == null || rented.Length < length) + { + if (rented != null) + { + ArrayPool.Shared.Return(rented); + } + rented = ArrayPool.Shared.Rent(length); + var bytesWritten = Encoding.UTF8.GetBytes(value, rented); + incrementalHash.AppendData(rented.AsSpan(0, bytesWritten)); + } + } + } + + [JsonSerializable(typeof(ResourceAssetCollection))] + [JsonSerializable(typeof(IReadOnlyList))] + [JsonSourceGenerationOptions(DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault, WriteIndented = false)] + private partial class ResourceCollectionSerializerContext : JsonSerializerContext + { + } + + private class ResourceCollectionEndpointsBuilder + { + private readonly byte[] _content; + private readonly string _contentETag; + private readonly byte[] _gzipContent; + private readonly string[] _gzipContentETags; + + public ResourceCollectionEndpointsBuilder(byte[] content, byte[] gzipContent) + { + _content = content; + _contentETag = ComputeETagTag(content); + _gzipContent = gzipContent; + _gzipContentETags = [$"W/ {_contentETag}", ComputeETagTag(gzipContent)]; + } + + private string ComputeETagTag(byte[] content) + { + Span data = stackalloc byte[32]; + SHA256.HashData(content, data); + return $"\"{Convert.ToBase64String(data)}\""; + } + + public async Task FingerprintedGzipContent(HttpContext context) + { + WriteCommonHeaders(context, _gzipContent); + WriteEncodingHeaders(context); + WriteFingerprintHeaders(context); + await context.Response.Body.WriteAsync(_gzipContent); + } + + public async Task FingerprintedContent(HttpContext context) + { + WriteCommonHeaders(context, _content); + WriteFingerprintHeaders(context); + await context.Response.Body.WriteAsync(_content); + } + + public async Task Content(HttpContext context) + { + WriteCommonHeaders(context, _content); + WriteNonFingerprintedHeaders(context); + await context.Response.Body.WriteAsync(_content); + } + + public async Task GzipContent(HttpContext context) + { + WriteCommonHeaders(context, _gzipContent); + WriteEncodingHeaders(context); + WriteNonFingerprintedHeaders(context); + await context.Response.Body.WriteAsync(_gzipContent); + } + + private void WriteEncodingHeaders(HttpContext context) + { + context.Response.Headers[HeaderNames.ContentEncoding] = "gzip"; + context.Response.Headers[HeaderNames.Vary] = HeaderNames.AcceptEncoding; + context.Response.Headers.ETag = new StringValues(_gzipContentETags); + } + + private void WriteNoEncodingHeaders(HttpContext context) + { + context.Response.Headers.ETag = new StringValues(_contentETag); + } + + private static void WriteFingerprintHeaders(HttpContext context) + { + context.Response.Headers[HeaderNames.CacheControl] = "max-age=31536000, immutable"; + } + + private static void WriteNonFingerprintedHeaders(HttpContext context) + { + context.Response.Headers[HeaderNames.CacheControl] = "no-cache, must-revalidate"; + } + + private static void WriteCommonHeaders(HttpContext context, byte[] contents) + { + context.Response.ContentType = "application/javascript"; + context.Response.ContentLength = contents.Length; + } + + internal IEnumerable CreateEndpoints( + string fingerprintedResourceCollectionUrl, + string fingerprintedGzResourceCollectionUrl, + string resourceCollectionUrl, + string gzResourceCollectionUrl) + { + var quality = 1 / (1 + _gzipContent.Length); + var encodingMetadata = new ContentEncodingMetadata("gzip", quality); + + var fingerprintedGzBuilder = new RouteEndpointBuilder( + FingerprintedGzipContent, + RoutePatternFactory.Parse(fingerprintedGzResourceCollectionUrl), + -100); + + var fingerprintedBuilder = new RouteEndpointBuilder( + FingerprintedContent, + RoutePatternFactory.Parse(fingerprintedResourceCollectionUrl), + -100); + + var fingerprintedBuilderConeg = new RouteEndpointBuilder( + FingerprintedGzipContent, + RoutePatternFactory.Parse(fingerprintedResourceCollectionUrl), + -100); + + fingerprintedBuilderConeg.Metadata.Add(encodingMetadata); + + var gzBuilder = new RouteEndpointBuilder( + GzipContent, + RoutePatternFactory.Parse(gzResourceCollectionUrl), + -100); + + var builder = new RouteEndpointBuilder( + Content, + RoutePatternFactory.Parse(resourceCollectionUrl), + -100); + + var builderConeg = new RouteEndpointBuilder( + Content, + RoutePatternFactory.Parse(resourceCollectionUrl), + -100); + + builderConeg.Metadata.Add(encodingMetadata); + + yield return fingerprintedGzBuilder; + yield return fingerprintedBuilder; + yield return fingerprintedBuilderConeg; + yield return gzBuilder; + yield return builderConeg; + yield return builder; + } + } +} diff --git a/src/Components/Endpoints/src/Builder/ResourceCollectionUrlMetadata.cs b/src/Components/Endpoints/src/Builder/ResourceCollectionUrlMetadata.cs new file mode 100644 index 000000000000..1099f240f4da --- /dev/null +++ b/src/Components/Endpoints/src/Builder/ResourceCollectionUrlMetadata.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components.Endpoints; + +internal class ResourceCollectionUrlMetadata(string url) +{ + public string Url { get; } = url; +} diff --git a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs index 9582ba579942..0a10d83538ca 100644 --- a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs +++ b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs @@ -72,6 +72,8 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection services.AddSupplyValueFromQueryProvider(); services.TryAddCascadingValue(sp => sp.GetRequiredService().HttpContext); + services.TryAddScoped(); + // Form handling services.AddSupplyValueFromFormProvider(); services.TryAddScoped(); diff --git a/src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj b/src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj index 3c85a67deaa4..327f717d4af1 100644 --- a/src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj +++ b/src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj @@ -30,11 +30,13 @@ + + @@ -54,6 +56,7 @@ + diff --git a/src/Components/Endpoints/src/PublicAPI.Unshipped.txt b/src/Components/Endpoints/src/PublicAPI.Unshipped.txt index 95d31104cc53..867b5c2e5949 100644 --- a/src/Components/Endpoints/src/PublicAPI.Unshipped.txt +++ b/src/Components/Endpoints/src/PublicAPI.Unshipped.txt @@ -1,8 +1,23 @@ #nullable enable +Microsoft.AspNetCore.Components.ImportMap +Microsoft.AspNetCore.Components.ImportMap.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext? +Microsoft.AspNetCore.Components.ImportMap.HttpContext.set -> void +Microsoft.AspNetCore.Components.ImportMap.ImportMap() -> void +Microsoft.AspNetCore.Components.ImportMap.ImportMapDefinition.get -> Microsoft.AspNetCore.Components.ImportMapDefinition? +Microsoft.AspNetCore.Components.ImportMap.ImportMapDefinition.set -> void +Microsoft.AspNetCore.Components.ImportMapDefinition +Microsoft.AspNetCore.Components.ImportMapDefinition.ImportMapDefinition(System.Collections.Generic.IReadOnlyDictionary? imports, System.Collections.Generic.IReadOnlyDictionary!>? scopes, System.Collections.Generic.IReadOnlyDictionary? integrity) -> void +Microsoft.AspNetCore.Components.ImportMapDefinition.Imports.get -> System.Collections.Generic.IReadOnlyDictionary? +Microsoft.AspNetCore.Components.ImportMapDefinition.Integrity.get -> System.Collections.Generic.IReadOnlyDictionary? +Microsoft.AspNetCore.Components.ImportMapDefinition.Scopes.get -> System.Collections.Generic.IReadOnlyDictionary!>? Microsoft.AspNetCore.Components.Routing.RazorComponentsEndpointHttpContextExtensions Microsoft.AspNetCore.Components.Server.ServerAuthenticationStateProvider Microsoft.AspNetCore.Components.Server.ServerAuthenticationStateProvider.ServerAuthenticationStateProvider() -> void Microsoft.AspNetCore.Components.Server.ServerAuthenticationStateProvider.SetAuthenticationState(System.Threading.Tasks.Task! authenticationStateTask) -> void +override Microsoft.AspNetCore.Components.ImportMapDefinition.ToString() -> string! override Microsoft.AspNetCore.Components.Server.ServerAuthenticationStateProvider.GetAuthenticationStateAsync() -> System.Threading.Tasks.Task! +static Microsoft.AspNetCore.Builder.RazorComponentsEndpointConventionBuilderExtensions.WithResourceCollection(this Microsoft.AspNetCore.Builder.RazorComponentsEndpointConventionBuilder! builder, string? manifestPath = null) -> Microsoft.AspNetCore.Builder.RazorComponentsEndpointConventionBuilder! static Microsoft.AspNetCore.Components.Endpoints.Infrastructure.ComponentEndpointConventionBuilderHelper.GetEndpointRouteBuilder(Microsoft.AspNetCore.Builder.RazorComponentsEndpointConventionBuilder! builder) -> Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! +static Microsoft.AspNetCore.Components.ImportMapDefinition.Combine(params Microsoft.AspNetCore.Components.ImportMapDefinition![]! sources) -> Microsoft.AspNetCore.Components.ImportMapDefinition! +static Microsoft.AspNetCore.Components.ImportMapDefinition.FromResourceCollection(Microsoft.AspNetCore.Components.ResourceAssetCollection! assets) -> Microsoft.AspNetCore.Components.ImportMapDefinition! static Microsoft.AspNetCore.Components.Routing.RazorComponentsEndpointHttpContextExtensions.AcceptsInteractiveRouting(this Microsoft.AspNetCore.Http.HttpContext! context) -> bool diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs index 83b1a94e4cf9..b13e4fdb0965 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs @@ -41,6 +41,7 @@ internal partial class EndpointHtmlRenderer : StaticHtmlRenderer, IComponentPrer private readonly RazorComponentsServiceOptions _options; private Task? _servicesInitializedTask; private HttpContext _httpContext = default!; // Always set at the start of an inbound call + private ResourceAssetCollection? _resourceCollection; // The underlying Renderer always tracks the pending tasks representing *full* quiescence, i.e., // when everything (regardless of streaming SSR) is fully complete. In this subclass we also track @@ -96,6 +97,8 @@ internal static async Task InitializeStandardComponentServicesAsync( } } + InitializeResourceCollection(httpContext); + if (handler != null && form != null) { httpContext.RequestServices.GetRequiredService() @@ -117,16 +120,40 @@ internal static async Task InitializeStandardComponentServicesAsync( // Saving RouteData to avoid routing twice in Router component var routingStateProvider = httpContext.RequestServices.GetRequiredService(); routingStateProvider.RouteData = new RouteData(componentType, httpContext.GetRouteData().Values); - if (httpContext.GetEndpoint() is RouteEndpoint endpoint) + if (httpContext.GetEndpoint() is RouteEndpoint routeEndpoint) { - routingStateProvider.RouteData.Template = endpoint.RoutePattern.RawText; + routingStateProvider.RouteData.Template = routeEndpoint.RoutePattern.RawText; } } } + private static void InitializeResourceCollection(HttpContext httpContext) + { + var resourceCollectionProvider = httpContext.RequestServices.GetRequiredService(); + + var endpoint = httpContext.GetEndpoint(); + var resourceCollection = GetResourceCollection(httpContext); + var resourceCollectionUrl = resourceCollection != null && endpoint != null ? + endpoint.Metadata.GetMetadata() : + null; + + if (resourceCollectionUrl != null) + { + resourceCollectionProvider.SetResourceCollectionUrl(resourceCollectionUrl.Url); + } + + resourceCollectionProvider.SetResourceCollection(resourceCollection ?? ResourceAssetCollection.Empty); + } + protected override ComponentState CreateComponentState(int componentId, IComponent component, ComponentState? parentComponentState) => new EndpointComponentState(this, componentId, component, parentComponentState); + /// + protected override ResourceAssetCollection Assets => + _resourceCollection ??= GetResourceCollection(_httpContext) ?? base.Assets; + + private static ResourceAssetCollection? GetResourceCollection(HttpContext httpContext) => httpContext.GetEndpoint()?.Metadata.GetMetadata(); + protected override void AddPendingTask(ComponentState? componentState, Task task) { var streamRendering = componentState is null diff --git a/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs b/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs index 4370d5bdedd2..1144c4360a89 100644 --- a/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs +++ b/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs @@ -1680,7 +1680,7 @@ private static ServiceCollection CreateDefaultServiceCollection() services.AddSingleton(sp => sp.GetRequiredService().State); services.AddSingleton(); services.AddSingleton(_ => new SupplyParameterFromFormValueProvider(null, "")); - + services.AddScoped(); return services; } diff --git a/src/Components/Endpoints/test/HotReloadServiceTests.cs b/src/Components/Endpoints/test/HotReloadServiceTests.cs index 08c83562a581..1989af91372c 100644 --- a/src/Components/Endpoints/test/HotReloadServiceTests.cs +++ b/src/Components/Endpoints/test/HotReloadServiceTests.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Components.Endpoints.Infrastructure; +using Microsoft.AspNetCore.Components.Infrastructure; namespace Microsoft.AspNetCore.Components.Endpoints.Tests; diff --git a/src/Components/Endpoints/test/RazorComponentEndpointFactoryTest.cs b/src/Components/Endpoints/test/RazorComponentEndpointFactoryTest.cs index a7dbd751bf25..9ad0897be660 100644 --- a/src/Components/Endpoints/test/RazorComponentEndpointFactoryTest.cs +++ b/src/Components/Endpoints/test/RazorComponentEndpointFactoryTest.cs @@ -20,7 +20,9 @@ public void AddEndpoints_CreatesEndpointWithExpectedMetadata() var finallyConventions = new List>(); var testRenderMode = new TestRenderMode(); var configuredRenderModes = new ConfiguredRenderModesMetadata(new[] { testRenderMode }); - factory.AddEndpoints(endpoints, typeof(App), new PageComponentInfo( + factory.AddEndpoints( + endpoints, + typeof(App), new PageComponentInfo( "App", typeof(App), "/", diff --git a/src/Components/Server/src/Circuits/CircuitFactory.cs b/src/Components/Server/src/Circuits/CircuitFactory.cs index be600dc4db1e..c22aca391664 100644 --- a/src/Components/Server/src/Circuits/CircuitFactory.cs +++ b/src/Components/Server/src/Circuits/CircuitFactory.cs @@ -39,7 +39,8 @@ public async ValueTask CreateCircuitHostAsync( string baseUri, string uri, ClaimsPrincipal user, - IPersistentComponentStateStore store) + IPersistentComponentStateStore store, + ResourceAssetCollection resourceCollection) { var scope = _scopeFactory.CreateAsyncScope(); var jsRuntime = (RemoteJSRuntime)scope.ServiceProvider.GetRequiredService(); @@ -80,7 +81,8 @@ public async ValueTask CreateCircuitHostAsync( serverComponentDeserializer, _loggerFactory.CreateLogger(), jsRuntime, - jsComponentInterop); + jsComponentInterop, + resourceCollection); // In Blazor Server we have already restored the app state, so we can get the handlers from DI. // In Blazor Web the state is provided in the first call to UpdateRootComponents, so we need to diff --git a/src/Components/Server/src/Circuits/ICircuitFactory.cs b/src/Components/Server/src/Circuits/ICircuitFactory.cs index dd68821c2c41..f2627dfd2ad5 100644 --- a/src/Components/Server/src/Circuits/ICircuitFactory.cs +++ b/src/Components/Server/src/Circuits/ICircuitFactory.cs @@ -13,5 +13,6 @@ ValueTask CreateCircuitHostAsync( string baseUri, string uri, ClaimsPrincipal user, - IPersistentComponentStateStore store); + IPersistentComponentStateStore store, + ResourceAssetCollection resourceCollection); } diff --git a/src/Components/Server/src/Circuits/RemoteRenderer.cs b/src/Components/Server/src/Circuits/RemoteRenderer.cs index 370c80d1edbd..c589d4eef88f 100644 --- a/src/Components/Server/src/Circuits/RemoteRenderer.cs +++ b/src/Components/Server/src/Circuits/RemoteRenderer.cs @@ -24,6 +24,7 @@ internal partial class RemoteRenderer : WebRenderer private readonly CircuitOptions _options; private readonly IServerComponentDeserializer _serverComponentDeserializer; private readonly ILogger _logger; + private readonly ResourceAssetCollection _resourceCollection; internal readonly ConcurrentQueue _unacknowledgedRenderBatches = new ConcurrentQueue(); private long _nextRenderId = 1; private bool _disposing; @@ -44,19 +45,23 @@ public RemoteRenderer( IServerComponentDeserializer serverComponentDeserializer, ILogger logger, RemoteJSRuntime jsRuntime, - CircuitJSComponentInterop jsComponentInterop) + CircuitJSComponentInterop jsComponentInterop, + ResourceAssetCollection resourceCollection = null) : base(serviceProvider, loggerFactory, jsRuntime.ReadJsonSerializerOptions(), jsComponentInterop) { _client = client; _options = options; _serverComponentDeserializer = serverComponentDeserializer; _logger = logger; + _resourceCollection = resourceCollection; ElementReferenceContext = jsRuntime.ElementReferenceContext; } public override Dispatcher Dispatcher { get; } = Dispatcher.CreateDefault(); + protected override ResourceAssetCollection Assets => _resourceCollection ?? base.Assets; + protected override ComponentPlatform ComponentPlatform => _componentPlatform; protected override IComponentRenderMode? GetComponentRenderMode(IComponent component) => RenderMode.InteractiveServer; diff --git a/src/Components/Server/src/ComponentHub.cs b/src/Components/Server/src/ComponentHub.cs index 3bac248d676e..b3308f17bfd7 100644 --- a/src/Components/Server/src/ComponentHub.cs +++ b/src/Components/Server/src/ComponentHub.cs @@ -123,14 +123,15 @@ public async ValueTask StartCircuit(string baseUri, string uri, string s var store = !string.IsNullOrEmpty(applicationState) ? new ProtectedPrerenderComponentApplicationStore(applicationState, _dataProtectionProvider) : new ProtectedPrerenderComponentApplicationStore(_dataProtectionProvider); - + var resourceCollection = Context.GetHttpContext().GetEndpoint()?.Metadata.GetMetadata(); circuitHost = await _circuitFactory.CreateCircuitHostAsync( components, circuitClient, baseUri, uri, Context.User, - store); + store, + resourceCollection); // Fire-and-forget the initialization process, because we can't block the // SignalR message loop (we'd get a deadlock if any of the initialization diff --git a/src/Components/Server/test/Circuits/ComponentHubTest.cs b/src/Components/Server/test/Circuits/ComponentHubTest.cs index 2800e311a064..0b658a611e1f 100644 --- a/src/Components/Server/test/Circuits/ComponentHubTest.cs +++ b/src/Components/Server/test/Circuits/ComponentHubTest.cs @@ -194,7 +194,8 @@ public ValueTask CreateCircuitHostAsync( string baseUri, string uri, ClaimsPrincipal user, - IPersistentComponentStateStore store) + IPersistentComponentStateStore store, + ResourceAssetCollection resourceCollection) { var serviceScope = new Mock(); var circuitHost = TestCircuitHost.Create(serviceScope: new AsyncServiceScope(serviceScope.Object)); diff --git a/src/Components/Shared/src/ResourceCollectionProvider.cs b/src/Components/Shared/src/ResourceCollectionProvider.cs new file mode 100644 index 000000000000..a0c50b8c2ae8 --- /dev/null +++ b/src/Components/Shared/src/ResourceCollectionProvider.cs @@ -0,0 +1,62 @@ +// 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.AspNetCore.Components.Web; +using Microsoft.JSInterop; + +namespace Microsoft.AspNetCore.Components; + +internal class ResourceCollectionProvider +{ + private const string ResourceCollectionUrlKey = "__ResourceCollectionUrl"; + private string? _url; + private ResourceAssetCollection? _resourceCollection; + private readonly PersistentComponentState _state; + private readonly IJSRuntime _jsRuntime; + + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Strings are not trimmed")] + public ResourceCollectionProvider(PersistentComponentState state, IJSRuntime jsRuntime) + { + _state = state; + _jsRuntime = jsRuntime; + _ = _state.TryTakeFromJson(ResourceCollectionUrlKey, out _url); + } + + [MemberNotNull(nameof(_url))] + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Strings are not trimmed")] + internal void SetResourceCollectionUrl(string url) + { + if (_url != null) + { + throw new InvalidOperationException("The resource collection URL has already been set."); + } + _url = url; + PersistingComponentStateSubscription registration = default; + registration = _state.RegisterOnPersisting(() => + { + _state.PersistAsJson(ResourceCollectionUrlKey, _url); + registration.Dispose(); + return Task.CompletedTask; + }, RenderMode.InteractiveWebAssembly); + } + + internal async Task GetResourceCollection() + { + _resourceCollection = _resourceCollection ??= await LoadResourceCollection(); + return _resourceCollection; + } + + internal void SetResourceCollection(ResourceAssetCollection resourceCollection) + { + _resourceCollection = resourceCollection; + } + + private async Task LoadResourceCollection() + { + _url ??= "/_framework/resource-collection.js"; + var module = await _jsRuntime.InvokeAsync("import", _url); + var result = await module.InvokeAsync("get"); + return result == null ? ResourceAssetCollection.Empty : new ResourceAssetCollection(result); + } +} diff --git a/src/Components/WebAssembly/Server/src/Builder/WebAssemblyRazorComponentsEndpointConventionBuilderExtensions.cs b/src/Components/WebAssembly/Server/src/Builder/WebAssemblyRazorComponentsEndpointConventionBuilderExtensions.cs index dd81603a225f..edcdc2616295 100644 --- a/src/Components/WebAssembly/Server/src/Builder/WebAssemblyRazorComponentsEndpointConventionBuilderExtensions.cs +++ b/src/Components/WebAssembly/Server/src/Builder/WebAssemblyRazorComponentsEndpointConventionBuilderExtensions.cs @@ -6,7 +6,6 @@ using Microsoft.AspNetCore.Components.Endpoints.Infrastructure; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Server; -using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.StaticAssets.Infrastructure; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -55,9 +54,8 @@ public static RazorComponentsEndpointConventionBuilder AddInteractiveWebAssembly // If the static assets data source for the given manifest name is already added, then just wire-up the Blazor WebAssembly conventions. // MapStaticWebAssetEndpoints is idempotent and will not add the data source if it already exists. - if (HasStaticAssetDataSource(endpointBuilder, options.StaticAssetsManifestPath)) + if (StaticAssetsEndpointDataSourceHelper.HasStaticAssetsDataSource(endpointBuilder, options.StaticAssetsManifestPath)) { - options.ConventionsApplied = true; endpointBuilder.MapStaticAssets(options.StaticAssetsManifestPath) .AddBlazorWebAssemblyConventions(); @@ -81,19 +79,6 @@ public static RazorComponentsEndpointConventionBuilder AddInteractiveWebAssembly return builder; } - private static bool HasStaticAssetDataSource(IEndpointRouteBuilder endpointRouteBuilder, string? staticAssetsManifestName) - { - foreach (var ds in endpointRouteBuilder.DataSources) - { - if (StaticAssetsEndpointDataSourceHelper.IsStaticAssetsDataSource(ds, staticAssetsManifestName)) - { - return true; - } - } - - return false; - } - internal static partial class Log { [LoggerMessage(1, LogLevel.Warning, $$"""Mapped static asset endpoints not found. Ensure '{{nameof(StaticAssetsEndpointRouteBuilderExtensions.MapStaticAssets)}}' is called before '{{nameof(AddInteractiveWebAssemblyRenderMode)}}'.""")] diff --git a/src/Components/WebAssembly/Server/src/WebAssemblyEndpointProvider.cs b/src/Components/WebAssembly/Server/src/WebAssemblyEndpointProvider.cs new file mode 100644 index 000000000000..7176462b7998 --- /dev/null +++ b/src/Components/WebAssembly/Server/src/WebAssemblyEndpointProvider.cs @@ -0,0 +1,83 @@ +// 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.Builder; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Endpoints.Infrastructure; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace Microsoft.Extensions.DependencyInjection; + +internal class WebAssemblyEndpointProvider(IServiceProvider services) : RenderModeEndpointProvider +{ + private const string ResourceCollectionKey = "__ResourceCollectionKey"; + + public override IEnumerable GetEndpointBuilders(IComponentRenderMode renderMode, IApplicationBuilder applicationBuilder) + { + if (renderMode is not WebAssemblyRenderModeWithOptions wasmWithOptions) + { + return renderMode is InteractiveWebAssemblyRenderMode + ? throw new InvalidOperationException("Invalid render mode. Use AddInteractiveWebAssemblyRenderMode(Action) to configure the WebAssembly render mode.") + : []; + } + if (applicationBuilder.Properties[ResourceCollectionKey] is ResourceAssetCollection assetMap) + { + return []; + } + else + { + // In case the app didn't call MapStaticAssets, use the 8.0 approach to map the assets. + var endpointRouteBuilder = new WebAssemblyEndpointRouteBuilder(services, applicationBuilder); + var pathPrefix = wasmWithOptions.EndpointOptions?.PathPrefix; + + applicationBuilder.UseBlazorFrameworkFiles(pathPrefix ?? default); + var app = applicationBuilder.Build(); + + endpointRouteBuilder.Map($"{pathPrefix}/_framework/{{*path}}", context => + { + // Set endpoint to null so the static files middleware will handle the request. + context.SetEndpoint(null); + + return app(context); + }); + + return endpointRouteBuilder.GetEndpoints(); + } + } + + public override bool Supports(IComponentRenderMode renderMode) => + renderMode is InteractiveWebAssemblyRenderMode or InteractiveAutoRenderMode; + + private class WebAssemblyEndpointRouteBuilder(IServiceProvider serviceProvider, IApplicationBuilder applicationBuilder) : IEndpointRouteBuilder + { + public IServiceProvider ServiceProvider { get; } = serviceProvider; + + public ICollection DataSources { get; } = []; + + public IApplicationBuilder CreateApplicationBuilder() + { + return applicationBuilder.New(); + } + + internal IEnumerable GetEndpoints() + { + foreach (var ds in DataSources) + { + foreach (var endpoint in ds.Endpoints) + { + var routeEndpoint = (RouteEndpoint)endpoint; + var builder = new RouteEndpointBuilder(endpoint.RequestDelegate, routeEndpoint.RoutePattern, routeEndpoint.Order); + for (var i = 0; i < routeEndpoint.Metadata.Count; i++) + { + var metadata = routeEndpoint.Metadata[i]; + builder.Metadata.Add(metadata); + } + + yield return builder; + } + } + } + } +} diff --git a/src/Components/WebAssembly/Server/src/WebAssemblyRazorComponentsBuilderExtensions.cs b/src/Components/WebAssembly/Server/src/WebAssemblyRazorComponentsBuilderExtensions.cs index ffe90126c662..0dfb50ef840a 100644 --- a/src/Components/WebAssembly/Server/src/WebAssemblyRazorComponentsBuilderExtensions.cs +++ b/src/Components/WebAssembly/Server/src/WebAssemblyRazorComponentsBuilderExtensions.cs @@ -1,14 +1,10 @@ // 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.Builder; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Endpoints.Infrastructure; -using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Server; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection.Extensions; namespace Microsoft.Extensions.DependencyInjection; @@ -50,82 +46,4 @@ public static IRazorComponentsBuilder AddAuthenticationStateSerialization(this I return builder; } - - private class WebAssemblyEndpointProvider(IServiceProvider services) : RenderModeEndpointProvider - { - public override IEnumerable GetEndpointBuilders(IComponentRenderMode renderMode, IApplicationBuilder applicationBuilder) - { - if (renderMode is not WebAssemblyRenderModeWithOptions wasmWithOptions) - { - return renderMode is InteractiveWebAssemblyRenderMode - ? throw new InvalidOperationException("Invalid render mode. Use AddInteractiveWebAssemblyRenderMode(Action) to configure the WebAssembly render mode.") - : (IEnumerable)Array.Empty(); - } - if (wasmWithOptions is { EndpointOptions.ConventionsApplied: true }) - { - return []; // No need to add additional endpoints to the DS, they are already added - } - else - { - // In case the app didn't call MapStaticAssets, use the 8.0 approach to map the assets. - var endpointRouteBuilder = new EndpointRouteBuilder(services, applicationBuilder); - var pathPrefix = wasmWithOptions.EndpointOptions?.PathPrefix; - - applicationBuilder.UseBlazorFrameworkFiles(pathPrefix ?? default); - var app = applicationBuilder.Build(); - - endpointRouteBuilder.Map($"{pathPrefix}/_framework/{{*path}}", context => - { - // Set endpoint to null so the static files middleware will handle the request. - context.SetEndpoint(null); - - return app(context); - }); - - return endpointRouteBuilder.GetEndpoints(); - } - } - - public override bool Supports(IComponentRenderMode renderMode) => - renderMode is InteractiveWebAssemblyRenderMode or InteractiveAutoRenderMode; - - private class EndpointRouteBuilder : IEndpointRouteBuilder - { - private readonly IApplicationBuilder _applicationBuilder; - - public EndpointRouteBuilder(IServiceProvider serviceProvider, IApplicationBuilder applicationBuilder) - { - ServiceProvider = serviceProvider; - _applicationBuilder = applicationBuilder; - } - - public IServiceProvider ServiceProvider { get; } - - public ICollection DataSources { get; } = []; - - public IApplicationBuilder CreateApplicationBuilder() - { - return _applicationBuilder.New(); - } - - internal IEnumerable GetEndpoints() - { - foreach (var ds in DataSources) - { - foreach (var endpoint in ds.Endpoints) - { - var routeEndpoint = (RouteEndpoint)endpoint; - var builder = new RouteEndpointBuilder(endpoint.RequestDelegate, routeEndpoint.RoutePattern, routeEndpoint.Order); - for (var i = 0; i < routeEndpoint.Metadata.Count; i++) - { - var metadata = routeEndpoint.Metadata[i]; - builder.Metadata.Add(metadata); - } - - yield return builder; - } - } - } - } - } } diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs index 01e51c2920f0..32cabdf6c564 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs @@ -143,12 +143,13 @@ internal async Task RunAsyncCore(CancellationToken cancellationToken, WebAssembl } var tcs = new TaskCompletionSource(); - using (cancellationToken.Register(() => tcs.TrySetResult())) { var loggerFactory = Services.GetRequiredService(); var jsComponentInterop = new JSComponentInterop(_rootComponents.JSComponents); - _renderer = new WebAssemblyRenderer(Services, loggerFactory, jsComponentInterop); + var collectionProvider = Services.GetRequiredService(); + var collection = await collectionProvider.GetResourceCollection(); + _renderer = new WebAssemblyRenderer(Services, collection, loggerFactory, jsComponentInterop); WebAssemblyNavigationManager.Instance.CreateLogger(loggerFactory); diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs index 47f7305e8b49..8bee06649691 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs @@ -307,6 +307,7 @@ internal void InitializeDefaultServices() Services.AddSingleton(sp => sp.GetRequiredService().State); Services.AddSingleton(); Services.AddSingleton(); + Services.AddSingleton(); Services.AddLogging(builder => { builder.AddProvider(new WebAssemblyConsoleLoggerProvider(DefaultWebAssemblyJSRuntime.Instance)); diff --git a/src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.csproj b/src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.csproj index 896817768457..de7bfc87167c 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.csproj +++ b/src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.csproj @@ -43,6 +43,7 @@ + diff --git a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs index 96174ead5c30..cc9426e2607c 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs @@ -23,10 +23,11 @@ internal sealed partial class WebAssemblyRenderer : WebRenderer { private readonly ILogger _logger; private readonly Dispatcher _dispatcher; + private readonly ResourceAssetCollection _resourceCollection; private readonly IInternalJSImportMethods _jsMethods; private static readonly ComponentPlatform _componentPlatform = new("WebAssembly", isInteractive: true); - public WebAssemblyRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, JSComponentInterop jsComponentInterop) + public WebAssemblyRenderer(IServiceProvider serviceProvider, ResourceAssetCollection resourceCollection, ILoggerFactory loggerFactory, JSComponentInterop jsComponentInterop) : base(serviceProvider, loggerFactory, DefaultWebAssemblyJSRuntime.Instance.ReadJsonSerializerOptions(), jsComponentInterop) { _logger = loggerFactory.CreateLogger(); @@ -37,6 +38,8 @@ public WebAssemblyRenderer(IServiceProvider serviceProvider, ILoggerFactory logg ? NullDispatcher.Instance : new WebAssemblyDispatcher(); + _resourceCollection = resourceCollection; + ElementReferenceContext = DefaultWebAssemblyJSRuntime.Instance.ElementReferenceContext; DefaultWebAssemblyJSRuntime.Instance.OnUpdateRootComponents += OnUpdateRootComponents; } @@ -80,6 +83,8 @@ public void NotifyEndUpdateRootComponents(long batchId) _jsMethods.EndUpdateRootComponents(batchId); } + protected override ResourceAssetCollection Assets => _resourceCollection; + protected override ComponentPlatform ComponentPlatform => _componentPlatform; public override Dispatcher Dispatcher => _dispatcher; diff --git a/src/Framework/App.Ref/src/CompatibilitySuppressions.xml b/src/Framework/App.Ref/src/CompatibilitySuppressions.xml index 9b3df761dcc8..5fd58d407c27 100644 --- a/src/Framework/App.Ref/src/CompatibilitySuppressions.xml +++ b/src/Framework/App.Ref/src/CompatibilitySuppressions.xml @@ -1,5 +1,5 @@  - + PKV004 diff --git a/src/Framework/App.Runtime/src/CompatibilitySuppressions.xml b/src/Framework/App.Runtime/src/CompatibilitySuppressions.xml index addf7e253000..044b90f8b6da 100644 --- a/src/Framework/App.Runtime/src/CompatibilitySuppressions.xml +++ b/src/Framework/App.Runtime/src/CompatibilitySuppressions.xml @@ -1,5 +1,5 @@  - + PKV0001 diff --git a/src/Http/WebUtilities/src/PublicAPI.Unshipped.txt b/src/Http/WebUtilities/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..1edfae904adf 100644 --- a/src/Http/WebUtilities/src/PublicAPI.Unshipped.txt +++ b/src/Http/WebUtilities/src/PublicAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +static Microsoft.AspNetCore.WebUtilities.WebEncoders.Base64UrlEncode(System.ReadOnlySpan input, System.Span output) -> int diff --git a/src/Mvc/Mvc.Core/src/Builder/ControllerActionEndpointConventionBuilder.cs b/src/Mvc/Mvc.Core/src/Builder/ControllerActionEndpointConventionBuilder.cs index d231cac8c3a0..8a59a8b1204d 100644 --- a/src/Mvc/Mvc.Core/src/Builder/ControllerActionEndpointConventionBuilder.cs +++ b/src/Mvc/Mvc.Core/src/Builder/ControllerActionEndpointConventionBuilder.cs @@ -23,6 +23,8 @@ internal ControllerActionEndpointConventionBuilder(object @lock, List Items { get; set; } = []; + /// /// Adds the specified convention to the builder. Conventions are used to customize instances. /// diff --git a/src/Mvc/Mvc.Core/src/Builder/ControllerEndpointRouteBuilderExtensions.cs b/src/Mvc/Mvc.Core/src/Builder/ControllerEndpointRouteBuilderExtensions.cs index 8156d0b7157b..f134890f4389 100644 --- a/src/Mvc/Mvc.Core/src/Builder/ControllerEndpointRouteBuilderExtensions.cs +++ b/src/Mvc/Mvc.Core/src/Builder/ControllerEndpointRouteBuilderExtensions.cs @@ -28,7 +28,9 @@ public static ControllerActionEndpointConventionBuilder MapControllers(this IEnd EnsureControllerServices(endpoints); - return GetOrCreateDataSource(endpoints).DefaultBuilder; + var result = GetOrCreateDataSource(endpoints).DefaultBuilder; + result.Items["__EndpointRouteBuilder"] = endpoints; + return result; } /// diff --git a/src/Mvc/Mvc.Razor/src/TagHelpers/UrlResolutionTagHelper.cs b/src/Mvc/Mvc.Razor/src/TagHelpers/UrlResolutionTagHelper.cs index b20c3b5c6300..12af3d549e5c 100644 --- a/src/Mvc/Mvc.Razor/src/TagHelpers/UrlResolutionTagHelper.cs +++ b/src/Mvc/Mvc.Razor/src/TagHelpers/UrlResolutionTagHelper.cs @@ -4,7 +4,9 @@ using System.Buffers; using System.Diagnostics.CodeAnalysis; using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.ViewFeatures; @@ -221,6 +223,8 @@ protected bool TryResolveUrl([StringSyntax(StringSyntaxAttribute.Uri, UriKind.Re return false; } + trimmedUrl = GetVersionedResourceUrl(trimmedUrl); + var urlHelper = UrlHelperFactory.GetUrlHelper(ViewContext); resolvedUrl = urlHelper.Content(trimmedUrl); @@ -244,6 +248,8 @@ protected bool TryResolveUrl([StringSyntax(StringSyntaxAttribute.Uri, UriKind.Re return false; } + trimmedUrl = GetVersionedResourceUrl(trimmedUrl); + var urlHelper = UrlHelperFactory.GetUrlHelper(ViewContext); var appRelativeUrl = urlHelper.Content(trimmedUrl); var postTildeSlashUrlValue = trimmedUrl.Substring(2); @@ -299,6 +305,36 @@ private static bool TryCreateTrimmedString(string input, [NotNullWhen(true)] out return true; } + private string GetVersionedResourceUrl(string value) + { + var assetCollection = GetAssetCollection(); + if (assetCollection != null) + { + var src = assetCollection[value]; + if (!string.Equals(src, value, StringComparison.Ordinal)) + { + return src; + } + var pathBase = ViewContext.HttpContext.Request.PathBase; + if (pathBase.HasValue && value.StartsWith(pathBase, StringComparison.OrdinalIgnoreCase)) + { + var relativePath = value[pathBase.Value.Length..]; + src = assetCollection[relativePath]; + if (!string.Equals(src, relativePath, StringComparison.Ordinal)) + { + return src; + } + } + } + + return value; + } + + private ResourceAssetCollection? GetAssetCollection() + { + return ViewContext.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); + } + private sealed class EncodeFirstSegmentContent : IHtmlContent { private readonly string _firstSegment; diff --git a/src/Mvc/Mvc.RazorPages/src/Builder/PageActionEndpointConventionBuilder.cs b/src/Mvc/Mvc.RazorPages/src/Builder/PageActionEndpointConventionBuilder.cs index c4b6694f9cb7..c041e25808a6 100644 --- a/src/Mvc/Mvc.RazorPages/src/Builder/PageActionEndpointConventionBuilder.cs +++ b/src/Mvc/Mvc.RazorPages/src/Builder/PageActionEndpointConventionBuilder.cs @@ -23,6 +23,8 @@ internal PageActionEndpointConventionBuilder(object @lock, List Items { get; set; } = new Dictionary(); + /// /// Adds the specified convention to the builder. Conventions are used to customize instances. /// diff --git a/src/Mvc/Mvc.RazorPages/src/Builder/PageActionEndpointConventionBuilderResourceCollectionExtensions.cs b/src/Mvc/Mvc.RazorPages/src/Builder/PageActionEndpointConventionBuilderResourceCollectionExtensions.cs new file mode 100644 index 000000000000..f0e351fa692e --- /dev/null +++ b/src/Mvc/Mvc.RazorPages/src/Builder/PageActionEndpointConventionBuilderResourceCollectionExtensions.cs @@ -0,0 +1,47 @@ +// 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.Components; +using Microsoft.AspNetCore.Components.Endpoints; +using Microsoft.AspNetCore.Routing; + +namespace Microsoft.AspNetCore.Builder; + +/// +/// Extensions for . +/// +public static class PageActionEndpointConventionBuilderResourceCollectionExtensions +{ + /// + /// Adds a metadata instance to the endpoints. + /// + /// The . + /// The manifest associated with the assets. + /// + public static PageActionEndpointConventionBuilder WithResourceCollection( + this PageActionEndpointConventionBuilder builder, + string? manifestPath = null) + { + ArgumentNullException.ThrowIfNull(builder); + + var endpointBuilder = builder.Items["__EndpointRouteBuilder"]; + var (resolver, registered) = builder.Items.TryGetValue("__ResourceCollectionResolver", out var value) + ? ((ResourceCollectionResolver)value, true) + : (new ResourceCollectionResolver((IEndpointRouteBuilder)endpointBuilder), false); + + resolver.ManifestName = manifestPath; + if (!registered) + { + var collection = resolver.ResolveResourceCollection(); + var importMap = resolver.ResolveImportMap(); + + builder.Add(endpointBuilder => + { + endpointBuilder.Metadata.Add(collection); + endpointBuilder.Metadata.Add(importMap); + }); + } + + return builder; + } +} diff --git a/src/Mvc/Mvc.RazorPages/src/Builder/RazorPagesEndpointRouteBuilderExtensions.cs b/src/Mvc/Mvc.RazorPages/src/Builder/RazorPagesEndpointRouteBuilderExtensions.cs index a5d579105888..4331de28d236 100644 --- a/src/Mvc/Mvc.RazorPages/src/Builder/RazorPagesEndpointRouteBuilderExtensions.cs +++ b/src/Mvc/Mvc.RazorPages/src/Builder/RazorPagesEndpointRouteBuilderExtensions.cs @@ -28,7 +28,12 @@ public static PageActionEndpointConventionBuilder MapRazorPages(this IEndpointRo EnsureRazorPagesServices(endpoints); - return GetOrCreateDataSource(endpoints).DefaultBuilder; + var builder = GetOrCreateDataSource(endpoints).DefaultBuilder; + if (!builder.Items.ContainsKey("__EndpointRouteBuilder")) + { + builder.Items["__EndpointRouteBuilder"] = endpoints; + } + return builder; } /// diff --git a/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageActionEndpointDataSourceFactory.cs b/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageActionEndpointDataSourceFactory.cs index b882f1029f42..b1e0b718bdd7 100644 --- a/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageActionEndpointDataSourceFactory.cs +++ b/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageActionEndpointDataSourceFactory.cs @@ -24,6 +24,10 @@ public PageActionEndpointDataSourceFactory( public PageActionEndpointDataSource Create(OrderedEndpointsSequenceProvider orderProvider) { - return new PageActionEndpointDataSource(_dataSourceIdProvider, _actions, _endpointFactory, orderProvider); + return new PageActionEndpointDataSource( + _dataSourceIdProvider, + _actions, + _endpointFactory, + orderProvider); } } diff --git a/src/Mvc/Mvc.RazorPages/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.RazorPages/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..048b7fc544c1 100644 --- a/src/Mvc/Mvc.RazorPages/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.RazorPages/src/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +Microsoft.AspNetCore.Builder.PageActionEndpointConventionBuilderResourceCollectionExtensions +static Microsoft.AspNetCore.Builder.PageActionEndpointConventionBuilderResourceCollectionExtensions.WithResourceCollection(this Microsoft.AspNetCore.Builder.PageActionEndpointConventionBuilder! builder, string? manifestPath = null) -> Microsoft.AspNetCore.Builder.PageActionEndpointConventionBuilder! diff --git a/src/Mvc/Mvc.TagHelpers/src/ImageTagHelper.cs b/src/Mvc/Mvc.TagHelpers/src/ImageTagHelper.cs index 9cdd30d32e13..583e3330772b 100644 --- a/src/Mvc/Mvc.TagHelpers/src/ImageTagHelper.cs +++ b/src/Mvc/Mvc.TagHelpers/src/ImageTagHelper.cs @@ -2,7 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Razor.Infrastructure; using Microsoft.AspNetCore.Mvc.Razor.TagHelpers; using Microsoft.AspNetCore.Mvc.Routing; @@ -123,8 +125,9 @@ public override void Process(TagHelperContext context, TagHelperOutput output) // pipeline have touched the value. If the value is already encoded this ImageTagHelper may // not function properly. Src = output.Attributes[SrcAttributeName].Value as string; + var src = GetVersionedResourceUrl(Src); - output.Attributes.SetAttribute(SrcAttributeName, FileVersionProvider.AddFileVersionToPath(ViewContext.HttpContext.Request.PathBase, Src)); + output.Attributes.SetAttribute(SrcAttributeName, src); } } @@ -135,4 +138,41 @@ private void EnsureFileVersionProvider() FileVersionProvider = ViewContext.HttpContext.RequestServices.GetRequiredService(); } } + + private string GetVersionedResourceUrl(string value) + { + if (AppendVersion == true) + { + var assetCollection = GetAssetCollection(); + var pathBase = ViewContext.HttpContext.Request.PathBase; + if (assetCollection != null) + { + var src = assetCollection[value]; + if (!string.Equals(src, value, StringComparison.Ordinal)) + { + return src; + } + if (pathBase.HasValue && value.StartsWith(pathBase, StringComparison.OrdinalIgnoreCase)) + { + var relativePath = value[pathBase.Value.Length..]; + src = assetCollection[relativePath]; + if (!string.Equals(src, relativePath, StringComparison.Ordinal)) + { + return src; + } + } + } + + EnsureFileVersionProvider(); + + value = FileVersionProvider.AddFileVersionToPath(pathBase, value); + } + + return value; + } + + private ResourceAssetCollection GetAssetCollection() + { + return ViewContext.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); + } } diff --git a/src/Mvc/Mvc.TagHelpers/src/LinkTagHelper.cs b/src/Mvc/Mvc.TagHelpers/src/LinkTagHelper.cs index f7950deae6d6..51ac412d37d9 100644 --- a/src/Mvc/Mvc.TagHelpers/src/LinkTagHelper.cs +++ b/src/Mvc/Mvc.TagHelpers/src/LinkTagHelper.cs @@ -4,8 +4,10 @@ using System.Diagnostics; using System.Globalization; using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Razor.Infrastructure; using Microsoft.AspNetCore.Mvc.Razor.TagHelpers; using Microsoft.AspNetCore.Mvc.Routing; @@ -276,11 +278,12 @@ public override void Process(TagHelperContext context, TagHelperOutput output) if (Href != null) { + var href = GetVersionedResourceUrl(Href); var index = output.Attributes.IndexOfName(HrefAttributeName); var existingAttribute = output.Attributes[index]; output.Attributes[index] = new TagHelperAttribute( existingAttribute.Name, - FileVersionProvider.AddFileVersionToPath(ViewContext.HttpContext.Request.PathBase, Href), + href, existingAttribute.ValueStyle); } } @@ -452,7 +455,7 @@ private void AppendFallbackHrefs(TagHelperContent builder, IReadOnlyList var valueToWrite = fallbackHrefs[i]; if (AppendVersion == true) { - valueToWrite = FileVersionProvider.AddFileVersionToPath(ViewContext.HttpContext.Request.PathBase, fallbackHrefs[i]); + valueToWrite = GetVersionedResourceUrl(fallbackHrefs[i]); } // Must HTML-encode the href attribute value to ensure the written element is valid. Must also @@ -520,11 +523,7 @@ private void BuildLinkTag(string href, TagHelperAttributeList attributes, TagHel private void AppendVersionedHref(string hrefName, string hrefValue, TagHelperContent builder) { - if (AppendVersion == true) - { - hrefValue = FileVersionProvider.AddFileVersionToPath(ViewContext.HttpContext.Request.PathBase, hrefValue); - } - + hrefValue = GetVersionedResourceUrl(hrefValue); builder .AppendHtml(hrefName) .AppendHtml("=\"") @@ -532,6 +531,41 @@ private void AppendVersionedHref(string hrefName, string hrefValue, TagHelperCon .AppendHtml("\" "); } + private string GetVersionedResourceUrl(string value) + { + if (AppendVersion == true) + { + var assetCollection = GetAssetCollection(); + var pathBase = ViewContext.HttpContext.Request.PathBase; + if (assetCollection != null) + { + var src = assetCollection[value]; + if (!string.Equals(src, value, StringComparison.Ordinal)) + { + return src; + } + if (pathBase.HasValue && value.StartsWith(pathBase, StringComparison.OrdinalIgnoreCase)) + { + var relativePath = value[pathBase.Value.Length..]; + src = assetCollection[relativePath]; + if (!string.Equals(src, relativePath, StringComparison.Ordinal)) + { + return src; + } + } + } + + value = FileVersionProvider.AddFileVersionToPath(pathBase, value); + } + + return value; + } + + private ResourceAssetCollection GetAssetCollection() + { + return ViewContext.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); + } + private enum Mode { /// diff --git a/src/Mvc/Mvc.TagHelpers/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.TagHelpers/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..1c4ffad9ba5e 100644 --- a/src/Mvc/Mvc.TagHelpers/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.TagHelpers/src/PublicAPI.Unshipped.txt @@ -1 +1,5 @@ #nullable enable +~Microsoft.AspNetCore.Mvc.TagHelpers.ScriptTagHelper.ImportMap.get -> Microsoft.AspNetCore.Components.ImportMapDefinition +~Microsoft.AspNetCore.Mvc.TagHelpers.ScriptTagHelper.ImportMap.set -> void +~Microsoft.AspNetCore.Mvc.TagHelpers.ScriptTagHelper.Type.get -> string +~Microsoft.AspNetCore.Mvc.TagHelpers.ScriptTagHelper.Type.set -> void diff --git a/src/Mvc/Mvc.TagHelpers/src/ScriptTagHelper.cs b/src/Mvc/Mvc.TagHelpers/src/ScriptTagHelper.cs index 3fdaa8410a31..80402647b81d 100644 --- a/src/Mvc/Mvc.TagHelpers/src/ScriptTagHelper.cs +++ b/src/Mvc/Mvc.TagHelpers/src/ScriptTagHelper.cs @@ -3,8 +3,10 @@ using System.Diagnostics; using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Razor.Infrastructure; using Microsoft.AspNetCore.Mvc.Razor.TagHelpers; using Microsoft.AspNetCore.Mvc.Routing; @@ -28,6 +30,8 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers; [HtmlTargetElement("script", Attributes = FallbackSrcExcludeAttributeName)] [HtmlTargetElement("script", Attributes = FallbackTestExpressionAttributeName)] [HtmlTargetElement("script", Attributes = AppendVersionAttributeName)] +[HtmlTargetElement("script", Attributes = TypeAttributeName)] +[HtmlTargetElement("script", Attributes = ImportMapAttributeName)] public class ScriptTagHelper : UrlResolutionTagHelper { private const string SrcIncludeAttributeName = "asp-src-include"; @@ -40,6 +44,9 @@ public class ScriptTagHelper : UrlResolutionTagHelper private const string SrcAttributeName = "src"; private const string IntegrityAttributeName = "integrity"; private const string AppendVersionAttributeName = "asp-append-version"; + private const string TypeAttributeName = "type"; + private const string ImportMapAttributeName = "asp-importmap"; + private static readonly Func Compare = (a, b) => a - b; private StringWriter _stringWriter; @@ -115,6 +122,12 @@ public ScriptTagHelper( [HtmlAttributeName(SrcAttributeName)] public string Src { get; set; } + /// + /// Type of the script. + /// + [HtmlAttributeName(TypeAttributeName)] + public string Type { get; set; } + /// /// A comma separated list of globbed file patterns of JavaScript scripts to load. /// The glob patterns are assessed relative to the application's 'webroot' setting. @@ -174,6 +187,16 @@ public ScriptTagHelper( [HtmlAttributeName(FallbackTestExpressionAttributeName)] public string FallbackTestExpression { get; set; } + /// + /// The to use for the document. + /// + /// + /// If this is not set and the type value is "importmap", + /// the import map will be retrieved by default from the current . + /// + [HtmlAttributeName(ImportMapAttributeName)] + public ImportMapDefinition ImportMap { get; set; } + /// /// Gets the for the application. /// @@ -217,6 +240,25 @@ public override void Process(TagHelperContext context, TagHelperOutput output) ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(output); + if (string.Equals(Type, "importmap", StringComparison.OrdinalIgnoreCase)) + { + // This is an importmap script, we'll write out the import map and + // stop processing. + var importMap = ImportMap ?? ViewContext.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); + if (importMap == null) + { + // No importmap found, nothing to do. + output.SuppressOutput(); + return; + } + + output.TagName = "script"; + output.TagMode = TagMode.StartTagAndEndTag; + output.Attributes.SetAttribute("type", "importmap"); + output.Content.SetHtmlContent(importMap.ToString()); + return; + } + // Pass through attribute that is also a well-known HTML attribute. if (Src != null) { @@ -240,14 +282,14 @@ public override void Process(TagHelperContext context, TagHelperOutput output) if (AppendVersion == true) { EnsureFileVersionProvider(); - + var versionedSrc = GetVersionedSrc(Src); if (Src != null) { var index = output.Attributes.IndexOfName(SrcAttributeName); var existingAttribute = output.Attributes[index]; output.Attributes[index] = new TagHelperAttribute( existingAttribute.Name, - FileVersionProvider.AddFileVersionToPath(ViewContext.HttpContext.Request.PathBase, Src), + versionedSrc, existingAttribute.ValueStyle); } } @@ -366,12 +408,37 @@ private string GetVersionedSrc(string srcValue) { if (AppendVersion == true) { - srcValue = FileVersionProvider.AddFileVersionToPath(ViewContext.HttpContext.Request.PathBase, srcValue); + var assetCollection = GetAssetCollection(); + var pathBase = ViewContext.HttpContext.Request.PathBase; + if (assetCollection != null) + { + var src = assetCollection[srcValue]; + if (!string.Equals(src, srcValue, StringComparison.Ordinal)) + { + return src; + } + if (pathBase.HasValue && srcValue.StartsWith(pathBase, StringComparison.OrdinalIgnoreCase)) + { + var relativePath = srcValue[pathBase.Value.Length..]; + src = assetCollection[relativePath]; + if (!string.Equals(src, relativePath, StringComparison.Ordinal)) + { + return src; + } + } + } + + srcValue = FileVersionProvider.AddFileVersionToPath(pathBase, srcValue); } return srcValue; } + private ResourceAssetCollection GetAssetCollection() + { + return ViewContext.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); + } + private void AppendVersionedSrc( string srcName, string srcValue, diff --git a/src/Mvc/Mvc.ViewFeatures/src/Builder/ControllerActionEndpointConventionBuilderResourceCollectionExtensions.cs b/src/Mvc/Mvc.ViewFeatures/src/Builder/ControllerActionEndpointConventionBuilderResourceCollectionExtensions.cs new file mode 100644 index 000000000000..26b5f2d5b8b7 --- /dev/null +++ b/src/Mvc/Mvc.ViewFeatures/src/Builder/ControllerActionEndpointConventionBuilderResourceCollectionExtensions.cs @@ -0,0 +1,55 @@ +// 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.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Endpoints; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; + +namespace Microsoft.AspNetCore.Builder; + +/// +/// Extensions for . +/// +public static class ControllerActionEndpointConventionBuilderResourceCollectionExtensions +{ + /// + /// Adds a metadata instance to the endpoints. + /// + /// The . + /// The manifest associated with the assets. + /// + public static ControllerActionEndpointConventionBuilder WithResourceCollection( + this ControllerActionEndpointConventionBuilder builder, + string manifestPath = null) + { + ArgumentNullException.ThrowIfNull(builder); + + var endpointBuilder = builder.Items["__EndpointRouteBuilder"]; + var (resolver, registered) = builder.Items.TryGetValue("__ResourceCollectionResolver", out var value) + ? ((ResourceCollectionResolver)value, true) + : (new ResourceCollectionResolver((IEndpointRouteBuilder)endpointBuilder), false); + + resolver.ManifestName = manifestPath; + if (!registered) + { + var collection = resolver.ResolveResourceCollection(); + var importMap = resolver.ResolveImportMap(); + + builder.Add(endpointBuilder => + { + // Do not add metadata to API controllers + if (endpointBuilder.Metadata.OfType().Any()) + { + return; + } + + endpointBuilder.Metadata.Add(collection); + endpointBuilder.Metadata.Add(importMap); + }); + } + + return builder; + } +} diff --git a/src/Mvc/Mvc.ViewFeatures/src/Microsoft.AspNetCore.Mvc.ViewFeatures.csproj b/src/Mvc/Mvc.ViewFeatures/src/Microsoft.AspNetCore.Mvc.ViewFeatures.csproj index c6af4f42a00d..24dd476f1022 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/Microsoft.AspNetCore.Mvc.ViewFeatures.csproj +++ b/src/Mvc/Mvc.ViewFeatures/src/Microsoft.AspNetCore.Mvc.ViewFeatures.csproj @@ -15,6 +15,7 @@ true false disable + $(DefineConstants);MVC_VIEWFEATURES @@ -31,6 +32,7 @@ + diff --git a/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..51b71fed986a 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +Microsoft.AspNetCore.Builder.ControllerActionEndpointConventionBuilderResourceCollectionExtensions +~static Microsoft.AspNetCore.Builder.ControllerActionEndpointConventionBuilderResourceCollectionExtensions.WithResourceCollection(this Microsoft.AspNetCore.Builder.ControllerActionEndpointConventionBuilder builder, string manifestPath = null) -> Microsoft.AspNetCore.Builder.ControllerActionEndpointConventionBuilder diff --git a/src/Shared/Components/ResourceCollectionResolver.cs b/src/Shared/Components/ResourceCollectionResolver.cs new file mode 100644 index 000000000000..61c71f0134f8 --- /dev/null +++ b/src/Shared/Components/ResourceCollectionResolver.cs @@ -0,0 +1,135 @@ +// 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.Routing; +using Microsoft.AspNetCore.StaticAssets; +using Microsoft.AspNetCore.StaticAssets.Infrastructure; + +namespace Microsoft.AspNetCore.Components.Endpoints; + +internal class ResourceCollectionResolver(IEndpointRouteBuilder endpoints) +{ +#if !MVC_VIEWFEATURES +#else + private ResourceAssetCollection _resourceCollection; + private ImportMapDefinition _importMapDefinition; + + public string ManifestName { get; set; } +#endif + +#if !MVC_VIEWFEATURES + public ResourceAssetCollection ResolveResourceCollection(string? manifestName = null) + { + var descriptors = StaticAssetsEndpointDataSourceHelper.ResolveStaticAssetDescriptors(endpoints, manifestName); +#else + public ResourceAssetCollection ResolveResourceCollection() + { + if (_resourceCollection != null) + { + return _resourceCollection; + } + + var descriptors = StaticAssetsEndpointDataSourceHelper.ResolveStaticAssetDescriptors(endpoints, ManifestName); +#endif + var resources = new List(); + + // We are converting a subset of the descriptors to resources and including a subset of the properties exposed by the + // descriptors that are useful for the resources in the context of Blazor. Specifically, we pass in the `label` property + // which contains the human-readable identifier for fingerprinted assets, and the integrity, which can be used to apply + // subresource integrity to things like images, script tags, etc. + foreach (var descriptor in descriptors) + { +#if !MVC_VIEWFEATURES + string? label = null; + string? integrity = null; +#else + string label = null; + string integrity = null; +#endif + + if (descriptor.Selectors.Count > 1) + { + // If there's a selector this means that this is an alternative representation for a resource, so skip it. + continue; + } + + var foundProperties = 0; + for (var i = 0; i < descriptor.Properties.Count; i++) + { + var property = descriptor.Properties[i]; + if (property.Name.Equals("label", StringComparison.OrdinalIgnoreCase)) + { + label = property.Value; + foundProperties++; + } + + else if (property.Name.Equals("integrity", StringComparison.OrdinalIgnoreCase)) + { + integrity = property.Value; + foundProperties++; + } + } + + AddResource(resources, descriptor, label, integrity, foundProperties); + } + + // Sort the resources because we are going to generate a hash for the collection to use when we expose it as an endpoint + // for webassembly to consume. This way, we can cache this collection forever until it changes. + resources.Sort((a, b) => string.Compare(a.Url, b.Url, StringComparison.Ordinal)); + + var result = new ResourceAssetCollection(resources); +#if MVC_VIEWFEATURES + _resourceCollection = result; +#endif + return result; + } + +#if !MVC_VIEWFEATURES + public bool IsRegistered(string? manifestName = null) +#else + public bool IsRegistered(string manifestName = null) +#endif + { + return StaticAssetsEndpointDataSourceHelper.HasStaticAssetsDataSource(endpoints, manifestName); + } + + private static void AddResource( + List resources, + StaticAssetDescriptor descriptor, +#if !MVC_VIEWFEATURES + string? label, + string? integrity, +#else + string label, + string integrity, +#endif + int foundProperties) + { + if (label != null || integrity != null) + { + var properties = new ResourceAssetProperty[foundProperties]; + var index = 0; + if (label != null) + { + properties[index++] = new ResourceAssetProperty("label", label); + } + if (integrity != null) + { + properties[index++] = new ResourceAssetProperty("integrity", integrity); + } + + resources.Add(new ResourceAsset(descriptor.Route, properties)); + } + else + { + resources.Add(new ResourceAsset(descriptor.Route, null)); + } + } + +#if MVC_VIEWFEATURES + internal ImportMapDefinition ResolveImportMap() + { + return _importMapDefinition ??= ImportMapDefinition.FromResourceCollection(_resourceCollection); + } +#endif +} diff --git a/src/Shared/WebEncoders/WebEncoders.cs b/src/Shared/WebEncoders/WebEncoders.cs index 4dc85bb8a227..5d31b0789ebb 100644 --- a/src/Shared/WebEncoders/WebEncoders.cs +++ b/src/Shared/WebEncoders/WebEncoders.cs @@ -339,7 +339,17 @@ public static string Base64UrlEncode(ReadOnlySpan input) return base64Url; } +#if NET9_0_OR_GREATER + /// + /// Encodes using base64url encoding. + /// + /// The binary input to encode. + /// The buffer to place the result in. + /// + public static int Base64UrlEncode(ReadOnlySpan input, Span output) +#else private static int Base64UrlEncode(ReadOnlySpan input, Span output) +#endif { Debug.Assert(output.Length >= GetArraySizeRequiredToEncode(input.Length)); From 74bc1b241a9b839a4e2e39194faee9f358da3a6a Mon Sep 17 00:00:00 2001 From: jacalvar Date: Tue, 4 Jun 2024 22:22:33 +0200 Subject: [PATCH 04/30] Template changes --- .../BlazorWeb-CSharp/BlazorWeb-CSharp/Components/App.razor | 7 ++++--- .../RazorPagesWeb-CSharp/Pages/Shared/_Layout.cshtml | 3 ++- .../content/RazorPagesWeb-CSharp/Program.Main.cs | 3 ++- .../content/RazorPagesWeb-CSharp/Program.cs | 3 ++- .../content/StarterWeb-CSharp/Program.Main.cs | 3 ++- .../content/StarterWeb-CSharp/Program.cs | 3 ++- .../content/StarterWeb-CSharp/Views/Shared/_Layout.cshtml | 1 + 7 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/App.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/App.razor index 0a2cdaa5391c..caa3e2244442 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/App.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/App.razor @@ -6,10 +6,11 @@ @*#if (SampleContent) - + ##endif*@ - - + + + @*#if (SampleContent) ##endif*@ diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Shared/_Layout.cshtml b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Shared/_Layout.cshtml index 0ad799723ae6..8fc776a1fd58 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Shared/_Layout.cshtml +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Shared/_Layout.cshtml @@ -4,6 +4,7 @@ @ViewData["Title"] - Company.WebApplication1 + @@ -53,4 +54,4 @@ @await RenderSectionAsync("Scripts", required: false) - \ No newline at end of file + diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Program.Main.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Program.Main.cs index 3d1056838dc8..1bc1b1932ad2 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Program.Main.cs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Program.Main.cs @@ -142,7 +142,8 @@ public static void Main(string[] args) app.UseAuthorization(); app.MapStaticAssets(); - app.MapRazorPages(); + app.MapRazorPages() + .WithResourceCollection(); #if (IndividualB2CAuth || OrganizationalAuth) app.MapControllers(); #endif diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Program.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Program.cs index 289247647714..d8aeddb20f05 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Program.cs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Program.cs @@ -136,7 +136,8 @@ app.UseAuthorization(); app.MapStaticAssets(); -app.MapRazorPages(); +app.MapRazorPages() + .WithResourceCollection(); #if (IndividualB2CAuth || OrganizationalAuth) app.MapControllers(); #endif diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Program.Main.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Program.Main.cs index b386e6f6840c..1910bf839515 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Program.Main.cs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Program.Main.cs @@ -147,7 +147,8 @@ public static void Main(string[] args) name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); #if (OrganizationalAuth || IndividualAuth) - app.MapRazorPages(); + app.MapRazorPages() + .WithResourceCollection(); #endif app.Run(); diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Program.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Program.cs index f8425822e284..22b2af88de88 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Program.cs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Program.cs @@ -142,7 +142,8 @@ name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); #if (OrganizationalAuth || IndividualAuth) -app.MapRazorPages(); +app.MapRazorPages() + .WithResourceCollection(); #endif app.Run(); diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Shared/_Layout.cshtml b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Shared/_Layout.cshtml index 553992a8978c..279f53d40fa5 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Shared/_Layout.cshtml +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Shared/_Layout.cshtml @@ -4,6 +4,7 @@ @ViewData["Title"] - Company.WebApplication1 + From 3884726560f4afcda8a2f76a2db401bd88dccf2d Mon Sep 17 00:00:00 2001 From: jacalvar Date: Wed, 5 Jun 2024 00:14:44 +0200 Subject: [PATCH 05/30] Missing changes --- .../src/Builder/WebAssemblyComponentsEndpointOptions.cs | 2 -- .../src/Builder/ControllerEndpointRouteBuilderExtensions.cs | 4 ++++ .../Web.ProjectTemplates/content/StarterWeb-CSharp/Program.cs | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Components/WebAssembly/Server/src/Builder/WebAssemblyComponentsEndpointOptions.cs b/src/Components/WebAssembly/Server/src/Builder/WebAssemblyComponentsEndpointOptions.cs index a3da958d6ff0..86977c2ca133 100644 --- a/src/Components/WebAssembly/Server/src/Builder/WebAssemblyComponentsEndpointOptions.cs +++ b/src/Components/WebAssembly/Server/src/Builder/WebAssemblyComponentsEndpointOptions.cs @@ -31,6 +31,4 @@ public sealed class WebAssemblyComponentsEndpointOptions /// Gets or sets the that determines the static assets manifest path mapped to this app. /// public string? StaticAssetsManifestPath { get; set; } - - internal bool ConventionsApplied { get; set; } } diff --git a/src/Mvc/Mvc.Core/src/Builder/ControllerEndpointRouteBuilderExtensions.cs b/src/Mvc/Mvc.Core/src/Builder/ControllerEndpointRouteBuilderExtensions.cs index f134890f4389..bc4ead832009 100644 --- a/src/Mvc/Mvc.Core/src/Builder/ControllerEndpointRouteBuilderExtensions.cs +++ b/src/Mvc/Mvc.Core/src/Builder/ControllerEndpointRouteBuilderExtensions.cs @@ -48,6 +48,8 @@ public static ControllerActionEndpointConventionBuilder MapDefaultControllerRout EnsureControllerServices(endpoints); var dataSource = GetOrCreateDataSource(endpoints); + dataSource.DefaultBuilder.Items["__EndpointRouteBuilder"] = endpoints; + return dataSource.AddRoute( "default", "{controller=Home}/{action=Index}/{id?}", @@ -92,6 +94,8 @@ public static ControllerActionEndpointConventionBuilder MapControllerRoute( EnsureControllerServices(endpoints); var dataSource = GetOrCreateDataSource(endpoints); + dataSource.DefaultBuilder.Items["__EndpointRouteBuilder"] = endpoints; + return dataSource.AddRoute( name, pattern, diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Program.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Program.cs index 22b2af88de88..360af5f6cb81 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Program.cs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Program.cs @@ -140,7 +140,9 @@ app.MapControllerRoute( name: "default", - pattern: "{controller=Home}/{action=Index}/{id?}"); + pattern: "{controller=Home}/{action=Index}/{id?}") + .WithResourceCollection(); + #if (OrganizationalAuth || IndividualAuth) app.MapRazorPages() .WithResourceCollection(); From 8bd95a623d91cbc047d54b885e68cf5547839acd Mon Sep 17 00:00:00 2001 From: jacalvar Date: Wed, 5 Jun 2024 21:34:14 +0200 Subject: [PATCH 06/30] Hot reload and dumb test fixes --- .../RazorComponentEndpointDataSource.cs | 2 +- .../src/Rendering/EndpointHtmlRenderer.cs | 7 +- .../Server/test/Circuits/ComponentHubTest.cs | 8 ++ .../Shared/src/ResourceCollectionProvider.cs | 6 +- .../TagHelpers/UrlResolutionTagHelperTest.cs | 16 +++- src/Mvc/Mvc.TagHelpers/src/ScriptTagHelper.cs | 5 +- .../StaticAssetDevelopmentRuntimeHandler.cs | 73 ++++++++++++++++++- 7 files changed, 106 insertions(+), 11 deletions(-) diff --git a/src/Components/Endpoints/src/Builder/RazorComponentEndpointDataSource.cs b/src/Components/Endpoints/src/Builder/RazorComponentEndpointDataSource.cs index 807c69ad4f68..7f8089446d80 100644 --- a/src/Components/Endpoints/src/Builder/RazorComponentEndpointDataSource.cs +++ b/src/Components/Endpoints/src/Builder/RazorComponentEndpointDataSource.cs @@ -127,7 +127,7 @@ private void UpdateEndpoints() } // Extract the endpoint collection from any of the endpoints - var resourceCollection = endpoints[^1].Metadata.GetMetadata(); + var resourceCollection = endpoints.Count > 0 ? endpoints[^1].Metadata.GetMetadata() : null; ICollection renderModes = Options.ConfiguredRenderModes; foreach (var renderMode in renderModes) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs index b13e4fdb0965..e66ca88536a9 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs @@ -129,7 +129,6 @@ internal static async Task InitializeStandardComponentServicesAsync( private static void InitializeResourceCollection(HttpContext httpContext) { - var resourceCollectionProvider = httpContext.RequestServices.GetRequiredService(); var endpoint = httpContext.GetEndpoint(); var resourceCollection = GetResourceCollection(httpContext); @@ -137,12 +136,12 @@ private static void InitializeResourceCollection(HttpContext httpContext) endpoint.Metadata.GetMetadata() : null; - if (resourceCollectionUrl != null) + var resourceCollectionProvider = resourceCollectionUrl != null ? httpContext.RequestServices.GetService() : null; + if (resourceCollectionUrl != null && resourceCollectionProvider != null) { resourceCollectionProvider.SetResourceCollectionUrl(resourceCollectionUrl.Url); + resourceCollectionProvider.SetResourceCollection(resourceCollection ?? ResourceAssetCollection.Empty); } - - resourceCollectionProvider.SetResourceCollection(resourceCollection ?? ResourceAssetCollection.Empty); } protected override ComponentState CreateComponentState(int componentId, IComponent component, ComponentState? parentComponentState) diff --git a/src/Components/Server/test/Circuits/ComponentHubTest.cs b/src/Components/Server/test/Circuits/ComponentHubTest.cs index 0b658a611e1f..9d98dc1bf7c2 100644 --- a/src/Components/Server/test/Circuits/ComponentHubTest.cs +++ b/src/Components/Server/test/Circuits/ComponentHubTest.cs @@ -6,6 +6,9 @@ using System.Text.RegularExpressions; using Microsoft.AspNetCore.Components.Server.Circuits; using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Connections.Features; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -128,6 +131,11 @@ private static (Mock, ComponentHub) InitializeComponentHub() mockCaller.Setup(x => x.Caller).Returns(mockClientProxy.Object); hub.Clients = mockCaller.Object; var mockContext = new Mock(); + var feature = new FeatureCollection(); + var httpContextFeature = new Mock(); + httpContextFeature.Setup(x => x.HttpContext).Returns(() => new DefaultHttpContext()); + feature.Set(httpContextFeature.Object); + mockContext.Setup(x => x.Features).Returns(feature); mockContext.Setup(x => x.ConnectionId).Returns("123"); hub.Context = mockContext.Object; diff --git a/src/Components/Shared/src/ResourceCollectionProvider.cs b/src/Components/Shared/src/ResourceCollectionProvider.cs index a0c50b8c2ae8..4a3751669c5c 100644 --- a/src/Components/Shared/src/ResourceCollectionProvider.cs +++ b/src/Components/Shared/src/ResourceCollectionProvider.cs @@ -54,7 +54,11 @@ internal void SetResourceCollection(ResourceAssetCollection resourceCollection) private async Task LoadResourceCollection() { - _url ??= "/_framework/resource-collection.js"; + if (_url == null) + { + return ResourceAssetCollection.Empty; + } + var module = await _jsRuntime.InvokeAsync("import", _url); var result = await module.InvokeAsync("get"); return result == null ? ResourceAssetCollection.Empty : new ResourceAssetCollection(result); diff --git a/src/Mvc/Mvc.Razor/test/TagHelpers/UrlResolutionTagHelperTest.cs b/src/Mvc/Mvc.Razor/test/TagHelpers/UrlResolutionTagHelperTest.cs index a6e2a92b5529..71de7b2fcd27 100644 --- a/src/Mvc/Mvc.Razor/test/TagHelpers/UrlResolutionTagHelperTest.cs +++ b/src/Mvc/Mvc.Razor/test/TagHelpers/UrlResolutionTagHelperTest.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.Extensions.WebEncoders.Testing; @@ -85,7 +86,10 @@ public void Process_ResolvesTildeSlashValues(string url, string expectedHref) urlHelperFactory .Setup(f => f.GetUrlHelper(It.IsAny())) .Returns(urlHelperMock.Object); - var tagHelper = new UrlResolutionTagHelper(urlHelperFactory.Object, new HtmlTestEncoder()); + var tagHelper = new UrlResolutionTagHelper(urlHelperFactory.Object, new HtmlTestEncoder()) + { + ViewContext = new Rendering.ViewContext { HttpContext = new DefaultHttpContext() } + }; var context = new TagHelperContext( tagName: "a", @@ -142,7 +146,10 @@ public void Process_ResolvesTildeSlashValues_InHtmlString(string url, string exp urlHelperFactory .Setup(f => f.GetUrlHelper(It.IsAny())) .Returns(urlHelperMock.Object); - var tagHelper = new UrlResolutionTagHelper(urlHelperFactory.Object, new HtmlTestEncoder()); + var tagHelper = new UrlResolutionTagHelper(urlHelperFactory.Object, new HtmlTestEncoder()) + { + ViewContext = new Rendering.ViewContext { HttpContext = new DefaultHttpContext() } + }; var context = new TagHelperContext( tagName: "a", @@ -333,7 +340,10 @@ public void Process_ThrowsWhenEncodingNeededAndIUrlHelperActsUnexpectedly() urlHelperFactory .Setup(f => f.GetUrlHelper(It.IsAny())) .Returns(urlHelperMock.Object); - var tagHelper = new UrlResolutionTagHelper(urlHelperFactory.Object, new HtmlTestEncoder()); + var tagHelper = new UrlResolutionTagHelper(urlHelperFactory.Object, new HtmlTestEncoder()) + { + ViewContext = new Rendering.ViewContext { HttpContext = new DefaultHttpContext() } + }; var context = new TagHelperContext( tagName: "a", diff --git a/src/Mvc/Mvc.TagHelpers/src/ScriptTagHelper.cs b/src/Mvc/Mvc.TagHelpers/src/ScriptTagHelper.cs index 80402647b81d..317aa9b6b4f4 100644 --- a/src/Mvc/Mvc.TagHelpers/src/ScriptTagHelper.cs +++ b/src/Mvc/Mvc.TagHelpers/src/ScriptTagHelper.cs @@ -428,7 +428,10 @@ private string GetVersionedSrc(string srcValue) } } - srcValue = FileVersionProvider.AddFileVersionToPath(pathBase, srcValue); + if (srcValue != null) + { + srcValue = FileVersionProvider.AddFileVersionToPath(pathBase, srcValue); + } } return srcValue; diff --git a/src/StaticAssets/src/Development/StaticAssetDevelopmentRuntimeHandler.cs b/src/StaticAssets/src/Development/StaticAssetDevelopmentRuntimeHandler.cs index 1f7b73d87dec..ee161fd962c0 100644 --- a/src/StaticAssets/src/Development/StaticAssetDevelopmentRuntimeHandler.cs +++ b/src/StaticAssets/src/Development/StaticAssetDevelopmentRuntimeHandler.cs @@ -172,7 +172,22 @@ internal static void EnableSupport( var config = endpoints.ServiceProvider.GetRequiredService(); var hotReloadHandler = new StaticAssetDevelopmentRuntimeHandler(descriptors); builder.Add(hotReloadHandler.AttachRuntimePatching); - var disableFallback = bool.TryParse(config["DisableStaticAssetNotFoundRuntimeFallback"], out var disableFallbackValue) && disableFallbackValue; + var disableFallback = IsEnabled(config, "DisableStaticAssetNotFoundRuntimeFallback"); + + foreach (var descriptor in descriptors) + { + var enableDevelopmentCaching = IsEnabled(config, "EnableStaticAssetsDevelopmentCaching"); + if (!enableDevelopmentCaching) + { + DisableCachingHeaders(descriptor); + } + + var enableDevelopmentIntegrity = IsEnabled(config, "EnableStaticAssetsDevelopmentIntegrity"); + if (!enableDevelopmentIntegrity) + { + RemoveIntegrityProperty(descriptor); + } + } if (!disableFallback) { @@ -227,6 +242,62 @@ internal static void EnableSupport( } } + private static bool IsEnabled(IConfiguration config, string key) + { + return bool.TryParse(config[key], out var value) && value; + } + + private static void DisableCachingHeaders(StaticAssetDescriptor descriptor) + { + if (descriptor.ResponseHeaders.Count == 0) + { + return; + } + + var responseHeaders = new List(descriptor.ResponseHeaders); + var replaced = false; + for (var i = 0; i < descriptor.ResponseHeaders.Count; i++) + { + var responseHeader = descriptor.ResponseHeaders[i]; + if (string.Equals(responseHeader.Name, HeaderNames.CacheControl, StringComparison.OrdinalIgnoreCase)) + { + if (!string.Equals(responseHeader.Value, "no-cache", StringComparison.OrdinalIgnoreCase)) + { + responseHeaders.RemoveAt(i); + responseHeaders.Insert(i, new StaticAssetResponseHeader(HeaderNames.CacheControl, "no-cache")); + replaced = true; + } + } + } + + if (replaced) + { + descriptor.ResponseHeaders = responseHeaders; + } + } + + private static void RemoveIntegrityProperty(StaticAssetDescriptor descriptor) + { + if (descriptor.Properties.Count == 0) + { + return; + } + var propertiesList = new List(descriptor.Properties); + for (var i = 0; i < descriptor.Properties.Count; i++) + { + var property = descriptor.Properties[i]; + if (string.Equals(property.Name, "integrity", StringComparison.OrdinalIgnoreCase)) + { + propertiesList.RemoveAt(i); + } + } + + if (propertiesList.Count < descriptor.Properties.Count) + { + descriptor.Properties = propertiesList; + } + } + private static partial class Log { private const string StaticAssetNotFoundInManifestMessage = """The static asset '{Path}' was not found in the built time manifest. This file will not be available at runtime if it is not available at compile time during the publish process. If the file was not added to the project during development, and is created at runtime, use the StaticFiles middleware to serve it instead."""; From f0c433ae05cdd88c35b721d061c0332beb1fd6dc Mon Sep 17 00:00:00 2001 From: jacalvar Date: Wed, 5 Jun 2024 22:53:13 +0200 Subject: [PATCH 07/30] Fix tests --- .../ServerFixtures/WebHostServerFixture.cs | 14 ++++++++++---- .../RemoteAuthenticationStartup.cs | 4 ++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/Components/test/E2ETest/Infrastructure/ServerFixtures/WebHostServerFixture.cs b/src/Components/test/E2ETest/Infrastructure/ServerFixtures/WebHostServerFixture.cs index 1b4ce4d6231f..6c8d84524b6f 100644 --- a/src/Components/test/E2ETest/Infrastructure/ServerFixtures/WebHostServerFixture.cs +++ b/src/Components/test/E2ETest/Infrastructure/ServerFixtures/WebHostServerFixture.cs @@ -36,9 +36,15 @@ public override void Dispose() private async ValueTask DisposeCore() { - // This can be null if creating the webhost throws, we don't want to throw here and hide - // the original exception. - Host?.Dispose(); - await Host?.StopAsync(); + try + { + // This can be null if creating the webhost throws, we don't want to throw here and hide + // the original exception. + await Host?.StopAsync(); + Host?.Dispose(); + } + catch + { + } } } diff --git a/src/Components/test/testassets/Components.TestServer/RemoteAuthenticationStartup.cs b/src/Components/test/testassets/Components.TestServer/RemoteAuthenticationStartup.cs index d2e2faa858e7..9c9641242521 100644 --- a/src/Components/test/testassets/Components.TestServer/RemoteAuthenticationStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/RemoteAuthenticationStartup.cs @@ -28,7 +28,11 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseAntiforgery(); app.UseEndpoints(endpoints => { +#if !DEBUG endpoints.MapStaticAssets(Path.Combine("trimmed-or-threading", "Components.TestServer", "Components.TestServer.staticwebassets.endpoints.json")); +#else + endpoints.MapStaticAssets("Components.TestServer.staticwebassets.endpoints.json"); +#endif endpoints.MapRazorComponents() .AddAdditionalAssemblies(Assembly.Load("Components.WasmRemoteAuthentication")) .AddInteractiveWebAssemblyRenderMode(options => options.PathPrefix = "/WasmRemoteAuthentication"); From 268e17c13dd730599e6ecab811d9ad63962c6116 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Mon, 10 Jun 2024 16:25:16 +0200 Subject: [PATCH 08/30] Revisit and remove --- .../Samples/BlazorUnitedApp/App.razor | 8 +++--- .../BlazorUnitedApp/BlazorUnitedApp.csproj | 23 +++++++++++++++++ .../BlazorUnitedApp/BlazorUnitedApp.sln | 25 +++++++++++++++++++ .../Properties/launchSettings.json | 2 +- .../appsettings.Development.json | 1 + .../StaticAssetDevelopmentRuntimeHandler.cs | 2 +- 6 files changed, 55 insertions(+), 6 deletions(-) create mode 100644 src/Components/Samples/BlazorUnitedApp/BlazorUnitedApp.sln diff --git a/src/Components/Samples/BlazorUnitedApp/App.razor b/src/Components/Samples/BlazorUnitedApp/App.razor index 62f51830ad4b..073400339c01 100644 --- a/src/Components/Samples/BlazorUnitedApp/App.razor +++ b/src/Components/Samples/BlazorUnitedApp/App.razor @@ -4,9 +4,9 @@ - - - + + + @@ -15,6 +15,6 @@ - + diff --git a/src/Components/Samples/BlazorUnitedApp/BlazorUnitedApp.csproj b/src/Components/Samples/BlazorUnitedApp/BlazorUnitedApp.csproj index 62460261002b..57add88b29ff 100644 --- a/src/Components/Samples/BlazorUnitedApp/BlazorUnitedApp.csproj +++ b/src/Components/Samples/BlazorUnitedApp/BlazorUnitedApp.csproj @@ -21,4 +21,27 @@ + + + + + + + <_FixedAssets> + $([System.String]::Copy('%(_FixedAssets.TargetPath)').Replace('%(_FixedAssets.BasePath)', '')) + + + + + + + + diff --git a/src/Components/Samples/BlazorUnitedApp/BlazorUnitedApp.sln b/src/Components/Samples/BlazorUnitedApp/BlazorUnitedApp.sln new file mode 100644 index 000000000000..82bcdb4b0685 --- /dev/null +++ b/src/Components/Samples/BlazorUnitedApp/BlazorUnitedApp.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorUnitedApp", "BlazorUnitedApp.csproj", "{3D806AD9-86E3-4253-B8C5-36B29E88F625}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3D806AD9-86E3-4253-B8C5-36B29E88F625}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3D806AD9-86E3-4253-B8C5-36B29E88F625}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3D806AD9-86E3-4253-B8C5-36B29E88F625}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3D806AD9-86E3-4253-B8C5-36B29E88F625}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {41A71825-ABB1-44F6-B009-1DA36A9E19F5} + EndGlobalSection +EndGlobal diff --git a/src/Components/Samples/BlazorUnitedApp/Properties/launchSettings.json b/src/Components/Samples/BlazorUnitedApp/Properties/launchSettings.json index fc639e6af0ed..265de0cd64ad 100644 --- a/src/Components/Samples/BlazorUnitedApp/Properties/launchSettings.json +++ b/src/Components/Samples/BlazorUnitedApp/Properties/launchSettings.json @@ -23,7 +23,7 @@ "dotnetRunMessages": true, "launchBrowser": true, "applicationUrl": "https://localhost:7247;http://localhost:5265", - "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + //"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/src/Components/Samples/BlazorUnitedApp/appsettings.Development.json b/src/Components/Samples/BlazorUnitedApp/appsettings.Development.json index 770d3e93146b..222e6b7e6b60 100644 --- a/src/Components/Samples/BlazorUnitedApp/appsettings.Development.json +++ b/src/Components/Samples/BlazorUnitedApp/appsettings.Development.json @@ -1,4 +1,5 @@ { + "EnableStaticAssetsDevelopmentCaching": true, "DetailedErrors": true, "Logging": { "LogLevel": { diff --git a/src/StaticAssets/src/Development/StaticAssetDevelopmentRuntimeHandler.cs b/src/StaticAssets/src/Development/StaticAssetDevelopmentRuntimeHandler.cs index ee161fd962c0..c57917d0edb4 100644 --- a/src/StaticAssets/src/Development/StaticAssetDevelopmentRuntimeHandler.cs +++ b/src/StaticAssets/src/Development/StaticAssetDevelopmentRuntimeHandler.cs @@ -139,7 +139,7 @@ public Task SendFileAsync(string path, long offset, long? count, CancellationTok public Task StartAsync(CancellationToken cancellationToken = default) { - throw new NotImplementedException(); + return _original.StartAsync(cancellationToken); } } From 356c493d92de9cd9a7c6442b71e82b4dd83d3b4f Mon Sep 17 00:00:00 2001 From: jacalvar Date: Tue, 11 Jun 2024 19:59:40 +0200 Subject: [PATCH 09/30] tmp --- src/Components/Components/src/ResourceAsset.cs | 4 ++-- src/Components/Components/src/ResourceAssetCollection.cs | 2 +- src/Components/Components/src/ResourceAssetProperty.cs | 4 ++-- src/Components/Endpoints/src/Assets/ImportMap.cs | 2 +- src/Components/Endpoints/src/Assets/ImportMapDefinition.cs | 2 +- src/Components/Samples/BlazorUnitedApp/App.razor | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Components/Components/src/ResourceAsset.cs b/src/Components/Components/src/ResourceAsset.cs index b42a7dd80ef7..149c091e70f9 100644 --- a/src/Components/Components/src/ResourceAsset.cs +++ b/src/Components/Components/src/ResourceAsset.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Microsoft.AspNetCore.Components; @@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Components; /// /// The URL of the resource. /// The properties associated to this resource. -public class ResourceAsset(string url, IReadOnlyList? properties) +public sealed class ResourceAsset(string url, IReadOnlyList? properties) { /// /// Gets the URL that identifies this resource. diff --git a/src/Components/Components/src/ResourceAssetCollection.cs b/src/Components/Components/src/ResourceAssetCollection.cs index dea6ffd56a86..a84ab8c96843 100644 --- a/src/Components/Components/src/ResourceAssetCollection.cs +++ b/src/Components/Components/src/ResourceAssetCollection.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Components; /// /// Describes a mapping of static assets to their corresponding unique URLs. /// -public class ResourceAssetCollection : IReadOnlyList +public sealed class ResourceAssetCollection : IReadOnlyList { /// /// An empty . diff --git a/src/Components/Components/src/ResourceAssetProperty.cs b/src/Components/Components/src/ResourceAssetProperty.cs index 1ec70700fcfd..03544a4e132e 100644 --- a/src/Components/Components/src/ResourceAssetProperty.cs +++ b/src/Components/Components/src/ResourceAssetProperty.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Microsoft.AspNetCore.Components; @@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Components; /// /// The name of the property. /// The value of the property. -public class ResourceAssetProperty(string name, string value) +public sealed class ResourceAssetProperty(string name, string value) { /// /// Gets the name of the property. diff --git a/src/Components/Endpoints/src/Assets/ImportMap.cs b/src/Components/Endpoints/src/Assets/ImportMap.cs index 8cc73bfb4880..6fb693569e24 100644 --- a/src/Components/Endpoints/src/Assets/ImportMap.cs +++ b/src/Components/Endpoints/src/Assets/ImportMap.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Components; /// Represents an element that defines the import map for module scripts /// in the application. /// -public class ImportMap : IComponent +public sealed class ImportMap : IComponent { private RenderHandle _renderHandle; private bool _firstRender = true; diff --git a/src/Components/Endpoints/src/Assets/ImportMapDefinition.cs b/src/Components/Endpoints/src/Assets/ImportMapDefinition.cs index 276264001469..cf2adf81b608 100644 --- a/src/Components/Endpoints/src/Assets/ImportMapDefinition.cs +++ b/src/Components/Endpoints/src/Assets/ImportMapDefinition.cs @@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Components; /// instances are expensive to create, so it is recommended to cache them if /// you are creating an additional instance. /// -public class ImportMapDefinition +public sealed class ImportMapDefinition { private Dictionary? _imports; private Dictionary>? _scopes; diff --git a/src/Components/Samples/BlazorUnitedApp/App.razor b/src/Components/Samples/BlazorUnitedApp/App.razor index 073400339c01..087662f9526f 100644 --- a/src/Components/Samples/BlazorUnitedApp/App.razor +++ b/src/Components/Samples/BlazorUnitedApp/App.razor @@ -15,6 +15,6 @@ - + From b8375056900ef1a19dc1d97cd7fa5bf236370c6e Mon Sep 17 00:00:00 2001 From: jacalvar Date: Wed, 12 Jun 2024 21:34:56 +0200 Subject: [PATCH 10/30] Tests --- .../src/TagHelpers/UrlResolutionTagHelper.cs | 31 ++++---- .../TagHelpers/UrlResolutionTagHelperTest.cs | 75 +++++++++++++++++++ src/Mvc/Mvc.TagHelpers/src/ScriptTagHelper.cs | 7 +- .../test/ScriptTagHelperTest.cs | 43 ++++++++++- .../MvcSandbox/Views/Shared/_Layout.cshtml | 2 + 5 files changed, 139 insertions(+), 19 deletions(-) diff --git a/src/Mvc/Mvc.Razor/src/TagHelpers/UrlResolutionTagHelper.cs b/src/Mvc/Mvc.Razor/src/TagHelpers/UrlResolutionTagHelper.cs index 12af3d549e5c..98a5f90de666 100644 --- a/src/Mvc/Mvc.Razor/src/TagHelpers/UrlResolutionTagHelper.cs +++ b/src/Mvc/Mvc.Razor/src/TagHelpers/UrlResolutionTagHelper.cs @@ -310,24 +310,29 @@ private string GetVersionedResourceUrl(string value) var assetCollection = GetAssetCollection(); if (assetCollection != null) { - var src = assetCollection[value]; - if (!string.Equals(src, value, StringComparison.Ordinal)) - { - return src; - } - var pathBase = ViewContext.HttpContext.Request.PathBase; - if (pathBase.HasValue && value.StartsWith(pathBase, StringComparison.OrdinalIgnoreCase)) + var (key, remainder) = ExtractKeyAndRest(value); + + var src = assetCollection[key]; + if (!string.Equals(src, key, StringComparison.Ordinal)) { - var relativePath = value[pathBase.Value.Length..]; - src = assetCollection[relativePath]; - if (!string.Equals(src, relativePath, StringComparison.Ordinal)) - { - return src; - } + return $"~/{src}{value[remainder..]}"; } } return value; + + static (string key, int rest) ExtractKeyAndRest(string value) + { + var lastNonWhitespaceChar = value.AsSpan().TrimEnd().LastIndexOfAnyExcept(ValidAttributeWhitespaceChars); + var keyEnd = lastNonWhitespaceChar > -1 ? lastNonWhitespaceChar + 1 : value.Length; + var key = value.AsSpan(); + if (key.StartsWith("~/", StringComparison.Ordinal)) + { + key = value.AsSpan()[2..keyEnd].Trim(); + } + + return (key.ToString(), keyEnd); + } } private ResourceAssetCollection? GetAssetCollection() diff --git a/src/Mvc/Mvc.Razor/test/TagHelpers/UrlResolutionTagHelperTest.cs b/src/Mvc/Mvc.Razor/test/TagHelpers/UrlResolutionTagHelperTest.cs index 71de7b2fcd27..94c2fe3bfdc0 100644 --- a/src/Mvc/Mvc.Razor/test/TagHelpers/UrlResolutionTagHelperTest.cs +++ b/src/Mvc/Mvc.Razor/test/TagHelpers/UrlResolutionTagHelperTest.cs @@ -1,6 +1,7 @@ // 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.Components; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Routing; @@ -37,6 +38,29 @@ public static TheoryData ResolvableUrlData } } + public static TheoryData ResolvableUrlVersionData + { + get + { + // url, expectedHref + return new TheoryData + { + { "~/home/index.html", "/approot/home/index.fingerprint.html" }, + { "~/home/index.html\r\n", "/approot/home/index.fingerprint.html" }, + { " ~/home/index.html", "/approot/home/index.fingerprint.html" }, + { "\u000C~/home/index.html\r\n", "/approot/home/index.fingerprint.html" }, + { "\t ~/home/index.html\n", "/approot/home/index.fingerprint.html" }, + { "\r\n~/home/index.html\u000C\t", "/approot/home/index.fingerprint.html" }, + { "\r~/home/index.html\t", "/approot/home/index.fingerprint.html" }, + { "\n~/home/index.html\u202F", "/approot/home/index.fingerprint.html\u202F" }, + { + "~/home/index.html ~/secondValue/index.html", + "/approot/home/index.html ~/secondValue/index.html" + }, + }; + } + } + [Fact] public void Process_DoesNothingIfTagNameIsNull() { @@ -357,4 +381,55 @@ public void Process_ThrowsWhenEncodingNeededAndIUrlHelperActsUnexpectedly() () => tagHelper.Process(context, tagHelperOutput)); Assert.Equal(expectedExceptionMessage, exception.Message, StringComparer.Ordinal); } + + [Theory] + [MemberData(nameof(ResolvableUrlVersionData))] + public void Process_ResolvesVersionedUrls_WhenResourceCollectionIsAvailable(string url, string expectedHref) + { + // Arrange + var tagHelperOutput = new TagHelperOutput( + tagName: "a", + attributes: new TagHelperAttributeList + { + { "href", url } + }, + getChildContentAsync: (useCachedResult, encoder) => Task.FromResult(null)); + var urlHelperMock = new Mock(); + urlHelperMock + .Setup(urlHelper => urlHelper.Content(It.IsAny())) + .Returns(new Func(value => "/approot" + value.Substring(1))); + var urlHelperFactory = new Mock(); + urlHelperFactory + .Setup(f => f.GetUrlHelper(It.IsAny())) + .Returns(urlHelperMock.Object); + + var httpContext = new DefaultHttpContext(); + httpContext.SetEndpoint(new Endpoint( + (context) => Task.CompletedTask, + new EndpointMetadataCollection( + [new ResourceAssetCollection([new("home/index.fingerprint.html", [new ResourceAssetProperty("label", "home/index.html")])])]), + "Test")); + + var tagHelper = new UrlResolutionTagHelper(urlHelperFactory.Object, new HtmlTestEncoder()) + { + ViewContext = new Rendering.ViewContext { HttpContext = httpContext } + }; + + var context = new TagHelperContext( + tagName: "a", + allAttributes: new TagHelperAttributeList( + Enumerable.Empty()), + items: new Dictionary(), + uniqueId: "test"); + + // Act + tagHelper.Process(context, tagHelperOutput); + + // Assert + var attribute = Assert.Single(tagHelperOutput.Attributes); + Assert.Equal("href", attribute.Name, StringComparer.Ordinal); + var attributeValue = Assert.IsType(attribute.Value); + Assert.Equal(expectedHref, attributeValue, StringComparer.Ordinal); + Assert.Equal(HtmlAttributeValueStyle.DoubleQuotes, attribute.ValueStyle); + } } diff --git a/src/Mvc/Mvc.TagHelpers/src/ScriptTagHelper.cs b/src/Mvc/Mvc.TagHelpers/src/ScriptTagHelper.cs index 317aa9b6b4f4..177dd17038a3 100644 --- a/src/Mvc/Mvc.TagHelpers/src/ScriptTagHelper.cs +++ b/src/Mvc/Mvc.TagHelpers/src/ScriptTagHelper.cs @@ -412,10 +412,11 @@ private string GetVersionedSrc(string srcValue) var pathBase = ViewContext.HttpContext.Request.PathBase; if (assetCollection != null) { - var src = assetCollection[srcValue]; - if (!string.Equals(src, srcValue, StringComparison.Ordinal)) + var value = srcValue.StartsWith('/') ? srcValue[1..] : srcValue; + var src = assetCollection[value]; + if (!string.Equals(src, value, StringComparison.Ordinal)) { - return src; + return srcValue.StartsWith('/') ? $"/{src}" : src; } if (pathBase.HasValue && srcValue.StartsWith(pathBase, StringComparison.OrdinalIgnoreCase)) { diff --git a/src/Mvc/Mvc.TagHelpers/test/ScriptTagHelperTest.cs b/src/Mvc/Mvc.TagHelpers/test/ScriptTagHelperTest.cs index ccee2f22d48a..da1e876106e7 100644 --- a/src/Mvc/Mvc.TagHelpers/test/ScriptTagHelperTest.cs +++ b/src/Mvc/Mvc.TagHelpers/test/ScriptTagHelperTest.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Http; @@ -13,7 +14,6 @@ using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.TagHelpers; -using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; @@ -638,6 +638,43 @@ public void RenderScriptTags_WithFileVersion() Assert.Equal("/js/site.js?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk", output.Attributes["src"].Value); } + [Theory] + [InlineData("~/js/site.js")] + [InlineData("/js/site.js")] + [InlineData("js/site.js")] + public void RenderScriptTags_WithFileVersion_UsingResourceCollection(string src) + { + // Arrange + var context = MakeTagHelperContext( + attributes: new TagHelperAttributeList + { + new TagHelperAttribute("src", "/js/site.js"), + new TagHelperAttribute("asp-append-version", "true") + }); + var output = MakeTagHelperOutput("script", attributes: new TagHelperAttributeList()); + + var helper = GetHelper(); + helper.ViewContext.HttpContext.SetEndpoint(CreateEndpoint()); + helper.Src = src; + helper.AppendVersion = true; + + // Act + helper.Process(context, output); + + // Assert + Assert.Equal("script", output.TagName); + Assert.Equal("/js/site.fingerprint.js", output.Attributes["src"].Value); + } + + private Endpoint CreateEndpoint() + { + return new Endpoint( + (context) => Task.CompletedTask, + new EndpointMetadataCollection( + [new ResourceAssetCollection([new("js/site.fingerprint.js", [new ResourceAssetProperty("label", "js/site.js")])])]), + "Test"); + } + [Fact] public void RenderScriptTags_WithFileVersion_AndRequestPathBase() { @@ -823,10 +860,10 @@ private TagHelperContext MakeTagHelperContext( private static ViewContext MakeViewContext(string requestPathBase = null) { - var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()); + var actionContext = new ActionContext(new DefaultHttpContext(), new AspNetCore.Routing.RouteData(), new ActionDescriptor()); if (requestPathBase != null) { - actionContext.HttpContext.Request.PathBase = new Http.PathString(requestPathBase); + actionContext.HttpContext.Request.PathBase = new PathString(requestPathBase); } var metadataProvider = new EmptyModelMetadataProvider(); diff --git a/src/Mvc/samples/MvcSandbox/Views/Shared/_Layout.cshtml b/src/Mvc/samples/MvcSandbox/Views/Shared/_Layout.cshtml index 08d9c383b09d..84f8a9a3dddb 100644 --- a/src/Mvc/samples/MvcSandbox/Views/Shared/_Layout.cshtml +++ b/src/Mvc/samples/MvcSandbox/Views/Shared/_Layout.cshtml @@ -4,8 +4,10 @@ @ViewData["Title"] - MvcSandbox + +
From 8a1a6a380495d65014ccb1e7908f34fd6de8c40c Mon Sep 17 00:00:00 2001 From: jacalvar Date: Wed, 12 Jun 2024 23:24:07 +0200 Subject: [PATCH 11/30] Tests --- src/Mvc/Mvc.TagHelpers/src/ScriptTagHelper.cs | 12 +- .../test/ScriptTagHelperTest.cs | 194 +++++++++++++++++- 2 files changed, 202 insertions(+), 4 deletions(-) diff --git a/src/Mvc/Mvc.TagHelpers/src/ScriptTagHelper.cs b/src/Mvc/Mvc.TagHelpers/src/ScriptTagHelper.cs index 177dd17038a3..d82fad895c1d 100644 --- a/src/Mvc/Mvc.TagHelpers/src/ScriptTagHelper.cs +++ b/src/Mvc/Mvc.TagHelpers/src/ScriptTagHelper.cs @@ -420,11 +420,19 @@ private string GetVersionedSrc(string srcValue) } if (pathBase.HasValue && srcValue.StartsWith(pathBase, StringComparison.OrdinalIgnoreCase)) { - var relativePath = srcValue[pathBase.Value.Length..]; + var length = pathBase.Value.EndsWith('/') ? pathBase.Value.Length : pathBase.Value.Length + 1; + var relativePath = srcValue[length..]; src = assetCollection[relativePath]; if (!string.Equals(src, relativePath, StringComparison.Ordinal)) { - return src; + if (pathBase.Value.EndsWith('/')) + { + return $"{pathBase}{src}"; + } + else + { + return $"{pathBase}/{src}"; + } } } } diff --git a/src/Mvc/Mvc.TagHelpers/test/ScriptTagHelperTest.cs b/src/Mvc/Mvc.TagHelpers/test/ScriptTagHelperTest.cs index da1e876106e7..c5ae4f3f945f 100644 --- a/src/Mvc/Mvc.TagHelpers/test/ScriptTagHelperTest.cs +++ b/src/Mvc/Mvc.TagHelpers/test/ScriptTagHelperTest.cs @@ -666,12 +666,131 @@ public void RenderScriptTags_WithFileVersion_UsingResourceCollection(string src) Assert.Equal("/js/site.fingerprint.js", output.Attributes["src"].Value); } - private Endpoint CreateEndpoint() + [Fact] + public void RenderScriptTags_PathBase_WithFileVersion_UsingResourceCollection() + { + // Arrange + var context = MakeTagHelperContext( + attributes: new TagHelperAttributeList + { + new TagHelperAttribute("src", "/approot/js/site.js"), + new TagHelperAttribute("asp-append-version", "true") + }); + var output = MakeTagHelperOutput("script", attributes: new TagHelperAttributeList()); + + var helper = GetHelper(); + helper.ViewContext.HttpContext.SetEndpoint(CreateEndpoint()); + helper.ViewContext.HttpContext.Request.PathBase = "/approot"; + helper.Src = "/approot/js/site.js"; + helper.AppendVersion = true; + + // Act + helper.Process(context, output); + + // Assert + Assert.Equal("script", output.TagName); + Assert.Equal("/approot/js/site.fingerprint.js", output.Attributes["src"].Value); + } + + [Fact] + public void ScriptTagHelper_RendersProvided_ImportMap() + { + // Arrange + var importMap = new ImportMapDefinition( + new Dictionary + { + { "jquery", "https://code.jquery.com/jquery-3.5.1.min.js" }, + { "bootstrap", "https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" } + }, + new Dictionary> + { + ["development"] = new Dictionary + { + { "jquery", "https://code.jquery.com/jquery-3.5.1.js" }, + { "bootstrap", "https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.js" } + }.AsReadOnly() + }, + new Dictionary + { + { "https://code.jquery.com/jquery-3.5.1.js", "sha384-jquery" }, + { "https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.js", "sha256-bootstrap" } + }); + + var context = MakeTagHelperContext( + attributes: new TagHelperAttributeList + { + new TagHelperAttribute("type", "importmap"), + new TagHelperAttribute("asp-importmap", importMap) + }); + var output = MakeTagHelperOutput("script", attributes: new TagHelperAttributeList()); + + var helper = GetHelper(); + helper.ViewContext.HttpContext.SetEndpoint(CreateEndpoint()); + helper.Type = "importmap"; + helper.ImportMap = importMap; + + // Act + helper.Process(context, output); + + // Assert + Assert.Equal("script", output.TagName); + Assert.Equal(importMap.ToJson(), output.Content.GetContent()); + } + + [Fact] + public void ScriptTagHelper_RendersImportMap_FromEndpoint() + { + // Arrange + var importMap = new ImportMapDefinition( + new Dictionary + { + { "jquery", "https://code.jquery.com/jquery-3.5.1.min.js" }, + { "bootstrap", "https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" } + }, + new Dictionary> + { + ["development"] = new Dictionary + { + { "jquery", "https://code.jquery.com/jquery-3.5.1.js" }, + { "bootstrap", "https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.js" } + }.AsReadOnly() + }, + new Dictionary + { + { "https://code.jquery.com/jquery-3.5.1.js", "sha384-jquery" }, + { "https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.js", "sha256-bootstrap" } + }); + + var context = MakeTagHelperContext( + attributes: new TagHelperAttributeList + { + new TagHelperAttribute("type", "importmap"), + }); + var output = MakeTagHelperOutput("script", attributes: new TagHelperAttributeList()); + + var helper = GetHelper(); + helper.ViewContext.HttpContext.SetEndpoint(CreateEndpoint(importMap)); + helper.Type = "importmap"; + + // Act + helper.Process(context, output); + + // Assert + Assert.Equal("script", output.TagName); + Assert.Equal(importMap.ToJson(), output.Content.GetContent()); + } + + private Endpoint CreateEndpoint(ImportMapDefinition importMap = null) { return new Endpoint( (context) => Task.CompletedTask, new EndpointMetadataCollection( - [new ResourceAssetCollection([new("js/site.fingerprint.js", [new ResourceAssetProperty("label", "js/site.js")])])]), + [new ResourceAssetCollection([ + new("js/site.fingerprint.js", [new ResourceAssetProperty("label", "js/site.js")]), + new("common.fingerprint.js", [new ResourceAssetProperty("label", "common.js")]), + new("fallback.fingerprint.js", [new ResourceAssetProperty("label", "fallback.js")]), + ]), + importMap ?? new ImportMapDefinition(null, null, null)]), "Test"); } @@ -731,6 +850,38 @@ public void RenderScriptTags_FallbackSrc_WithFileVersion() "]]\"));", output.PostElement.GetContent()); } + [Fact] + public void RenderScriptTags_FallbackSrc_AppendVersion_WithResourceCollection() + { + // Arrange + var context = MakeTagHelperContext( + attributes: new TagHelperAttributeList + { + new TagHelperAttribute("src", "/js/site.js"), + new TagHelperAttribute("asp-fallback-src-include", "fallback.js"), + new TagHelperAttribute("asp-fallback-test", "isavailable()"), + new TagHelperAttribute("asp-append-version", "true") + }); + var output = MakeTagHelperOutput("script", attributes: new TagHelperAttributeList()); + + var helper = GetHelper(); + helper.ViewContext.HttpContext.SetEndpoint(CreateEndpoint()); + helper.FallbackSrc = "fallback.js"; + helper.FallbackTestExpression = "isavailable()"; + helper.AppendVersion = true; + helper.Src = "/js/site.js"; + + // Act + helper.Process(context, output); + + // Assert + Assert.Equal("script", output.TagName); + Assert.Equal("/js/site.fingerprint.js", output.Attributes["src"].Value); + Assert.Equal(Environment.NewLine + "]]\"));", output.PostElement.GetContent()); + } + [Fact] public void RenderScriptTags_FallbackSrc_WithFileVersion_EncodesAsExpected() { @@ -821,6 +972,45 @@ public void RenderScriptTags_GlobbedSrc_WithFileVersion() Assert.Equal(expectedContent, content); } + [Fact] + public void RenderScriptTags_GlobbedSrc_WithFileVersion_WithResourceCollection() + { + // Arrange + var expectedContent = "" + + ""; + var context = MakeTagHelperContext( + attributes: new TagHelperAttributeList + { + new TagHelperAttribute("src", "/js/site.js"), + new TagHelperAttribute("asp-src-include", "*.js"), + new TagHelperAttribute("asp-append-version", "true") + }); + var output = MakeTagHelperOutput("script", attributes: new TagHelperAttributeList()); + var globbingUrlBuilder = new Mock( + new TestFileProvider(), + Mock.Of(), + PathString.Empty); + globbingUrlBuilder.Setup(g => g.BuildUrlList(null, "*.js", null)) + .Returns(new[] { "/common.js" }); + + var helper = GetHelper(); + helper.ViewContext.HttpContext.SetEndpoint(CreateEndpoint()); + helper.GlobbingUrlBuilder = globbingUrlBuilder.Object; + helper.SrcInclude = "*.js"; + helper.AppendVersion = true; + helper.Src = "/js/site.js"; + + // Act + helper.Process(context, output); + + // Assert + Assert.Equal("script", output.TagName); + Assert.Equal("/js/site.fingerprint.js", output.Attributes["src"].Value); + var content = HtmlContentUtilities.HtmlContentToString(output, new HtmlTestEncoder()); + Assert.Equal(expectedContent, content); + } + private static ScriptTagHelper GetHelper( IWebHostEnvironment hostingEnvironment = null, IUrlHelperFactory urlHelperFactory = null, From 7b79a5375b2c5567baed502be969f6ba11f1d9c1 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Thu, 13 Jun 2024 00:48:28 +0200 Subject: [PATCH 12/30] More tests --- .../Components/src/PublicAPI.Unshipped.txt | 1 + .../Components/src/ResourceAssetCollection.cs | 13 ++++ src/Mvc/Mvc.TagHelpers/src/ImageTagHelper.cs | 38 +++++++--- src/Mvc/Mvc.TagHelpers/src/ScriptTagHelper.cs | 9 +++ .../Mvc.TagHelpers/test/ImageTagHelperTest.cs | 72 +++++++++++++++++-- .../test/ScriptTagHelperTest.cs | 28 +++++--- 6 files changed, 140 insertions(+), 21 deletions(-) diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index c669b0dc567c..77e24cf1f6e5 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -16,6 +16,7 @@ Microsoft.AspNetCore.Components.ResourceAsset.Properties.get -> System.Collectio Microsoft.AspNetCore.Components.ResourceAsset.ResourceAsset(string! url, System.Collections.Generic.IReadOnlyList? properties) -> void Microsoft.AspNetCore.Components.ResourceAsset.Url.get -> string! Microsoft.AspNetCore.Components.ResourceAssetCollection +Microsoft.AspNetCore.Components.ResourceAssetCollection.IsContentSpecificUrl(string! path) -> bool Microsoft.AspNetCore.Components.ResourceAssetCollection.ResourceAssetCollection(System.Collections.Generic.IReadOnlyList! resources) -> void Microsoft.AspNetCore.Components.ResourceAssetCollection.this[string! key].get -> string! Microsoft.AspNetCore.Components.ResourceAssetProperty diff --git a/src/Components/Components/src/ResourceAssetCollection.cs b/src/Components/Components/src/ResourceAssetCollection.cs index a84ab8c96843..56fb5c334b62 100644 --- a/src/Components/Components/src/ResourceAssetCollection.cs +++ b/src/Components/Components/src/ResourceAssetCollection.cs @@ -16,6 +16,7 @@ public sealed class ResourceAssetCollection : IReadOnlyList public static readonly ResourceAssetCollection Empty = new([]); private readonly Dictionary _uniqueUrlMappings; + private readonly HashSet _contentSpecificUrls; private readonly IReadOnlyList _resources; /// @@ -25,6 +26,7 @@ public sealed class ResourceAssetCollection : IReadOnlyList public ResourceAssetCollection(IReadOnlyList resources) { _uniqueUrlMappings = new Dictionary(StringComparer.OrdinalIgnoreCase); + _contentSpecificUrls = new HashSet(StringComparer.OrdinalIgnoreCase); _resources = resources; foreach (var resource in resources) { @@ -33,6 +35,7 @@ public ResourceAssetCollection(IReadOnlyList resources) if (property.Name.Equals("label", StringComparison.OrdinalIgnoreCase)) { _uniqueUrlMappings[property.Value] = resource; + _contentSpecificUrls.Add(resource.Url); } } } @@ -56,6 +59,16 @@ public string this[string key] } } + /// + /// Determines whether the specified path is a content-specific URL. + /// + /// The path to check. + /// true if the path is a content-specific URL; otherwise, false. + public bool IsContentSpecificUrl(string path) + { + return _contentSpecificUrls.Contains(path); + } + ResourceAsset IReadOnlyList.this[int index] => _resources[index]; int IReadOnlyCollection.Count => _resources.Count; IEnumerator IEnumerable.GetEnumerator() => _resources.GetEnumerator(); diff --git a/src/Mvc/Mvc.TagHelpers/src/ImageTagHelper.cs b/src/Mvc/Mvc.TagHelpers/src/ImageTagHelper.cs index 583e3330772b..52a41089121d 100644 --- a/src/Mvc/Mvc.TagHelpers/src/ImageTagHelper.cs +++ b/src/Mvc/Mvc.TagHelpers/src/ImageTagHelper.cs @@ -139,7 +139,7 @@ private void EnsureFileVersionProvider() } } - private string GetVersionedResourceUrl(string value) + private string GetVersionedResourceUrl(string url) { if (AppendVersion == true) { @@ -147,28 +147,48 @@ private string GetVersionedResourceUrl(string value) var pathBase = ViewContext.HttpContext.Request.PathBase; if (assetCollection != null) { + var value = url.StartsWith('/') ? url[1..] : url; + if (assetCollection.IsContentSpecificUrl(value)) + { + return url; + } + var src = assetCollection[value]; if (!string.Equals(src, value, StringComparison.Ordinal)) { - return src; + return url.StartsWith('/') ? $"/{src}" : src; } - if (pathBase.HasValue && value.StartsWith(pathBase, StringComparison.OrdinalIgnoreCase)) + if (pathBase.HasValue && url.StartsWith(pathBase, StringComparison.OrdinalIgnoreCase)) { - var relativePath = value[pathBase.Value.Length..]; + var length = pathBase.Value.EndsWith('/') ? pathBase.Value.Length : pathBase.Value.Length + 1; + var relativePath = url[length..]; + if (assetCollection.IsContentSpecificUrl(relativePath)) + { + return url; + } + src = assetCollection[relativePath]; if (!string.Equals(src, relativePath, StringComparison.Ordinal)) { - return src; + if (pathBase.Value.EndsWith('/')) + { + return $"{pathBase}{src}"; + } + else + { + return $"{pathBase}/{src}"; + } } } } - EnsureFileVersionProvider(); - - value = FileVersionProvider.AddFileVersionToPath(pathBase, value); + if (url != null) + { + url = FileVersionProvider.AddFileVersionToPath(pathBase, url); + } } - return value; + return url; } private ResourceAssetCollection GetAssetCollection() diff --git a/src/Mvc/Mvc.TagHelpers/src/ScriptTagHelper.cs b/src/Mvc/Mvc.TagHelpers/src/ScriptTagHelper.cs index d82fad895c1d..c5b6c1fbc4b6 100644 --- a/src/Mvc/Mvc.TagHelpers/src/ScriptTagHelper.cs +++ b/src/Mvc/Mvc.TagHelpers/src/ScriptTagHelper.cs @@ -413,15 +413,24 @@ private string GetVersionedSrc(string srcValue) if (assetCollection != null) { var value = srcValue.StartsWith('/') ? srcValue[1..] : srcValue; + if (assetCollection.IsContentSpecificUrl(value)) + { + return srcValue; + } var src = assetCollection[value]; if (!string.Equals(src, value, StringComparison.Ordinal)) { return srcValue.StartsWith('/') ? $"/{src}" : src; } + if (pathBase.HasValue && srcValue.StartsWith(pathBase, StringComparison.OrdinalIgnoreCase)) { var length = pathBase.Value.EndsWith('/') ? pathBase.Value.Length : pathBase.Value.Length + 1; var relativePath = srcValue[length..]; + if (assetCollection.IsContentSpecificUrl(relativePath)) + { + return srcValue; + } src = assetCollection[relativePath]; if (!string.Equals(src, relativePath, StringComparison.Ordinal)) { diff --git a/src/Mvc/Mvc.TagHelpers/test/ImageTagHelperTest.cs b/src/Mvc/Mvc.TagHelpers/test/ImageTagHelperTest.cs index a31fbac04ce7..9199d1d09001 100644 --- a/src/Mvc/Mvc.TagHelpers/test/ImageTagHelperTest.cs +++ b/src/Mvc/Mvc.TagHelpers/test/ImageTagHelperTest.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Http; @@ -160,6 +161,69 @@ public void RendersImageTag_AddsFileVersion() Assert.Equal("/images/test-image.png?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk", srcAttribute.Value); } + [Theory] + [InlineData("~/images/test-image.png", "/bar", "/bar/images/test-image.fingerprint.png")] + [InlineData("/images/test-image.png", null, "/images/test-image.fingerprint.png")] + [InlineData("images/test-image.png", null, "images/test-image.fingerprint.png")] + public void RendersImageTag_AddsFileVersion_WithResourceCollection(string src, string pathBase, string expectedValue) + { + // Arrange + var context = MakeTagHelperContext( + attributes: new TagHelperAttributeList + { + { "alt", new HtmlString("Alt image text") }, + { "src", src }, + { "asp-append-version", "true" } + }); + var output = MakeImageTagHelperOutput(attributes: new TagHelperAttributeList + { + { "alt", new HtmlString("Alt image text") }, + }); + var hostingEnvironment = MakeHostingEnvironment(); + var viewContext = MakeViewContext(); + + var urlHelperFactory = MakeUrlHelperFactory(value => + { + if (value.StartsWith("~/", StringComparison.Ordinal)) + { + return pathBase == null ? value.Replace("~/", string.Empty) : value.Replace("~/", pathBase + "/"); + } + return value; + }); + + var helper = GetHelper(urlHelperFactory: urlHelperFactory); + helper.ViewContext = viewContext; + helper.ViewContext.HttpContext = new DefaultHttpContext(); + helper.ViewContext.HttpContext.SetEndpoint(CreateEndpoint()); + if (pathBase != null) + { + helper.ViewContext.HttpContext.Request.PathBase = new PathString(pathBase); + } + helper.Src = src; + helper.AppendVersion = true; + + // Act + helper.Process(context, output); + + // Assert + Assert.True(output.Content.GetContent().Length == 0); + Assert.Equal("img", output.TagName); + Assert.Equal(2, output.Attributes.Count); + var srcAttribute = Assert.Single(output.Attributes, attr => attr.Name.Equals("src")); + Assert.Equal(expectedValue, srcAttribute.Value); + } + + private Endpoint CreateEndpoint() + { + return new Endpoint( + (context) => Task.CompletedTask, + new EndpointMetadataCollection( + [new ResourceAssetCollection([ + new("images/test-image.fingerprint.png", [new ResourceAssetProperty("label", "images/test-image.png")]), + ])]), + "Test"); + } + [Fact] public void RendersImageTag_DoesNotAddFileVersion() { @@ -226,7 +290,7 @@ public void RendersImageTag_AddsFileVersion_WithRequestPathBase() private static ViewContext MakeViewContext(string requestPathBase = null) { - var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()); + var actionContext = new ActionContext(new DefaultHttpContext(), new AspNetCore.Routing.RouteData(), new ActionDescriptor()); if (requestPathBase != null) { actionContext.HttpContext.Request.PathBase = new Http.PathString(requestPathBase); @@ -314,13 +378,13 @@ private static IWebHostEnvironment MakeHostingEnvironment() return hostingEnvironment.Object; } - private static IUrlHelperFactory MakeUrlHelperFactory() + private static IUrlHelperFactory MakeUrlHelperFactory(Func contentAction = null) { var urlHelper = new Mock(); - + contentAction ??= (url) => url; urlHelper .Setup(helper => helper.Content(It.IsAny())) - .Returns(new Func(url => url)); + .Returns(new Func(contentAction)); var urlHelperFactory = new Mock(); urlHelperFactory diff --git a/src/Mvc/Mvc.TagHelpers/test/ScriptTagHelperTest.cs b/src/Mvc/Mvc.TagHelpers/test/ScriptTagHelperTest.cs index c5ae4f3f945f..ee79dc21b2b8 100644 --- a/src/Mvc/Mvc.TagHelpers/test/ScriptTagHelperTest.cs +++ b/src/Mvc/Mvc.TagHelpers/test/ScriptTagHelperTest.cs @@ -666,22 +666,34 @@ public void RenderScriptTags_WithFileVersion_UsingResourceCollection(string src) Assert.Equal("/js/site.fingerprint.js", output.Attributes["src"].Value); } - [Fact] - public void RenderScriptTags_PathBase_WithFileVersion_UsingResourceCollection() + [Theory] + [InlineData("~/js/site.js")] + [InlineData("/approot/js/site.js")] + public void RenderScriptTags_PathBase_WithFileVersion_UsingResourceCollection(string path) { // Arrange var context = MakeTagHelperContext( attributes: new TagHelperAttributeList { - new TagHelperAttribute("src", "/approot/js/site.js"), + new TagHelperAttribute("src", path), new TagHelperAttribute("asp-append-version", "true") }); var output = MakeTagHelperOutput("script", attributes: new TagHelperAttributeList()); - var helper = GetHelper(); + var urlHelperFactory = MakeUrlHelperFactory(value => + { + if (value.StartsWith("~/", StringComparison.Ordinal)) + { + return value.Replace("~/", "/approot/"); + } + + return value; + }); + + var helper = GetHelper(urlHelperFactory: urlHelperFactory); helper.ViewContext.HttpContext.SetEndpoint(CreateEndpoint()); helper.ViewContext.HttpContext.Request.PathBase = "/approot"; - helper.Src = "/approot/js/site.js"; + helper.Src = path; helper.AppendVersion = true; // Act @@ -1103,13 +1115,13 @@ private static IWebHostEnvironment MakeHostingEnvironment() return hostingEnvironment.Object; } - private static IUrlHelperFactory MakeUrlHelperFactory() + private static IUrlHelperFactory MakeUrlHelperFactory(Func urlResolver = null) { var urlHelper = new Mock(); - + urlResolver ??= (url) => url; urlHelper .Setup(helper => helper.Content(It.IsAny())) - .Returns(new Func(url => url)); + .Returns(new Func(urlResolver)); var urlHelperFactory = new Mock(); urlHelperFactory From 00f1ce8cea2499465b7637792eca96bd3e49c995 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Thu, 13 Jun 2024 12:24:11 +0200 Subject: [PATCH 13/30] LinkTagHelper tests --- src/Mvc/Mvc.TagHelpers/src/LinkTagHelper.cs | 36 +++- .../Mvc.TagHelpers/test/LinkTagHelperTest.cs | 197 +++++++++++++++++- .../test/ScriptTagHelperTest.cs | 16 +- 3 files changed, 230 insertions(+), 19 deletions(-) diff --git a/src/Mvc/Mvc.TagHelpers/src/LinkTagHelper.cs b/src/Mvc/Mvc.TagHelpers/src/LinkTagHelper.cs index 51ac412d37d9..91d07a6510b3 100644 --- a/src/Mvc/Mvc.TagHelpers/src/LinkTagHelper.cs +++ b/src/Mvc/Mvc.TagHelpers/src/LinkTagHelper.cs @@ -531,7 +531,7 @@ private void AppendVersionedHref(string hrefName, string hrefValue, TagHelperCon .AppendHtml("\" "); } - private string GetVersionedResourceUrl(string value) + private string GetVersionedResourceUrl(string url) { if (AppendVersion == true) { @@ -539,26 +539,48 @@ private string GetVersionedResourceUrl(string value) var pathBase = ViewContext.HttpContext.Request.PathBase; if (assetCollection != null) { + var value = url.StartsWith('/') ? url[1..] : url; + if (assetCollection.IsContentSpecificUrl(value)) + { + return url; + } + var src = assetCollection[value]; if (!string.Equals(src, value, StringComparison.Ordinal)) { - return src; + return url.StartsWith('/') ? $"/{src}" : src; } - if (pathBase.HasValue && value.StartsWith(pathBase, StringComparison.OrdinalIgnoreCase)) + if (pathBase.HasValue && url.StartsWith(pathBase, StringComparison.OrdinalIgnoreCase)) { - var relativePath = value[pathBase.Value.Length..]; + var length = pathBase.Value.EndsWith('/') ? pathBase.Value.Length : pathBase.Value.Length + 1; + var relativePath = url[length..]; + if (assetCollection.IsContentSpecificUrl(relativePath)) + { + return url; + } + src = assetCollection[relativePath]; if (!string.Equals(src, relativePath, StringComparison.Ordinal)) { - return src; + if (pathBase.Value.EndsWith('/')) + { + return $"{pathBase}{src}"; + } + else + { + return $"{pathBase}/{src}"; + } } } } - value = FileVersionProvider.AddFileVersionToPath(pathBase, value); + if (url != null) + { + url = FileVersionProvider.AddFileVersionToPath(pathBase, url); + } } - return value; + return url; } private ResourceAssetCollection GetAssetCollection() diff --git a/src/Mvc/Mvc.TagHelpers/test/LinkTagHelperTest.cs b/src/Mvc/Mvc.TagHelpers/test/LinkTagHelperTest.cs index 8b04fa7e6835..7668d461604a 100644 --- a/src/Mvc/Mvc.TagHelpers/test/LinkTagHelperTest.cs +++ b/src/Mvc/Mvc.TagHelpers/test/LinkTagHelperTest.cs @@ -3,9 +3,11 @@ using System.Globalization; using System.Text; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Razor.Infrastructure; @@ -14,8 +16,6 @@ using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.TagHelpers; -using Microsoft.AspNetCore.Routing; -using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Primitives; @@ -725,6 +725,82 @@ public void RendersLinkTags_WithFileVersion() Assert.Equal("/css/site.css?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk", output.Attributes["href"].Value); } + [Theory] + [InlineData("~/css/site.css", "/css/site.fingerprint.css")] + [InlineData("/css/site.css", "/css/site.fingerprint.css")] + [InlineData("css/site.css", "css/site.fingerprint.css")] + public void RendersLinkTag_WithFileVersion_UsingResourceCollection(string href, string expected) + { + // Arrange + var context = MakeTagHelperContext( + attributes: new TagHelperAttributeList + { + { "rel", new HtmlString("stylesheet") }, + { "href", href }, + { "asp-append-version", "true" } + }); + var output = MakeTagHelperOutput("link", attributes: new TagHelperAttributeList + { + { "rel", new HtmlString("stylesheet") }, + }); + + var helper = GetHelper(urlHelperFactory: MakeUrlHelperFactory(value => + value.StartsWith("~/", StringComparison.Ordinal) ? value[1..] : value)); + + helper.ViewContext.HttpContext.SetEndpoint(CreateEndpoint()); + helper.Href = href; + helper.AppendVersion = true; + + // Act + helper.Process(context, output); + + // Assert + Assert.Equal("link", output.TagName); + Assert.Equal(expected, output.Attributes["href"].Value); + } + + [Theory] + [InlineData("~/css/site.css")] + [InlineData("/approot/css/site.css")] + public void RenderLinkTags_PathBase_WithFileVersion_UsingResourceCollection(string href) + { + // Arrange + var context = MakeTagHelperContext( + attributes: new TagHelperAttributeList + { + { "rel", new HtmlString("stylesheet") }, + { "href", href }, + { "asp-append-version", "true" } + }); + var output = MakeTagHelperOutput("link", attributes: new TagHelperAttributeList + { + { "rel", new HtmlString("stylesheet") }, + }); + + var urlHelperFactory = MakeUrlHelperFactory(value => + { + if (value.StartsWith("~/", StringComparison.Ordinal)) + { + return value.Replace("~/", "/approot/"); + } + + return value; + }); + + var helper = GetHelper(urlHelperFactory: urlHelperFactory); + helper.ViewContext.HttpContext.SetEndpoint(CreateEndpoint()); + helper.ViewContext.HttpContext.Request.PathBase = "/approot"; + helper.Href = href; + helper.AppendVersion = true; + + // Act + helper.Process(context, output); + + // Assert + Assert.Equal("link", output.TagName); + Assert.Equal("/approot/css/site.fingerprint.css", output.Attributes["href"].Value); + } + [Fact] public void RendersLinkTags_WithFileVersion_AndRequestPathBase() { @@ -809,6 +885,61 @@ public void RenderLinkTags_FallbackHref_WithFileVersion() Assert.Equal(expectedPostElement, output.PostElement.GetContent()); } + [Fact] + public void RenderLinkTags_FallbackHref_WithFileVersion_WithResourceCollection() + { + // Arrange + var expectedPostElement = Environment.NewLine + + ""; + var context = MakeTagHelperContext( + attributes: new TagHelperAttributeList + { + { "asp-append-version", "true" }, + { "asp-fallback-href-include", "**/fallback.css" }, + { "asp-fallback-test-class", "hidden" }, + { "asp-fallback-test-property", "visibility" }, + { "asp-fallback-test-value", "hidden" }, + { "href", "/css/site.css" }, + { "rel", new HtmlString("stylesheet") }, + }); + var output = MakeTagHelperOutput( + "link", + attributes: new TagHelperAttributeList + { + { "rel", new HtmlString("stylesheet") }, + }); + var globbingUrlBuilder = new Mock( + new TestFileProvider(), + Mock.Of(), + PathString.Empty); + globbingUrlBuilder.Setup(g => g.BuildUrlList(null, "**/fallback.css", null)) + .Returns(new[] { "/fallback.css" }); + + var helper = GetHelper(); + helper.ViewContext.HttpContext.SetEndpoint(CreateEndpoint()); + helper.AppendVersion = true; + helper.Href = "/css/site.css"; + helper.FallbackHrefInclude = "**/fallback.css"; + helper.FallbackTestClass = "hidden"; + helper.FallbackTestProperty = "visibility"; + helper.FallbackTestValue = "hidden"; + helper.GlobbingUrlBuilder = globbingUrlBuilder.Object; + + // Act + helper.Process(context, output); + + // Assert + Assert.Equal("link", output.TagName); + Assert.Equal("/css/site.fingerprint.css", output.Attributes["href"].Value); + Assert.Equal(expectedPostElement, output.PostElement.GetContent()); + } + [Fact] public void RenderLinkTags_FallbackHref_WithFileVersion_EncodesAsExpected() { @@ -922,6 +1053,48 @@ public void RendersLinkTags_GlobbedHref_WithFileVersion() content); } + [Fact] + public void RendersLinkTags_GlobbedHref_WithFileVersion_WithResourceCollection() + { + // Arrange + var context = MakeTagHelperContext( + attributes: new TagHelperAttributeList + { + { "rel", new HtmlString("stylesheet") }, + { "href", "/css/site.css" }, + { "asp-href-include", "**/*.css" }, + { "asp-append-version", "true" }, + }); + var output = MakeTagHelperOutput("link", attributes: new TagHelperAttributeList + { + { "rel", new HtmlString("stylesheet") }, + }); + var globbingUrlBuilder = new Mock( + new TestFileProvider(), + Mock.Of(), + PathString.Empty); + globbingUrlBuilder.Setup(g => g.BuildUrlList(null, "**/*.css", null)) + .Returns(new[] { "/base.css" }); + + var helper = GetHelper(); + helper.ViewContext.HttpContext.SetEndpoint(CreateEndpoint()); + helper.GlobbingUrlBuilder = globbingUrlBuilder.Object; + helper.Href = "/css/site.css"; + helper.HrefInclude = "**/*.css"; + helper.AppendVersion = true; + + // Act + helper.Process(context, output); + + // Assert + Assert.Equal("link", output.TagName); + Assert.Equal("/css/site.fingerprint.css", output.Attributes["href"].Value); + var content = HtmlContentUtilities.HtmlContentToString(output.PostElement, new HtmlTestEncoder()); + Assert.Equal( + "", + content); + } + private static LinkTagHelper GetHelper( IWebHostEnvironment hostingEnvironment = null, IUrlHelperFactory urlHelperFactory = null, @@ -948,7 +1121,7 @@ private static LinkTagHelper GetHelper( private static ViewContext MakeViewContext(string requestPathBase = null) { - var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()); + var actionContext = new ActionContext(new DefaultHttpContext(), new AspNetCore.Routing.RouteData(), new ActionDescriptor()); if (requestPathBase != null) { actionContext.HttpContext.Request.PathBase = new PathString(requestPathBase); @@ -1015,13 +1188,27 @@ private static IWebHostEnvironment MakeHostingEnvironment() return hostingEnvironment.Object; } - private static IUrlHelperFactory MakeUrlHelperFactory() + private Endpoint CreateEndpoint() + { + return new Endpoint( + (context) => Task.CompletedTask, + new EndpointMetadataCollection( + [new ResourceAssetCollection([ + new("css/site.fingerprint.css", [new ResourceAssetProperty("label", "css/site.css")]), + new("base.fingerprint.css", [new ResourceAssetProperty("label", "base.css")]), + new("fallback.fingerprint.css", [new ResourceAssetProperty("label", "fallback.css")]), + ])]), + "Test"); + } + + private static IUrlHelperFactory MakeUrlHelperFactory(Func content = null) { var urlHelper = new Mock(); + content ??= (url) => url; urlHelper .Setup(helper => helper.Content(It.IsAny())) - .Returns(new Func(url => url)); + .Returns(new Func(content)); var urlHelperFactory = new Mock(); urlHelperFactory .Setup(f => f.GetUrlHelper(It.IsAny())) diff --git a/src/Mvc/Mvc.TagHelpers/test/ScriptTagHelperTest.cs b/src/Mvc/Mvc.TagHelpers/test/ScriptTagHelperTest.cs index ee79dc21b2b8..b8bac882436d 100644 --- a/src/Mvc/Mvc.TagHelpers/test/ScriptTagHelperTest.cs +++ b/src/Mvc/Mvc.TagHelpers/test/ScriptTagHelperTest.cs @@ -639,21 +639,23 @@ public void RenderScriptTags_WithFileVersion() } [Theory] - [InlineData("~/js/site.js")] - [InlineData("/js/site.js")] - [InlineData("js/site.js")] - public void RenderScriptTags_WithFileVersion_UsingResourceCollection(string src) + [InlineData("~/js/site.js", "/js/site.fingerprint.js")] + [InlineData("/js/site.js", "/js/site.fingerprint.js")] + [InlineData("js/site.js", "js/site.fingerprint.js")] + public void RenderScriptTags_WithFileVersion_UsingResourceCollection(string src, string expected) { // Arrange var context = MakeTagHelperContext( attributes: new TagHelperAttributeList { - new TagHelperAttribute("src", "/js/site.js"), + new TagHelperAttribute("src", src), new TagHelperAttribute("asp-append-version", "true") }); var output = MakeTagHelperOutput("script", attributes: new TagHelperAttributeList()); - var helper = GetHelper(); + var helper = GetHelper(urlHelperFactory: MakeUrlHelperFactory(value => + value.StartsWith("~/", StringComparison.Ordinal) ? value[1..] : value)); + helper.ViewContext.HttpContext.SetEndpoint(CreateEndpoint()); helper.Src = src; helper.AppendVersion = true; @@ -663,7 +665,7 @@ public void RenderScriptTags_WithFileVersion_UsingResourceCollection(string src) // Assert Assert.Equal("script", output.TagName); - Assert.Equal("/js/site.fingerprint.js", output.Attributes["src"].Value); + Assert.Equal(expected, output.Attributes["src"].Value); } [Theory] From 988464fd9494626a6692c5d994cd7189109535b6 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Thu, 13 Jun 2024 18:43:46 +0200 Subject: [PATCH 14/30] More tests --- .../Components/src/ResourceAssetCollection.cs | 19 +- .../test/ResourceAssetCollectionTest.cs | 88 ++++ .../src/Assets/ImportMapDefinition.cs | 14 +- .../test/Assets/ImportMapDefinitionTest.cs | 200 +++++++++ .../Endpoints/test/ImportMapTest.cs | 393 ++++++++++++++++++ ...BuilderResourceCollectionExtensionsTest.cs | 287 +++++++++++++ ...soft.AspNetCore.Mvc.RazorPages.Test.csproj | 8 +- ...Application.staticwebassets.endpoints.json | 17 + .../Test.staticwebassets.endpoints.json | 17 + ...BuilderResourceCollectionExtensionsTest.cs | 362 ++++++++++++++++ ...ft.AspNetCore.Mvc.ViewFeatures.Test.csproj | 10 + ...Application.staticwebassets.endpoints.json | 17 + .../Test.staticwebassets.endpoints.json | 17 + 13 files changed, 1428 insertions(+), 21 deletions(-) create mode 100644 src/Components/Components/test/ResourceAssetCollectionTest.cs create mode 100644 src/Components/Endpoints/test/Assets/ImportMapDefinitionTest.cs create mode 100644 src/Components/Endpoints/test/ImportMapTest.cs create mode 100644 src/Mvc/Mvc.RazorPages/test/Builder/PageActionEndpointConventionBuilderResourceCollectionExtensionsTest.cs create mode 100644 src/Mvc/Mvc.RazorPages/test/TestApplication.staticwebassets.endpoints.json create mode 100644 src/Mvc/Mvc.RazorPages/test/TestManifests/Test.staticwebassets.endpoints.json create mode 100644 src/Mvc/Mvc.ViewFeatures/test/Builder/ControllerActionEndpointConventionBuilderResourceCollectionExtensionsTest.cs create mode 100644 src/Mvc/Mvc.ViewFeatures/test/TestApplication.staticwebassets.endpoints.json create mode 100644 src/Mvc/Mvc.ViewFeatures/test/TestManifests/Test.staticwebassets.endpoints.json diff --git a/src/Components/Components/src/ResourceAssetCollection.cs b/src/Components/Components/src/ResourceAssetCollection.cs index 56fb5c334b62..3167defc88c1 100644 --- a/src/Components/Components/src/ResourceAssetCollection.cs +++ b/src/Components/Components/src/ResourceAssetCollection.cs @@ -46,29 +46,16 @@ public ResourceAssetCollection(IReadOnlyList resources) /// /// The asset name. /// The unique URL if availabe, the same if not available. - public string this[string key] - { - get - { - if (_uniqueUrlMappings.TryGetValue(key, out var value)) - { - return value.Url; - } - - return key; - } - } + public string this[string key] => _uniqueUrlMappings.TryGetValue(key, out var value) ? value.Url : key; /// /// Determines whether the specified path is a content-specific URL. /// /// The path to check. /// true if the path is a content-specific URL; otherwise, false. - public bool IsContentSpecificUrl(string path) - { - return _contentSpecificUrls.Contains(path); - } + public bool IsContentSpecificUrl(string path) => _contentSpecificUrls.Contains(path); + // IReadOnlyList implementation ResourceAsset IReadOnlyList.this[int index] => _resources[index]; int IReadOnlyCollection.Count => _resources.Count; IEnumerator IEnumerable.GetEnumerator() => _resources.GetEnumerator(); diff --git a/src/Components/Components/test/ResourceAssetCollectionTest.cs b/src/Components/Components/test/ResourceAssetCollectionTest.cs new file mode 100644 index 000000000000..fa1ed1c09f09 --- /dev/null +++ b/src/Components/Components/test/ResourceAssetCollectionTest.cs @@ -0,0 +1,88 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components; + +public class ResourceAssetCollectionTest +{ + [Fact] + public void CanCreateResourceCollection() + { + // Arrange + var resourceAssetCollection = new ResourceAssetCollection([ + new ResourceAsset("image1.jpg",[]), + ]); + + // Act + var collectionAsReadOnlyList = resourceAssetCollection as IReadOnlyList; + + // Assert + Assert.Equal(1, collectionAsReadOnlyList.Count); + Assert.Equal("image1.jpg", collectionAsReadOnlyList[0].Url); + } + + [Fact] + public void CanResolveFingerprintedResources() + { + // Arrange + var resourceAssetCollection = new ResourceAssetCollection([ + new ResourceAsset( + "image1.fingerprint.jpg", + [new ResourceAssetProperty("label", "image1.jpg")]), + ]); + + // Act + var resolvedUrl = resourceAssetCollection["image1.jpg"]; + + // Assert + Assert.Equal("image1.fingerprint.jpg", resolvedUrl); + } + + [Fact] + public void ResolvingNoFingerprintedResourcesReturnsSameUrl() + { + // Arrange + var resourceAssetCollection = new ResourceAssetCollection([ + new ResourceAsset("image1.jpg",[])]); + + // Act + var resolvedUrl = resourceAssetCollection["image1.jpg"]; + + // Assert + Assert.Equal("image1.jpg", resolvedUrl); + } + + [Fact] + public void ResolvingNonExistentResourceReturnsSameUrl() + { + // Arrange + var resourceAssetCollection = new ResourceAssetCollection([ + new ResourceAsset("image1.jpg",[])]); + + // Act + var resolvedUrl = resourceAssetCollection["image2.jpg"]; + + // Assert + Assert.Equal("image2.jpg", resolvedUrl); + } + + [Fact] + public void CanDetermineContentSpecificUrls() + { + // Arrange + var resourceAssetCollection = new ResourceAssetCollection([ + new ResourceAsset("image1.jpg",[]), + new ResourceAsset( + "image2.fingerprint.jpg", + [new ResourceAssetProperty("label", "image2.jpg")]), + ]); + + // Act + var isContentSpecificUrl1 = resourceAssetCollection.IsContentSpecificUrl("image1.jpg"); + var isContentSpecificUrl2 = resourceAssetCollection.IsContentSpecificUrl("image2.fingerprint.jpg"); + + // Assert + Assert.False(isContentSpecificUrl1); + Assert.True(isContentSpecificUrl2); + } +} diff --git a/src/Components/Endpoints/src/Assets/ImportMapDefinition.cs b/src/Components/Endpoints/src/Assets/ImportMapDefinition.cs index cf2adf81b608..c5075032f0c8 100644 --- a/src/Components/Endpoints/src/Assets/ImportMapDefinition.cs +++ b/src/Components/Endpoints/src/Assets/ImportMapDefinition.cs @@ -95,7 +95,7 @@ private static (string? integrity, string? label) GetAssetProperties(ResourceAss } else if (string.Equals(property.Name, "label", StringComparison.OrdinalIgnoreCase)) { - label = property.Name; + label = property.Value; } if (integrity != null && label != null) @@ -134,10 +134,16 @@ public static ImportMapDefinition Combine(params ImportMapDefinition[] sources) importMap._scopes ??= []; foreach (var (key, value) in item.Scopes) { - foreach (var (scopeKey, scopeValue) in value) + if (importMap._scopes.TryGetValue(key, out var existingScope) && existingScope != null) { - importMap._scopes[key] ??= new Dictionary(); - ((Dictionary)importMap._scopes[key])[scopeKey] = scopeValue; + foreach (var (scopeKey, scopeValue) in value) + { + ((Dictionary)importMap._scopes[key])[scopeKey] = scopeValue; + } + } + else + { + importMap._scopes[key] = new Dictionary(value); } } } diff --git a/src/Components/Endpoints/test/Assets/ImportMapDefinitionTest.cs b/src/Components/Endpoints/test/Assets/ImportMapDefinitionTest.cs new file mode 100644 index 000000000000..6b4c69105b44 --- /dev/null +++ b/src/Components/Endpoints/test/Assets/ImportMapDefinitionTest.cs @@ -0,0 +1,200 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Components.Endpoints.Assets; + +public class ImportMapDefinitionTest +{ + [Fact] + public void CanCreate_Basic_ImportMapDefinition() + { + // Arrange + var expectedJson = """ + { + "imports": { + "jquery": "https://cdn.example.com/jquery.js" + } + } + """; + + var importMapDefinition = new ImportMapDefinition( + new Dictionary + { + { "jquery", "https://cdn.example.com/jquery.js" }, + }, + null, + null + ); + + // Assert + Assert.Equal(expectedJson, importMapDefinition.ToJson()); + } + + [Fact] + public void CanCreate_Scoped_ImportMapDefinition() + { + // Arrange + var expectedJson = """ + { + "scopes": { + "/scoped/": { + "jquery": "https://cdn.example.com/jquery.js" + } + } + } + """; + + var importMapDefinition = new ImportMapDefinition( + null, + new Dictionary> + { + ["/scoped/"] = new Dictionary + { + { "jquery", "https://cdn.example.com/jquery.js" }, + } + }, + null); + + // Assert + Assert.Equal(expectedJson, importMapDefinition.ToJson()); + } + + [Fact] + public void CanCreate_ImportMap_WithIntegrity() + { + // Arrange + var expectedJson = """ + { + "imports": { + "jquery": "https://cdn.example.com/jquery.js" + }, + "integrity": { + "https://cdn.example.com/jquery.js": "sha384-abc123" + } + } + """; + + var importMapDefinition = new ImportMapDefinition( + new Dictionary + { + { "jquery", "https://cdn.example.com/jquery.js" }, + }, + null, + new Dictionary + { + { "https://cdn.example.com/jquery.js", "sha384-abc123" }, + }); + + // Assert + Assert.Equal(expectedJson, importMapDefinition.ToJson()); + } + + [Fact] + public void CanBuildImportMap_FromResourceCollection() + { + // Arrange + var resourceAssetCollection = new ResourceAssetCollection( + [ + new ResourceAsset( + "jquery.fingerprint.js", + [ + new ResourceAssetProperty("integrity", "sha384-abc123"), + new ResourceAssetProperty("label", "jquery.js"), + ]) + ]); + + var expectedJson = """ + { + "imports": { + "jquery.js": "jquery.fingerprint.js" + }, + "integrity": { + "jquery.fingerprint.js": "sha384-abc123" + } + } + """; + + // Act + var importMapDefinition = ImportMapDefinition.FromResourceCollection(resourceAssetCollection); + + // Assert + Assert.Equal(expectedJson, importMapDefinition.ToJson()); + } + + [Fact] + public void CanCombine_ImportMaps() + { + // Arrange + var firstImportMap = new ImportMapDefinition( + new Dictionary + { + { "jquery", "https://cdn.example.com/jquery.js" }, + }, + new Dictionary> + { + ["/legacy/"] = new Dictionary + { + { "jquery", "https://legacy.example.com/jquery.js" }, + } + }, + new Dictionary + { + { "https://cdn.example.com/jquery.js", "sha384-abc123" }, + }); + + var secondImportMap = new ImportMapDefinition( + new Dictionary + { + { "react", "https://cdn.example.com/react.js" }, + { "jquery", "https://updated.example.com/jquery.js" } + }, + new Dictionary> + { + ["/scoped/"] = new Dictionary + { + { "jquery", "https://cdn.example.com/jquery.js" }, + }, + ["/legacy/"] = new Dictionary + { + { "jquery", "https://updated.example.com/jquery.js" }, + } + }, + new Dictionary + { + { "https://cdn.example.com/react.js", "sha384-def456" }, + }); + + var expectedImportMap = """ + { + "imports": { + "jquery": "https://updated.example.com/jquery.js", + "react": "https://cdn.example.com/react.js" + }, + "scopes": { + "/legacy/": { + "jquery": "https://updated.example.com/jquery.js" + }, + "/scoped/": { + "jquery": "https://cdn.example.com/jquery.js" + } + }, + "integrity": { + "https://cdn.example.com/jquery.js": "sha384-abc123", + "https://cdn.example.com/react.js": "sha384-def456" + } + } + """; + + // Act + var combinedImportMap = ImportMapDefinition.Combine(firstImportMap, secondImportMap); + + // Assert + Assert.Equal(expectedImportMap, combinedImportMap.ToJson()); + } +} diff --git a/src/Components/Endpoints/test/ImportMapTest.cs b/src/Components/Endpoints/test/ImportMapTest.cs new file mode 100644 index 000000000000..de49830d7626 --- /dev/null +++ b/src/Components/Endpoints/test/ImportMapTest.cs @@ -0,0 +1,393 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.ObjectModel; +using System.Runtime.ExceptionServices; +using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Microsoft.AspNetCore.Components.Endpoints; + +public class ImportMapTest +{ + private readonly ImportMap _importMap; + private readonly TestRenderer _renderer; + + public ImportMapTest() + { + var services = new ServiceCollection(); + services.AddSingleton(NullLoggerFactory.Instance); + var serviceProvider = services.BuildServiceProvider(); + + _renderer = new TestRenderer(serviceProvider) + { + ShouldHandleExceptions = true + }; + _importMap = (ImportMap)_renderer.InstantiateComponent(); + } + + //[Fact] + //public async Task CanRunOnNavigateAsync() + //{ + // // Arrange + // var called = false; + // Action OnNavigateAsync = async (NavigationContext args) => + // { + // await Task.CompletedTask; + // called = true; + // }; + // _importMap.OnNavigateAsync = new EventCallback(null, OnNavigateAsync); + + // // Act + // await _renderer.Dispatcher.InvokeAsync(() => _importMap.RunOnNavigateAsync("http://example.com/jan", false)); + + // // Assert + // Assert.True(called); + //} + + [Fact] + public async Task CanRenderImportMap() + { + // Arrange + var importMap = new ImportMap(); + var importMapDefinition = new ImportMapDefinition( + new Dictionary + { + { "jquery", "https://code.jquery.com/jquery-3.5.1.min.js" }, + { "bootstrap", "https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" } + }, + new Dictionary> + { + ["development"] = new Dictionary + { + { "jquery", "https://code.jquery.com/jquery-3.5.1.js" }, + { "bootstrap", "https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.js" } + }.AsReadOnly() + }, + new Dictionary + { + { "https://code.jquery.com/jquery-3.5.1.js", "sha384-jquery" }, + { "https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.js", "sha256-bootstrap" } + }); + + importMap.ImportMapDefinition = importMapDefinition; + var id = _renderer.AssignRootComponentId(importMap); + // Act + await _renderer.Dispatcher.InvokeAsync(() => _renderer.RenderRootComponent(id)); + + // Assert + var frames = _renderer.GetCurrentRenderTreeFrames(id); + Assert.Equal(3, frames.Count); + Assert.Equal(RenderTreeFrameType.Element, frames.Array[0].FrameType); + Assert.Equal("script", frames.Array[0].ElementName); + Assert.Equal(RenderTreeFrameType.Attribute, frames.Array[1].FrameType); + Assert.Equal("type", frames.Array[1].AttributeName); + Assert.Equal("importmap", frames.Array[1].AttributeValue); + Assert.Equal(RenderTreeFrameType.Markup, frames.Array[2].FrameType); + Assert.Equal(importMapDefinition.ToJson(), frames.Array[2].TextContent); + } + + [Fact] + public async Task ResolvesImportMap_FromHttpContext() + { + // Arrange + var importMap = new ImportMap(); + var importMapDefinition = new ImportMapDefinition( + new Dictionary + { + { "jquery", "https://code.jquery.com/jquery-3.5.1.min.js" }, + { "bootstrap", "https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" } + }, + new Dictionary> + { + ["development"] = new Dictionary + { + { "jquery", "https://code.jquery.com/jquery-3.5.1.js" }, + { "bootstrap", "https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.js" } + }.AsReadOnly() + }, + new Dictionary + { + { "https://code.jquery.com/jquery-3.5.1.js", "sha384-jquery" }, + { "https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.js", "sha256-bootstrap" } + }); + + var id = _renderer.AssignRootComponentId(importMap); + var context = new DefaultHttpContext(); + context.SetEndpoint(new Endpoint((ctx) => Task.CompletedTask, new EndpointMetadataCollection(importMapDefinition), "Test")); + importMap.HttpContext = context; + + // Act + await _renderer.Dispatcher.InvokeAsync(() => _renderer.RenderRootComponent(id)); + + // Assert + var frames = _renderer.GetCurrentRenderTreeFrames(id); + Assert.Equal(3, frames.Count); + Assert.Equal(RenderTreeFrameType.Element, frames.Array[0].FrameType); + Assert.Equal("script", frames.Array[0].ElementName); + Assert.Equal(RenderTreeFrameType.Attribute, frames.Array[1].FrameType); + Assert.Equal("type", frames.Array[1].AttributeName); + Assert.Equal("importmap", frames.Array[1].AttributeValue); + Assert.Equal(RenderTreeFrameType.Markup, frames.Array[2].FrameType); + Assert.Equal(importMapDefinition.ToJson(), frames.Array[2].TextContent); + } + + [Fact] + public async Task Rerenders_WhenImportmapChanges() + { + // Arrange + var importMap = new ImportMap(); + var importMapDefinition = new ImportMapDefinition( + new Dictionary + { + { "jquery", "https://code.jquery.com/jquery-3.5.1.min.js" }, + { "bootstrap", "https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" } + }, + new Dictionary> + { + ["development"] = new Dictionary + { + { "jquery", "https://code.jquery.com/jquery-3.5.1.js" }, + { "bootstrap", "https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.js" } + }.AsReadOnly() + }, + new Dictionary + { + { "https://code.jquery.com/jquery-3.5.1.js", "sha384-jquery" }, + { "https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.js", "sha256-bootstrap" } + }); + + var otherImportMapDefinition = new ImportMapDefinition( + new Dictionary + { + { "jquery", "./jquery-3.5.1.js" }, + { "bootstrap", "./bootstrap/4.5.2/js/bootstrap.min.js" } + }, + ReadOnlyDictionary>.Empty, + ReadOnlyDictionary.Empty); + + var id = _renderer.AssignRootComponentId(importMap); + var context = new DefaultHttpContext(); + context.SetEndpoint(new Endpoint((ctx) => Task.CompletedTask, new EndpointMetadataCollection(importMapDefinition), "Test")); + importMap.HttpContext = context; + + // Act + await _renderer.Dispatcher.InvokeAsync(() => _renderer.RenderRootComponent(id)); + + var component = importMap as IComponent; + await _renderer.Dispatcher.InvokeAsync(async () => + { + await component.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(ImportMap.ImportMapDefinition), otherImportMapDefinition } + })); + }); + + await _renderer.Dispatcher.InvokeAsync(_renderer.ProcessPendingRender); + + // Assert + var frames = _renderer.GetCurrentRenderTreeFrames(id); + Assert.Equal(3, frames.Count); + Assert.Equal(RenderTreeFrameType.Element, frames.Array[0].FrameType); + Assert.Equal("script", frames.Array[0].ElementName); + Assert.Equal(RenderTreeFrameType.Attribute, frames.Array[1].FrameType); + Assert.Equal("type", frames.Array[1].AttributeName); + Assert.Equal("importmap", frames.Array[1].AttributeValue); + Assert.Equal(RenderTreeFrameType.Markup, frames.Array[2].FrameType); + Assert.Equal(otherImportMapDefinition.ToJson(), frames.Array[2].TextContent); + } + + [Fact] + public async Task DoesNot_Rerender_WhenImportmap_DoesNotChange() + { + // Arrange + var importMap = new ImportMap(); + var importMapDefinition = new ImportMapDefinition( + new Dictionary + { + { "jquery", "https://code.jquery.com/jquery-3.5.1.min.js" }, + { "bootstrap", "https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" } + }, + new Dictionary> + { + ["development"] = new Dictionary + { + { "jquery", "https://code.jquery.com/jquery-3.5.1.js" }, + { "bootstrap", "https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.js" } + }.AsReadOnly() + }, + new Dictionary + { + { "https://code.jquery.com/jquery-3.5.1.js", "sha384-jquery" }, + { "https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.js", "sha256-bootstrap" } + }); + + var id = _renderer.AssignRootComponentId(importMap); + var context = new DefaultHttpContext(); + context.SetEndpoint(new Endpoint((ctx) => Task.CompletedTask, new EndpointMetadataCollection(importMapDefinition), "Test")); + importMap.HttpContext = context; + + // Act + await _renderer.Dispatcher.InvokeAsync(() => _renderer.RenderRootComponent(id)); + + var component = importMap as IComponent; + await _renderer.Dispatcher.InvokeAsync(async () => + { + await component.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(ImportMap.ImportMapDefinition), importMapDefinition } + })); + }); + + await _renderer.Dispatcher.InvokeAsync(_renderer.ProcessPendingRender); + + // Assert + Assert.Equal(1, _renderer.CapturedBatch.UpdatedComponents.Count); + Assert.Equal(0, _renderer.CapturedBatch.UpdatedComponents.Array[0].Edits.Count); + + var frames = _renderer.GetCurrentRenderTreeFrames(id); + Assert.Equal(3, frames.Count); + Assert.Equal(RenderTreeFrameType.Element, frames.Array[0].FrameType); + Assert.Equal("script", frames.Array[0].ElementName); + Assert.Equal(RenderTreeFrameType.Attribute, frames.Array[1].FrameType); + Assert.Equal("type", frames.Array[1].AttributeName); + Assert.Equal("importmap", frames.Array[1].AttributeValue); + Assert.Equal(RenderTreeFrameType.Markup, frames.Array[2].FrameType); + Assert.Equal(importMapDefinition.ToJson(), frames.Array[2].TextContent); + } + + public class TestRenderer : Renderer + { + public TestRenderer(IServiceProvider serviceProvider) : base(serviceProvider, NullLoggerFactory.Instance) + { + Dispatcher = Dispatcher.CreateDefault(); + } + + public TestRenderer(IServiceProvider serviceProvider, IComponentActivator componentActivator) + : base(serviceProvider, NullLoggerFactory.Instance, componentActivator) + { + Dispatcher = Dispatcher.CreateDefault(); + } + + public override Dispatcher Dispatcher { get; } + + public Action OnExceptionHandled { get; set; } + + public Action OnUpdateDisplay { get; set; } + + public Action OnUpdateDisplayComplete { get; set; } + + public List HandledExceptions { get; } = new List(); + + public bool ShouldHandleExceptions { get; set; } + + public Task NextRenderResultTask { get; set; } = Task.CompletedTask; + + public RenderBatch CapturedBatch { get; set; } + + private HashSet UndisposedComponentStates { get; } = new(); + + protected override Task UpdateDisplayAsync(in RenderBatch renderBatch) + { + CapturedBatch = renderBatch; + return Task.CompletedTask; + } + + public new int AssignRootComponentId(IComponent component) + => base.AssignRootComponentId(component); + + public new void RemoveRootComponent(int componentId) + => base.RemoveRootComponent(componentId); + + public new ArrayRange GetCurrentRenderTreeFrames(int componentId) + => base.GetCurrentRenderTreeFrames(componentId); + + public void RenderRootComponent(int componentId, ParameterView? parameters = default) + { + var task = Dispatcher.InvokeAsync(() => base.RenderRootComponentAsync(componentId, parameters ?? ParameterView.Empty)); + UnwrapTask(task); + } + + public new Task RenderRootComponentAsync(int componentId) + => Dispatcher.InvokeAsync(() => base.RenderRootComponentAsync(componentId)); + + public new Task RenderRootComponentAsync(int componentId, ParameterView parameters) + => Dispatcher.InvokeAsync(() => base.RenderRootComponentAsync(componentId, parameters)); + + public Task DispatchEventAsync(ulong eventHandlerId, EventArgs args) + => Dispatcher.InvokeAsync(() => base.DispatchEventAsync(eventHandlerId, null, args)); + + public new Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo eventFieldInfo, EventArgs args) + => Dispatcher.InvokeAsync(() => base.DispatchEventAsync(eventHandlerId, eventFieldInfo, args)); + + private static Task UnwrapTask(Task task) + { + // This should always be run synchronously + Assert.True(task.IsCompleted); + if (task.IsFaulted) + { + var exception = task.Exception.Flatten().InnerException; + while (exception is AggregateException e) + { + exception = e.InnerException; + } + + ExceptionDispatchInfo.Capture(exception).Throw(); + } + + return task; + } + + public IComponent InstantiateComponent() + => InstantiateComponent(typeof(T)); + + protected override void HandleException(Exception exception) + { + if (!ShouldHandleExceptions) + { + ExceptionDispatchInfo.Capture(exception).Throw(); + } + + HandledExceptions.Add(exception); + OnExceptionHandled?.Invoke(); + } + + public new void ProcessPendingRender() + => base.ProcessPendingRender(); + + protected override ComponentState CreateComponentState(int componentId, IComponent component, ComponentState parentComponentState) + => new TestRendererComponentState(this, componentId, component, parentComponentState); + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (UndisposedComponentStates.Count > 0) + { + throw new InvalidOperationException("Did not dispose all the ComponentState instances. This could lead to ArrayBuffer not returning buffers to its pool."); + } + } + + class TestRendererComponentState : ComponentState, IAsyncDisposable + { + private readonly TestRenderer _renderer; + + public TestRendererComponentState(Renderer renderer, int componentId, IComponent component, ComponentState parentComponentState) + : base(renderer, componentId, component, parentComponentState) + { + _renderer = (TestRenderer)renderer; + _renderer.UndisposedComponentStates.Add(this); + } + + public override ValueTask DisposeAsync() + { + _renderer.UndisposedComponentStates.Remove(this); + return base.DisposeAsync(); + } + } + } +} diff --git a/src/Mvc/Mvc.RazorPages/test/Builder/PageActionEndpointConventionBuilderResourceCollectionExtensionsTest.cs b/src/Mvc/Mvc.RazorPages/test/Builder/PageActionEndpointConventionBuilderResourceCollectionExtensionsTest.cs new file mode 100644 index 000000000000..7e24b4dca64d --- /dev/null +++ b/src/Mvc/Mvc.RazorPages/test/Builder/PageActionEndpointConventionBuilderResourceCollectionExtensionsTest.cs @@ -0,0 +1,287 @@ +// 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; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Razor.Compilation; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.Razor.Hosting; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; + +namespace Microsoft.AspNetCore.Builder; + +public class PageActionEndpointConventionBuilderResourceCollectionExtensionsTest +{ + [Fact] + public void WithResourceCollection_AddsEmptyResourceCollection_ToEndpoints_NoStaticAssetsMapped() + { + // Arrange + var endpointBuilder = new TestEndpointRouteBuilder(); + var builder = endpointBuilder.MapRazorPages(); + + // Act + builder.WithResourceCollection(); + + // Assert + Assert.All(endpointBuilder.DataSources.First().Endpoints, e => + { + var metadata = e.Metadata.GetMetadata(); + Assert.NotNull(metadata); + var list = Assert.IsAssignableFrom>(metadata); + Assert.Equal(0, list.Count); + }); + } + + [Fact] + public void WithResourceCollection_AddsEmptyResourceCollection_ToEndpoints_NoMatchingStaticAssetsMapped() + { + // Arrange + var endpointBuilder = new TestEndpointRouteBuilder(); + endpointBuilder.MapStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); + var builder = endpointBuilder.MapRazorPages(); + + // Act + builder.WithResourceCollection(); + + // Assert + Assert.All(endpointBuilder.DataSources.Skip(1).First().Endpoints, e => + { + var metadata = e.Metadata.GetMetadata(); + Assert.NotNull(metadata); + var list = Assert.IsAssignableFrom>(metadata); + Assert.Equal(0, list.Count); + }); + } + + [Fact] + public void WithResourceCollection_AddsResourceCollection_ToEndpoints_NamedManifest() + { + // Arrange + var endpointBuilder = new TestEndpointRouteBuilder(); + endpointBuilder.MapStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); + var builder = endpointBuilder.MapRazorPages(); + + // Act + builder.WithResourceCollection("TestManifests/Test.staticwebassets.endpoints.json"); + + // Assert + Assert.All(endpointBuilder.DataSources.Skip(1).First().Endpoints, e => + { + var metadata = e.Metadata.GetMetadata(); + Assert.NotNull(metadata); + var list = Assert.IsAssignableFrom>(metadata); + Assert.Equal(1, list.Count); + Assert.Equal("named.css", list[0].Url); + }); + } + + [Fact] + public void WithResourceCollection_AddsResourceCollection_ToEndpoints_DefaultManifest() + { + // Arrange + var endpointBuilder = new TestEndpointRouteBuilder(); + endpointBuilder.MapStaticAssets(); + var builder = endpointBuilder.MapRazorPages(); + + // Act + builder.WithResourceCollection(); + + // Assert + Assert.All(endpointBuilder.DataSources.Skip(1).First().Endpoints, e => + { + var metadata = e.Metadata.GetMetadata(); + Assert.NotNull(metadata); + var list = Assert.IsAssignableFrom>(metadata); + Assert.Equal(1, list.Count); + Assert.Equal("default.css", list[0].Url); + }); + } + + [Fact] + public void WithResourceCollection_AddsDefaultResourceCollectionToEndpoints_WhenNoManifestProvided_EvenIfManyAvailable() + { + // Arrange + var endpointBuilder = new TestEndpointRouteBuilder(); + endpointBuilder.MapStaticAssets(); + endpointBuilder.MapStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); + var builder = endpointBuilder.MapRazorPages(); + + // Act + builder.WithResourceCollection(); + + // Assert + Assert.All(endpointBuilder.DataSources.Skip(2).First().Endpoints, e => + { + var metadata = e.Metadata.GetMetadata(); + Assert.NotNull(metadata); + var list = Assert.IsAssignableFrom>(metadata); + Assert.Equal(1, list.Count); + Assert.Equal("default.css", list[0].Url); + }); + } + + [Fact] + public void WithResourceCollection_AddsMatchingResourceCollectionToEndpoints_WhenExplicitManifestProvided_EvenIfManyAvailable() + { + // Arrange + var endpointBuilder = new TestEndpointRouteBuilder(); + endpointBuilder.MapStaticAssets(); + endpointBuilder.MapStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); + var builder = endpointBuilder.MapRazorPages(); + + // Act + builder.WithResourceCollection("TestManifests/Test.staticwebassets.endpoints.json"); + + // Assert + Assert.All(endpointBuilder.DataSources.Skip(2).First().Endpoints, e => + { + var metadata = e.Metadata.GetMetadata(); + Assert.NotNull(metadata); + var list = Assert.IsAssignableFrom>(metadata); + Assert.Equal(1, list.Count); + Assert.Equal("named.css", list[0].Url); + }); + } + + [Fact] + public void WithResourceCollection_AddsCollectionFromGroup_WhenMappedInsideAnEndpointGroup() + { + // Arrange + var endpointBuilder = new TestEndpointRouteBuilder(); + endpointBuilder.MapStaticAssets(); + + var group = endpointBuilder.MapGroup("/group"); + group.MapStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); + var builder = group.MapRazorPages(); + + // Act + builder.WithResourceCollection("TestManifests/Test.staticwebassets.endpoints.json"); + + // Assert + var groupEndpoints = Assert.IsAssignableFrom(group).DataSources; + Assert.All(groupEndpoints.Skip(1).First().Endpoints, e => + { + var metadata = e.Metadata.GetMetadata(); + Assert.NotNull(metadata); + var list = Assert.IsAssignableFrom>(metadata); + Assert.Equal(1, list.Count); + Assert.Equal("named.css", list[0].Url); + }); + } + + [Fact] + public void WithResourceCollection_AddsEmptyCollectionFromGroup_WhenMappingNotFound_InsideGroup() + { + // Arrange + var endpointBuilder = new TestEndpointRouteBuilder(); + endpointBuilder.MapStaticAssets(); + + var group = endpointBuilder.MapGroup("/group"); + group.MapStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); + var builder = group.MapRazorPages(); + + // Act + builder.WithResourceCollection(); + + // Assert + var groupEndpoints = Assert.IsAssignableFrom(group).DataSources; + Assert.All(groupEndpoints.Skip(1).First().Endpoints, e => + { + var metadata = e.Metadata.GetMetadata(); + Assert.NotNull(metadata); + var list = Assert.IsAssignableFrom>(metadata); + Assert.Equal(0, list.Count); + }); + } + + private class TestEndpointRouteBuilder : IEndpointRouteBuilder + { + private readonly ApplicationBuilder _applicationBuilder; + + public TestEndpointRouteBuilder() + { + _applicationBuilder = new ApplicationBuilder(ServiceProvider); + } + + public IServiceProvider ServiceProvider { get; } = CreateServiceProvider(); + + private static IServiceProvider CreateServiceProvider() + { + var collection = new ServiceCollection(); + collection.AddSingleton(new ConfigurationBuilder().Build()); + collection.AddSingleton(new TestWebHostEnvironment()); + collection.AddSingleton(new ApplicationPartManager()); + collection.AddSingleton(new DiagnosticListener("Microsoft.AspNetCore")); + collection.AddSingleton(new TestDiagnosticSource()); + collection.AddLogging(); + collection.AddOptions(); + collection.AddMvc() + .ConfigureApplicationPartManager(apm => + { + apm.FeatureProviders.Clear(); + apm.FeatureProviders.Add(new TestRazorPagesFeatureProvider()); + }); + return collection.BuildServiceProvider(); + } + + public ICollection DataSources { get; } = []; + + public IApplicationBuilder CreateApplicationBuilder() + { + return _applicationBuilder.New(); + } + + private class TestRazorPagesFeatureProvider : IApplicationFeatureProvider + { + public void PopulateFeature(IEnumerable parts, ViewsFeature feature) + { + feature.ViewDescriptors.Clear(); + feature.ViewDescriptors.Add(new CompiledViewDescriptor(TestRazorCompiledItem.CreateForPage(typeof(Index), "/Pages/Index.cshtml"))); + } + } + + [Route("/")] + private class Index : PageBase + { + public object Model { get; set; } + + public override Task ExecuteAsync() + { + throw new NotImplementedException(); + } + } + + private class TestWebHostEnvironment : IWebHostEnvironment + { + public string ApplicationName { get; set; } = "TestApplication"; + public string EnvironmentName { get; set; } = "TestEnvironment"; + public string WebRootPath { get; set; } = ""; + public IFileProvider WebRootFileProvider { get => ContentRootFileProvider; set { } } + public string ContentRootPath { get; set; } = Directory.GetCurrentDirectory(); + public IFileProvider ContentRootFileProvider { get; set; } = CreateTestFileProvider(); + + private static TestFileProvider CreateTestFileProvider() + { + var provider = new TestFileProvider(); + provider.AddFile("site.css", "body { color: red; }"); + return provider; + } + } + + private class TestDiagnosticSource : DiagnosticSource + { + public override bool IsEnabled(string name) + { + return false; + } + + public override void Write(string name, object value) { } + } + } +} + diff --git a/src/Mvc/Mvc.RazorPages/test/Microsoft.AspNetCore.Mvc.RazorPages.Test.csproj b/src/Mvc/Mvc.RazorPages/test/Microsoft.AspNetCore.Mvc.RazorPages.Test.csproj index ea72cc610342..c9e51798bea6 100644 --- a/src/Mvc/Mvc.RazorPages/test/Microsoft.AspNetCore.Mvc.RazorPages.Test.csproj +++ b/src/Mvc/Mvc.RazorPages/test/Microsoft.AspNetCore.Mvc.RazorPages.Test.csproj @@ -1,4 +1,4 @@ - + $(DefaultNetCoreTargetFramework) @@ -8,8 +8,14 @@ + + + + + + diff --git a/src/Mvc/Mvc.RazorPages/test/TestApplication.staticwebassets.endpoints.json b/src/Mvc/Mvc.RazorPages/test/TestApplication.staticwebassets.endpoints.json new file mode 100644 index 000000000000..586241880352 --- /dev/null +++ b/src/Mvc/Mvc.RazorPages/test/TestApplication.staticwebassets.endpoints.json @@ -0,0 +1,17 @@ +{ + "Version": 1, + "Endpoints": [ + { + "Route": "default.css", + "AssetFile": "default.css", + "Selectors": [], + "EndpointProperties": [], + "ResponseHeaders": [ + { + "Name": "ETag", + "Value": "\"Fake\"" + } + ] + } + ] +} diff --git a/src/Mvc/Mvc.RazorPages/test/TestManifests/Test.staticwebassets.endpoints.json b/src/Mvc/Mvc.RazorPages/test/TestManifests/Test.staticwebassets.endpoints.json new file mode 100644 index 000000000000..77815b7c379c --- /dev/null +++ b/src/Mvc/Mvc.RazorPages/test/TestManifests/Test.staticwebassets.endpoints.json @@ -0,0 +1,17 @@ +{ + "Version": 1, + "Endpoints": [ + { + "Route": "named.css", + "AssetFile": "named.css", + "Selectors": [], + "EndpointProperties": [], + "ResponseHeaders": [ + { + "Name": "ETag", + "Value": "\"Fake\"" + } + ] + } + ] +} diff --git a/src/Mvc/Mvc.ViewFeatures/test/Builder/ControllerActionEndpointConventionBuilderResourceCollectionExtensionsTest.cs b/src/Mvc/Mvc.ViewFeatures/test/Builder/ControllerActionEndpointConventionBuilderResourceCollectionExtensionsTest.cs new file mode 100644 index 000000000000..61ccf151caf1 --- /dev/null +++ b/src/Mvc/Mvc.ViewFeatures/test/Builder/ControllerActionEndpointConventionBuilderResourceCollectionExtensionsTest.cs @@ -0,0 +1,362 @@ +// 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; +using System.Reflection; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; + +namespace Microsoft.AspNetCore.Builder; + +public class ControllerActionEndpointConventionBuilderResourceCollectionExtensionsTest +{ + [Fact] + public void WithResourceCollection_AddsEmptyResourceCollection_ToEndpoints_NoStaticAssetsMapped() + { + // Arrange + var endpointBuilder = new TestEndpointRouteBuilder(); + var builder = endpointBuilder.MapControllers(); + + // Act + builder.WithResourceCollection(); + + // Assert + Assert.All(endpointBuilder.DataSources.First().Endpoints, e => + { + var apiController = e.Metadata.GetMetadata(); + if (apiController != null) + { + var metadata = e.Metadata.GetMetadata(); + Assert.Null(metadata); + } + else + { + var metadata = e.Metadata.GetMetadata(); + Assert.NotNull(metadata); + var list = Assert.IsAssignableFrom>(metadata); + Assert.Equal(0, list.Count); + } + }); + } + + [Fact] + public void WithResourceCollection_AddsEmptyResourceCollection_ToEndpoints_NoMatchingStaticAssetsMapped() + { + // Arrange + var endpointBuilder = new TestEndpointRouteBuilder(); + endpointBuilder.MapStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); + var builder = endpointBuilder.MapControllers(); + + // Act + builder.WithResourceCollection(); + + // Assert + Assert.All(endpointBuilder.DataSources.Skip(1).First().Endpoints, e => + { + var apiController = e.Metadata.GetMetadata(); + if (apiController != null) + { + var metadata = e.Metadata.GetMetadata(); + Assert.Null(metadata); + } + else + { + var metadata = e.Metadata.GetMetadata(); + Assert.NotNull(metadata); + var list = Assert.IsAssignableFrom>(metadata); + Assert.Equal(0, list.Count); + } + }); + } + + [Fact] + public void WithResourceCollection_AddsResourceCollection_ToEndpoints_NamedManifest() + { + // Arrange + var endpointBuilder = new TestEndpointRouteBuilder(); + endpointBuilder.MapStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); + var builder = endpointBuilder.MapControllers(); + + // Act + builder.WithResourceCollection("TestManifests/Test.staticwebassets.endpoints.json"); + + // Assert + var endpoints = endpointBuilder.DataSources.Skip(1).First().Endpoints; + Assert.All(endpoints, e => + { + var apiController = e.Metadata.GetMetadata(); + if (apiController != null) + { + var metadata = e.Metadata.GetMetadata(); + Assert.Null(metadata); + } + else + { + var metadata = e.Metadata.GetMetadata(); + Assert.NotNull(metadata); + var list = Assert.IsAssignableFrom>(metadata); + Assert.Equal(1, list.Count); + Assert.Equal("named.css", list[0].Url); + } + }); + } + + [Fact] + public void WithResourceCollection_AddsResourceCollection_ToEndpoints_DefaultManifest() + { + // Arrange + var endpointBuilder = new TestEndpointRouteBuilder(); + endpointBuilder.MapStaticAssets(); + var builder = endpointBuilder.MapControllers(); + + // Act + builder.WithResourceCollection(); + + // Assert + Assert.All(endpointBuilder.DataSources.Skip(1).First().Endpoints, e => + { + var apiController = e.Metadata.GetMetadata(); + if (apiController != null) + { + var metadata = e.Metadata.GetMetadata(); + Assert.Null(metadata); + } + else + { + var metadata = e.Metadata.GetMetadata(); + Assert.NotNull(metadata); + var list = Assert.IsAssignableFrom>(metadata); + Assert.Equal(1, list.Count); + Assert.Equal("default.css", list[0].Url); + } + }); + } + + [Fact] + public void WithResourceCollection_AddsDefaultResourceCollectionToEndpoints_WhenNoManifestProvided_EvenIfManyAvailable() + { + // Arrange + var endpointBuilder = new TestEndpointRouteBuilder(); + endpointBuilder.MapStaticAssets(); + endpointBuilder.MapStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); + var builder = endpointBuilder.MapControllers(); + + // Act + builder.WithResourceCollection(); + + // Assert + Assert.All(endpointBuilder.DataSources.Skip(2).First().Endpoints, e => + { + var apiController = e.Metadata.GetMetadata(); + if (apiController != null) + { + var metadata = e.Metadata.GetMetadata(); + Assert.Null(metadata); + } + else + { + var metadata = e.Metadata.GetMetadata(); + Assert.NotNull(metadata); + var list = Assert.IsAssignableFrom>(metadata); + Assert.Equal(1, list.Count); + Assert.Equal("default.css", list[0].Url); + } + }); + } + + [Fact] + public void WithResourceCollection_AddsMatchingResourceCollectionToEndpoints_WhenExplicitManifestProvided_EvenIfManyAvailable() + { + // Arrange + var endpointBuilder = new TestEndpointRouteBuilder(); + endpointBuilder.MapStaticAssets(); + endpointBuilder.MapStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); + var builder = endpointBuilder.MapControllers(); + + // Act + builder.WithResourceCollection("TestManifests/Test.staticwebassets.endpoints.json"); + + // Assert + Assert.All(endpointBuilder.DataSources.Skip(2).First().Endpoints, e => + { + var apiController = e.Metadata.GetMetadata(); + if (apiController != null) + { + var metadata = e.Metadata.GetMetadata(); + Assert.Null(metadata); + } + else + { + var metadata = e.Metadata.GetMetadata(); + Assert.NotNull(metadata); + var list = Assert.IsAssignableFrom>(metadata); + Assert.Equal(1, list.Count); + Assert.Equal("named.css", list[0].Url); + } + }); + } + + [Fact] + public void WithResourceCollection_AddsCollectionFromGroup_WhenMappedInsideAnEndpointGroup() + { + // Arrange + var endpointBuilder = new TestEndpointRouteBuilder(); + endpointBuilder.MapStaticAssets(); + + var group = endpointBuilder.MapGroup("/group"); + group.MapStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); + var builder = group.MapControllers(); + + // Act + builder.WithResourceCollection("TestManifests/Test.staticwebassets.endpoints.json"); + + // Assert + var groupEndpoints = Assert.IsAssignableFrom(group).DataSources; + Assert.All(groupEndpoints.Skip(1).First().Endpoints, e => + { + var apiController = e.Metadata.GetMetadata(); + if (apiController != null) + { + var metadata = e.Metadata.GetMetadata(); + Assert.Null(metadata); + } + else + { + var metadata = e.Metadata.GetMetadata(); + Assert.NotNull(metadata); + var list = Assert.IsAssignableFrom>(metadata); + Assert.Equal(1, list.Count); + Assert.Equal("named.css", list[0].Url); + } + }); + } + + [Fact] + public void WithResourceCollection_AddsEmptyCollectionFromGroup_WhenMappingNotFound_InsideGroup() + { + // Arrange + var endpointBuilder = new TestEndpointRouteBuilder(); + endpointBuilder.MapStaticAssets(); + + var group = endpointBuilder.MapGroup("/group"); + group.MapStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); + var builder = group.MapControllers(); + + // Act + builder.WithResourceCollection(); + + // Assert + var groupEndpoints = Assert.IsAssignableFrom(group).DataSources; + Assert.All(groupEndpoints.Skip(1).First().Endpoints, e => + { + var apiController = e.Metadata.GetMetadata(); + if (apiController != null) + { + var metadata = e.Metadata.GetMetadata(); + Assert.Null(metadata); + } + else + { + var metadata = e.Metadata.GetMetadata(); + Assert.NotNull(metadata); + var list = Assert.IsAssignableFrom>(metadata); + Assert.Equal(0, list.Count); + } + }); + } + + private class TestEndpointRouteBuilder : IEndpointRouteBuilder + { + private readonly ApplicationBuilder _applicationBuilder; + + public TestEndpointRouteBuilder() + { + _applicationBuilder = new ApplicationBuilder(ServiceProvider); + } + + public IServiceProvider ServiceProvider { get; } = CreateServiceProvider(); + + private static IServiceProvider CreateServiceProvider() + { + var collection = new ServiceCollection(); + collection.AddSingleton(new ConfigurationBuilder().Build()); + collection.AddSingleton(new TestWebHostEnvironment()); + collection.AddSingleton(new ApplicationPartManager()); + collection.AddSingleton(new DiagnosticListener("Microsoft.AspNetCore")); + collection.AddSingleton(new TestDiagnosticSource()); + collection.AddLogging(); + collection.AddOptions(); + collection.AddMvcCore() + .ConfigureApplicationPartManager(apm => + { + apm.FeatureProviders.Clear(); + apm.FeatureProviders.Add(new TestControllerFeatureProvider()); + }); + return collection.BuildServiceProvider(); + } + + public ICollection DataSources { get; } = []; + + public IApplicationBuilder CreateApplicationBuilder() + { + return _applicationBuilder.New(); + } + + private class TestControllerFeatureProvider : IApplicationFeatureProvider + { + public void PopulateFeature(IEnumerable parts, ControllerFeature feature) + { + feature.Controllers.Clear(); + feature.Controllers.Add(typeof(TestController).GetTypeInfo()); + feature.Controllers.Add(typeof(MyApiController).GetTypeInfo()); + } + } + + private class TestController : Controller + { + [HttpGet("/")] + public void Index() { } + } + + [ApiController] + private class MyApiController : ControllerBase + { + [HttpGet("other")] + public void Index() { } + } + + private class TestWebHostEnvironment : IWebHostEnvironment + { + public string ApplicationName { get; set; } = "TestApplication"; + public string EnvironmentName { get; set; } = "TestEnvironment"; + public string WebRootPath { get; set; } = ""; + public IFileProvider WebRootFileProvider { get => ContentRootFileProvider; set { } } + public string ContentRootPath { get; set; } = Directory.GetCurrentDirectory(); + public IFileProvider ContentRootFileProvider { get; set; } = CreateTestFileProvider(); + + private static TestFileProvider CreateTestFileProvider() + { + var provider = new TestFileProvider(); + provider.AddFile("site.css", "body { color: red; }"); + return provider; + } + } + + private class TestDiagnosticSource : DiagnosticSource + { + public override bool IsEnabled(string name) + { + return false; + } + + public override void Write(string name, object value) { } + } + } +} diff --git a/src/Mvc/Mvc.ViewFeatures/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test.csproj b/src/Mvc/Mvc.ViewFeatures/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test.csproj index 2957fc97c638..1231fc4ce0b1 100644 --- a/src/Mvc/Mvc.ViewFeatures/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test.csproj +++ b/src/Mvc/Mvc.ViewFeatures/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test.csproj @@ -4,6 +4,16 @@ $(DefaultNetCoreTargetFramework) + + + + + + + + + + diff --git a/src/Mvc/Mvc.ViewFeatures/test/TestApplication.staticwebassets.endpoints.json b/src/Mvc/Mvc.ViewFeatures/test/TestApplication.staticwebassets.endpoints.json new file mode 100644 index 000000000000..586241880352 --- /dev/null +++ b/src/Mvc/Mvc.ViewFeatures/test/TestApplication.staticwebassets.endpoints.json @@ -0,0 +1,17 @@ +{ + "Version": 1, + "Endpoints": [ + { + "Route": "default.css", + "AssetFile": "default.css", + "Selectors": [], + "EndpointProperties": [], + "ResponseHeaders": [ + { + "Name": "ETag", + "Value": "\"Fake\"" + } + ] + } + ] +} diff --git a/src/Mvc/Mvc.ViewFeatures/test/TestManifests/Test.staticwebassets.endpoints.json b/src/Mvc/Mvc.ViewFeatures/test/TestManifests/Test.staticwebassets.endpoints.json new file mode 100644 index 000000000000..77815b7c379c --- /dev/null +++ b/src/Mvc/Mvc.ViewFeatures/test/TestManifests/Test.staticwebassets.endpoints.json @@ -0,0 +1,17 @@ +{ + "Version": 1, + "Endpoints": [ + { + "Route": "named.css", + "AssetFile": "named.css", + "Selectors": [], + "EndpointProperties": [], + "ResponseHeaders": [ + { + "Name": "ETag", + "Value": "\"Fake\"" + } + ] + } + ] +} From 2fb8ba7789454852d4a9048e321d825226cf2a65 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Thu, 13 Jun 2024 20:49:36 +0200 Subject: [PATCH 15/30] More tests --- ...EndpointConventionBuilderExtensionsTest.cs | 294 ++++++++++++++++++ .../TestFileProvider/TestDirectoryContent.cs | 38 +++ .../TestFileProvider/TestFileChangeToken.cs | 32 ++ .../Builder/TestFileProvider/TestFileInfo.cs | 42 +++ .../TestFileProvider/TestFileProvider.cs | 179 +++++++++++ ...pNetCore.Components.Endpoints.Tests.csproj | 5 + ...Application.staticwebassets.endpoints.json | 17 + .../Test.staticwebassets.endpoints.json | 17 + 8 files changed, 624 insertions(+) create mode 100644 src/Components/Endpoints/test/Builder/RazorComponentsEndpointConventionBuilderExtensionsTest.cs create mode 100644 src/Components/Endpoints/test/Builder/TestFileProvider/TestDirectoryContent.cs create mode 100644 src/Components/Endpoints/test/Builder/TestFileProvider/TestFileChangeToken.cs create mode 100644 src/Components/Endpoints/test/Builder/TestFileProvider/TestFileInfo.cs create mode 100644 src/Components/Endpoints/test/Builder/TestFileProvider/TestFileProvider.cs create mode 100644 src/Components/Endpoints/test/TestApplication.staticwebassets.endpoints.json create mode 100644 src/Components/Endpoints/test/TestManifests/Test.staticwebassets.endpoints.json diff --git a/src/Components/Endpoints/test/Builder/RazorComponentsEndpointConventionBuilderExtensionsTest.cs b/src/Components/Endpoints/test/Builder/RazorComponentsEndpointConventionBuilderExtensionsTest.cs new file mode 100644 index 000000000000..a33f1f2e7008 --- /dev/null +++ b/src/Components/Endpoints/test/Builder/RazorComponentsEndpointConventionBuilderExtensionsTest.cs @@ -0,0 +1,294 @@ +// 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; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Discovery; +using Microsoft.AspNetCore.Components.Endpoints; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; + +namespace Microsoft.AspNetCore.Builder; + +public class RazorComponentsEndpointConventionBuilderExtensionsTest +{ + [Fact] + public void WithResourceCollection_DoesNotAddResourceCollection_ToEndpoints_NoStaticAssetsMapped() + { + // Arrange + var endpointBuilder = new TestEndpointRouteBuilder(); + var builder = CreateRazorComponentsAppBuilder(endpointBuilder); + + // Act + builder.WithResourceCollection(); + + // Assert + Assert.All(endpointBuilder.DataSources.Skip(1).First().Endpoints, e => + { + if (e.Metadata.GetMetadata() == null) + { + return; + } + + var metadata = e.Metadata.GetMetadata(); + Assert.Null(metadata); + }); + } + + [Fact] + public void WithResourceCollection_DoesNotAddResourceCollection_ToEndpoints_NoMatchingStaticAssetsMapped() + { + // Arrange + var endpointBuilder = new TestEndpointRouteBuilder(); + endpointBuilder.MapStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); + var builder = CreateRazorComponentsAppBuilder(endpointBuilder); + + // Act + builder.WithResourceCollection(); + + // Assert + Assert.All(endpointBuilder.DataSources.Skip(2).First().Endpoints, e => + { + var metadata = e.Metadata.GetMetadata(); + Assert.Null(metadata); + }); + } + + [Fact] + public void WithResourceCollection_AddsResourceCollection_ToEndpoints_NamedManifest() + { + // Arrange + var endpointBuilder = new TestEndpointRouteBuilder(); + endpointBuilder.MapStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); + var builder = CreateRazorComponentsAppBuilder(endpointBuilder); + + // Act + builder.WithResourceCollection("TestManifests/Test.staticwebassets.endpoints.json"); + + // Assert + Assert.All(endpointBuilder.DataSources.Skip(2).First().Endpoints, e => + { + var metadata = e.Metadata.GetMetadata(); + Assert.NotNull(metadata); + var list = Assert.IsAssignableFrom>(metadata); + Assert.Equal(1, list.Count); + Assert.Equal("named.css", list[0].Url); + }); + } + + [Fact] + public void WithResourceCollection_AddsDefaultResourceCollection_ToEndpoints_ByDefault() + { + // Arrange + var endpointBuilder = new TestEndpointRouteBuilder(); + endpointBuilder.MapStaticAssets(); + + // Act + var builder = CreateRazorComponentsAppBuilder(endpointBuilder); + + // Assert + Assert.All(endpointBuilder.DataSources.Skip(2).First().Endpoints, e => + { + var metadata = e.Metadata.GetMetadata(); + Assert.NotNull(metadata); + var list = Assert.IsAssignableFrom>(metadata); + Assert.Equal(1, list.Count); + Assert.Equal("default.css", list[0].Url); + }); + } + + [Fact] + public void WithResourceCollection_AddsResourceCollection_ToEndpoints_DefaultManifest() + { + // Arrange + var endpointBuilder = new TestEndpointRouteBuilder(); + endpointBuilder.MapStaticAssets(); + var builder = CreateRazorComponentsAppBuilder(endpointBuilder); + + // Act + builder.WithResourceCollection(); + + // Assert + Assert.All(endpointBuilder.DataSources.Skip(2).First().Endpoints, e => + { + var metadata = e.Metadata.GetMetadata(); + Assert.NotNull(metadata); + var list = Assert.IsAssignableFrom>(metadata); + Assert.Equal(1, list.Count); + Assert.Equal("default.css", list[0].Url); + }); + } + + [Fact] + public void WithResourceCollection_AddsDefaultResourceCollectionToEndpoints_WhenNoManifestProvided_EvenIfManyAvailable() + { + // Arrange + var endpointBuilder = new TestEndpointRouteBuilder(); + endpointBuilder.MapStaticAssets(); + endpointBuilder.MapStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); + + // Act + var builder = CreateRazorComponentsAppBuilder(endpointBuilder); + + // Assert + Assert.All(endpointBuilder.DataSources.Skip(3).First().Endpoints, e => + { + var metadata = e.Metadata.GetMetadata(); + Assert.NotNull(metadata); + var list = Assert.IsAssignableFrom>(metadata); + Assert.Equal(1, list.Count); + Assert.Equal("default.css", list[0].Url); + }); + } + + [Fact] + public void WithResourceCollection_AddsMatchingResourceCollectionToEndpoints_WhenExplicitManifestProvided_EvenIfManyAvailable() + { + // Arrange + var endpointBuilder = new TestEndpointRouteBuilder(); + endpointBuilder.MapStaticAssets(); + endpointBuilder.MapStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); + var builder = CreateRazorComponentsAppBuilder(endpointBuilder); + + // Act + builder.WithResourceCollection("TestManifests/Test.staticwebassets.endpoints.json"); + + // Assert + Assert.All(endpointBuilder.DataSources.Skip(3).First().Endpoints, e => + { + var metadata = e.Metadata.GetMetadata(); + Assert.NotNull(metadata); + var list = Assert.IsAssignableFrom>(metadata); + Assert.Equal(1, list.Count); + Assert.Equal("named.css", list[0].Url); + }); + } + + [Fact] + public void WithResourceCollection_AddsCollectionFromGroup_WhenMappedInsideAnEndpointGroup() + { + // Arrange + var endpointBuilder = new TestEndpointRouteBuilder(); + endpointBuilder.MapStaticAssets(); + + var group = endpointBuilder.MapGroup("/group"); + group.MapStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); + var builder = CreateRazorComponentsAppBuilder(group); + + // Act + builder.WithResourceCollection("TestManifests/Test.staticwebassets.endpoints.json"); + + // Assert + var groupEndpoints = Assert.IsAssignableFrom(group).DataSources; + Assert.All(groupEndpoints.Skip(2).First().Endpoints, e => + { + var metadata = e.Metadata.GetMetadata(); + Assert.NotNull(metadata); + var list = Assert.IsAssignableFrom>(metadata); + Assert.Equal(1, list.Count); + Assert.Equal("named.css", list[0].Url); + }); + } + + [Fact] + public void WithResourceCollection_DoesNotAddResourceCollectionFromGroup_WhenMappingNotFound_InsideGroup() + { + // Arrange + var endpointBuilder = new TestEndpointRouteBuilder(); + endpointBuilder.MapStaticAssets(); + + var group = endpointBuilder.MapGroup("/group"); + group.MapStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); + var builder = CreateRazorComponentsAppBuilder(group); + + // Act + builder.WithResourceCollection(); + + // Assert + var groupEndpoints = Assert.IsAssignableFrom(group).DataSources; + Assert.All(groupEndpoints.Skip(2).First().Endpoints, e => + { + var metadata = e.Metadata.GetMetadata(); + Assert.Null(metadata); + }); + } + + private RazorComponentsEndpointConventionBuilder CreateRazorComponentsAppBuilder(IEndpointRouteBuilder endpointBuilder) + { + var builder = endpointBuilder.MapRazorComponents(); + builder.ApplicationBuilder.AddLibrary(new AssemblyComponentLibraryDescriptor( + "App", + [new PageComponentBuilder { + PageType = typeof(App), + RouteTemplates = ["/"], + AssemblyName = "App", + }], + [] + )); + return builder; + } + + private class TestEndpointRouteBuilder : IEndpointRouteBuilder + { + private readonly ApplicationBuilder _applicationBuilder; + + public TestEndpointRouteBuilder() + { + _applicationBuilder = new ApplicationBuilder(ServiceProvider); + } + + public IServiceProvider ServiceProvider { get; } = CreateServiceProvider(); + + private static IServiceProvider CreateServiceProvider() + { + var collection = new ServiceCollection(); + collection.AddSingleton(new ConfigurationBuilder().Build()); + collection.AddSingleton(new TestWebHostEnvironment()); + collection.AddRazorComponents(); + return collection.BuildServiceProvider(); + } + + public ICollection DataSources { get; } = []; + + public IApplicationBuilder CreateApplicationBuilder() + { + return _applicationBuilder.New(); + } + + private class TestWebHostEnvironment : IWebHostEnvironment + { + public string ApplicationName { get; set; } = "TestApplication"; + public string EnvironmentName { get; set; } = "TestEnvironment"; + public string WebRootPath { get; set; } = ""; + public IFileProvider WebRootFileProvider { get => ContentRootFileProvider; set { } } + public string ContentRootPath { get; set; } = Directory.GetCurrentDirectory(); + public IFileProvider ContentRootFileProvider { get; set; } = CreateTestFileProvider(); + + private static TestFileProvider CreateTestFileProvider() + { + var provider = new TestFileProvider(); + provider.AddFile("site.css", "body { color: red; }"); + return provider; + } + } + + private class TestDiagnosticSource : DiagnosticSource + { + public override bool IsEnabled(string name) + { + return false; + } + + public override void Write(string name, object value) { } + } + } + + private class App : IComponent + { + void IComponent.Attach(RenderHandle renderHandle) => throw new NotImplementedException(); + Task IComponent.SetParametersAsync(ParameterView parameters) => throw new NotImplementedException(); + } +} diff --git a/src/Components/Endpoints/test/Builder/TestFileProvider/TestDirectoryContent.cs b/src/Components/Endpoints/test/Builder/TestFileProvider/TestDirectoryContent.cs new file mode 100644 index 000000000000..bb3dea238785 --- /dev/null +++ b/src/Components/Endpoints/test/Builder/TestFileProvider/TestDirectoryContent.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; + +namespace Microsoft.Extensions.FileProviders; + +public class TestDirectoryContent : IDirectoryContents, IFileInfo +{ + private readonly IEnumerable _files; + + public TestDirectoryContent(string name, IEnumerable files) + { + Name = name; + _files = files; + } + + public bool Exists => true; + + public long Length => throw new NotSupportedException(); + + public string PhysicalPath => throw new NotSupportedException(); + + public string Name { get; } + + public DateTimeOffset LastModified => throw new NotSupportedException(); + + public bool IsDirectory => true; + + public Stream CreateReadStream() + { + throw new NotSupportedException(); + } + + public IEnumerator GetEnumerator() => _files.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/src/Components/Endpoints/test/Builder/TestFileProvider/TestFileChangeToken.cs b/src/Components/Endpoints/test/Builder/TestFileProvider/TestFileChangeToken.cs new file mode 100644 index 000000000000..52a2019896ff --- /dev/null +++ b/src/Components/Endpoints/test/Builder/TestFileProvider/TestFileChangeToken.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Primitives; + +public class TestFileChangeToken : IChangeToken +{ + public TestFileChangeToken(string filter = "") + { + Filter = filter; + } + + public bool ActiveChangeCallbacks => false; + + public bool HasChanged { get; set; } + + public string Filter { get; } + + public IDisposable RegisterChangeCallback(Action callback, object state) + { + return new NullDisposable(); + } + + private sealed class NullDisposable : IDisposable + { + public void Dispose() + { + } + } + + public override string ToString() => Filter; +} diff --git a/src/Components/Endpoints/test/Builder/TestFileProvider/TestFileInfo.cs b/src/Components/Endpoints/test/Builder/TestFileProvider/TestFileInfo.cs new file mode 100644 index 000000000000..0614d7148629 --- /dev/null +++ b/src/Components/Endpoints/test/Builder/TestFileProvider/TestFileInfo.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; + +namespace Microsoft.Extensions.FileProviders; + +public class TestFileInfo : IFileInfo +{ + private string _content; + + public bool IsDirectory => false; + + public DateTimeOffset LastModified { get; set; } + + public long Length { get; set; } + + public string Name { get; set; } + + public string PhysicalPath { get; set; } + + public string Content + { + get { return _content; } + set + { + _content = value; + Length = Encoding.UTF8.GetByteCount(Content); + } + } + + public bool Exists + { + get { return true; } + } + + public Stream CreateReadStream() + { + var bytes = Encoding.UTF8.GetBytes(Content); + return new MemoryStream(bytes); + } +} diff --git a/src/Components/Endpoints/test/Builder/TestFileProvider/TestFileProvider.cs b/src/Components/Endpoints/test/Builder/TestFileProvider/TestFileProvider.cs new file mode 100644 index 000000000000..6d161a062f48 --- /dev/null +++ b/src/Components/Endpoints/test/Builder/TestFileProvider/TestFileProvider.cs @@ -0,0 +1,179 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Extensions.FileProviders; + +public class TestFileProvider : IFileProvider +{ + private readonly Dictionary _lookup = + new Dictionary(StringComparer.Ordinal); + private readonly Dictionary _directoryContentsLookup = + new Dictionary(); + + private readonly Dictionary _fileTriggers = + new Dictionary(StringComparer.Ordinal); + + public TestFileProvider() : this(string.Empty) + { + } + + public TestFileProvider(string root) + { + Root = root; + } + + public string Root { get; } + + public virtual IDirectoryContents GetDirectoryContents(string subpath) + { + if (_directoryContentsLookup.TryGetValue(subpath, out var value)) + { + return value; + } + + return new NotFoundDirectoryContents(); + } + + public TestFileInfo AddFile(string path, string contents) + { + var fileInfo = new TestFileInfo + { + Content = contents, + PhysicalPath = Path.Combine(Root, NormalizeAndEnsureValidPhysicalPath(path)), + Name = Path.GetFileName(path), + LastModified = DateTime.UtcNow, + }; + + AddFile(path, fileInfo); + + return fileInfo; + } + + public TestDirectoryContent AddDirectoryContent(string path, IEnumerable files) + { + var directoryContent = new TestDirectoryContent(Path.GetFileName(path), files); + _directoryContentsLookup[path] = directoryContent; + return directoryContent; + } + + public void AddFile(string path, IFileInfo contents) + { + _lookup[path] = contents; + } + + public void DeleteFile(string path) + { + _lookup.Remove(path); + } + + public virtual IFileInfo GetFileInfo(string subpath) + { + if (_lookup.TryGetValue(subpath, out var fileInfo)) + { + return fileInfo; + } + else + { + return new NotFoundFileInfo(); + } + } + + public virtual TestFileChangeToken AddChangeToken(string filter) + { + var changeToken = new TestFileChangeToken(filter); + _fileTriggers[filter] = changeToken; + + return changeToken; + } + + public virtual IChangeToken Watch(string filter) + { + if (!_fileTriggers.TryGetValue(filter, out var changeToken) || changeToken.HasChanged) + { + changeToken = new TestFileChangeToken(filter); + _fileTriggers[filter] = changeToken; + } + + return changeToken; + } + + public TestFileChangeToken GetChangeToken(string filter) + { + return _fileTriggers[filter]; + } + + private static string NormalizeAndEnsureValidPhysicalPath(string filePath) + { + if (string.IsNullOrEmpty(filePath)) + { + return filePath; + } + + filePath = filePath.Replace('/', Path.DirectorySeparatorChar); + + if (filePath[0] == Path.DirectorySeparatorChar) + { + filePath = filePath.Substring(1); + } + + return filePath; + } + + private sealed class NotFoundFileInfo : IFileInfo + { + public bool Exists + { + get + { + return false; + } + } + + public bool IsDirectory + { + get + { + throw new NotImplementedException(); + } + } + + public DateTimeOffset LastModified + { + get + { + throw new NotImplementedException(); + } + } + + public long Length + { + get + { + throw new NotImplementedException(); + } + } + + public string Name + { + get + { + throw new NotImplementedException(); + } + } + + public string PhysicalPath + { + get + { + throw new NotImplementedException(); + } + } + + public Stream CreateReadStream() + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Components/Endpoints/test/Microsoft.AspNetCore.Components.Endpoints.Tests.csproj b/src/Components/Endpoints/test/Microsoft.AspNetCore.Components.Endpoints.Tests.csproj index 53440c2dbd6d..3fe362d5d236 100644 --- a/src/Components/Endpoints/test/Microsoft.AspNetCore.Components.Endpoints.Tests.csproj +++ b/src/Components/Endpoints/test/Microsoft.AspNetCore.Components.Endpoints.Tests.csproj @@ -14,4 +14,9 @@ + + + + + diff --git a/src/Components/Endpoints/test/TestApplication.staticwebassets.endpoints.json b/src/Components/Endpoints/test/TestApplication.staticwebassets.endpoints.json new file mode 100644 index 000000000000..586241880352 --- /dev/null +++ b/src/Components/Endpoints/test/TestApplication.staticwebassets.endpoints.json @@ -0,0 +1,17 @@ +{ + "Version": 1, + "Endpoints": [ + { + "Route": "default.css", + "AssetFile": "default.css", + "Selectors": [], + "EndpointProperties": [], + "ResponseHeaders": [ + { + "Name": "ETag", + "Value": "\"Fake\"" + } + ] + } + ] +} diff --git a/src/Components/Endpoints/test/TestManifests/Test.staticwebassets.endpoints.json b/src/Components/Endpoints/test/TestManifests/Test.staticwebassets.endpoints.json new file mode 100644 index 000000000000..77815b7c379c --- /dev/null +++ b/src/Components/Endpoints/test/TestManifests/Test.staticwebassets.endpoints.json @@ -0,0 +1,17 @@ +{ + "Version": 1, + "Endpoints": [ + { + "Route": "named.css", + "AssetFile": "named.css", + "Selectors": [], + "EndpointProperties": [], + "ResponseHeaders": [ + { + "Name": "ETag", + "Value": "\"Fake\"" + } + ] + } + ] +} From 88383db14561ff9e4222caad6cf701d8ecab0aa0 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Thu, 13 Jun 2024 23:09:26 +0200 Subject: [PATCH 16/30] Add components E2E tests --- AspNetCore.sln | 2 +- .../Components/src/ResourceAsset.cs | 7 +++ .../src/Assets/ImportMapDefinition.cs | 2 +- .../BlazorUnitedApp/BlazorUnitedApp.sln | 25 -------- .../ResourceCollectionTest.cs | 63 +++++++++++++++++++ .../Components.TestServer.csproj | 1 - .../Pages/ResourceCollection/Index.razor | 44 +++++++++++++ .../ResourceCollectionSample.razor | 22 +++++++ 8 files changed, 138 insertions(+), 28 deletions(-) delete mode 100644 src/Components/Samples/BlazorUnitedApp/BlazorUnitedApp.sln create mode 100644 src/Components/test/E2ETest/ServerRenderingTests/ResourceCollectionTest.cs create mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/ResourceCollection/Index.razor create mode 100644 src/Components/test/testassets/TestContentPackage/ResourceCollectionSample.razor diff --git a/AspNetCore.sln b/AspNetCore.sln index d9d83a1961f7..9164c1b75bba 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1816,7 +1816,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GetDocumentInsider.Tests", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GetDocumentSample", "src\Tools\GetDocumentInsider\sample\GetDocumentSample.csproj", "{D8F7091E-A2D1-4E81-BA7C-97EAE392D683}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazorUnitedApp.Client", "src\Components\Samples\BlazorUnitedApp.Client\BlazorUnitedApp.Client.csproj", "{757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorUnitedApp.Client", "src\Components\Samples\BlazorUnitedApp.Client\BlazorUnitedApp.Client.csproj", "{757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/Components/Components/src/ResourceAsset.cs b/src/Components/Components/src/ResourceAsset.cs index 149c091e70f9..315fbadff537 100644 --- a/src/Components/Components/src/ResourceAsset.cs +++ b/src/Components/Components/src/ResourceAsset.cs @@ -1,6 +1,9 @@ // 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; +using System.Linq; + namespace Microsoft.AspNetCore.Components; /// @@ -8,6 +11,7 @@ namespace Microsoft.AspNetCore.Components; /// /// The URL of the resource. /// The properties associated to this resource. +[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")] public sealed class ResourceAsset(string url, IReadOnlyList? properties) { /// @@ -19,4 +23,7 @@ public sealed class ResourceAsset(string url, IReadOnlyList public IReadOnlyList? Properties { get; } = properties; + + private string GetDebuggerDisplay() => + $"Url: '{Url}' - Properties: {string.Join(", ", Properties?.Select(p => $"{p.Name} = {p.Value}") ?? [])}"; } diff --git a/src/Components/Endpoints/src/Assets/ImportMapDefinition.cs b/src/Components/Endpoints/src/Assets/ImportMapDefinition.cs index c5075032f0c8..166efadaacb8 100644 --- a/src/Components/Endpoints/src/Assets/ImportMapDefinition.cs +++ b/src/Components/Endpoints/src/Assets/ImportMapDefinition.cs @@ -75,7 +75,7 @@ public static ImportMapDefinition FromResourceCollection(ResourceAssetCollection if (label != null) { importMap._imports ??= []; - importMap._imports[label] = asset.Url; + importMap._imports[$"./{label}"] = $"./{asset.Url}"; } } diff --git a/src/Components/Samples/BlazorUnitedApp/BlazorUnitedApp.sln b/src/Components/Samples/BlazorUnitedApp/BlazorUnitedApp.sln deleted file mode 100644 index 82bcdb4b0685..000000000000 --- a/src/Components/Samples/BlazorUnitedApp/BlazorUnitedApp.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.5.002.0 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorUnitedApp", "BlazorUnitedApp.csproj", "{3D806AD9-86E3-4253-B8C5-36B29E88F625}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {3D806AD9-86E3-4253-B8C5-36B29E88F625}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3D806AD9-86E3-4253-B8C5-36B29E88F625}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3D806AD9-86E3-4253-B8C5-36B29E88F625}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3D806AD9-86E3-4253-B8C5-36B29E88F625}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {41A71825-ABB1-44F6-B009-1DA36A9E19F5} - EndGlobalSection -EndGlobal diff --git a/src/Components/test/E2ETest/ServerRenderingTests/ResourceCollectionTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/ResourceCollectionTest.cs new file mode 100644 index 000000000000..f935ac4fe3a0 --- /dev/null +++ b/src/Components/test/E2ETest/ServerRenderingTests/ResourceCollectionTest.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Components.TestServer.RazorComponents; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using Microsoft.AspNetCore.E2ETesting; +using OpenQA.Selenium; +using TestServer; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Components.E2ETests.ServerRenderingTests; + +public partial class ResourceCollectionTest : ServerTestBase>> +{ + public ResourceCollectionTest(BrowserFixture browserFixture, + BasicTestAppServerSiteFixture> serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) + { + } + + [Fact] + public void StaticRendering_CanUseFingerprintedResources() + { + var url = $"{ServerPathBase}/resource-collection"; + Navigate(url); + + Browser.True(() => AppStylesRegex().IsMatch(Browser.Exists(By.Id("basic-app-styles")).Text)); + + Browser.Exists(By.Id("import-module")).Click(); + + Browser.True(() => JsModuleRegex().IsMatch(Browser.Exists(By.Id("js-module")).Text)); + } + + [Theory] + [InlineData("Server")] + [InlineData("WebAssembly")] + public void StaticRendering_CanUseFingerprintedResources_InteractiveModes(string renderMode) + { + var url = $"{ServerPathBase}/resource-collection?render-mode={renderMode}"; + Navigate(url); + + Browser.Equal(renderMode, () => Browser.Exists(By.Id("platform-name")).Text); + + Browser.True(() => AppStylesRegex().IsMatch(Browser.Exists(By.Id("basic-app-styles")).Text)); + + Browser.Exists(By.Id("import-module")).Click(); + + Browser.True(() => JsModuleRegex().IsMatch(Browser.Exists(By.Id("js-module")).Text)); + } + + [GeneratedRegex("""BasicTestApp\.[a-zA-Z0-9]{10}\.styles\.css""")] + private static partial Regex AppStylesRegex(); + [GeneratedRegex(""".*Index\.[a-zA-Z0-9]{10}\.mjs""")] + private static partial Regex JsModuleRegex(); +} diff --git a/src/Components/test/testassets/Components.TestServer/Components.TestServer.csproj b/src/Components/test/testassets/Components.TestServer/Components.TestServer.csproj index 405a26d27291..1341319c942c 100644 --- a/src/Components/test/testassets/Components.TestServer/Components.TestServer.csproj +++ b/src/Components/test/testassets/Components.TestServer/Components.TestServer.csproj @@ -48,5 +48,4 @@ <_Parameter2 Condition="'$(IsHelixJob)' == 'true'">..\BasicTestApp - diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/ResourceCollection/Index.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/ResourceCollection/Index.razor new file mode 100644 index 000000000000..d3f2ff66ee24 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/ResourceCollection/Index.razor @@ -0,0 +1,44 @@ +@page "/resource-collection" +@using Microsoft.AspNetCore.Components.Web; +@using Microsoft.AspNetCore.Components.Endpoints; + +Resource Collection + + + + + +

Hello from resource collection

+ +

+ This page demonstrates the workings of resource collection. + + The ResourceCollectionSample component will display fingerprinted URLs for the BasicTestApp.styles.css + and will trigger an import of a JavaScript module via it's human readable name. + + Upon import, the script will append a new element to the dom and will display its import.meta.url, which + when the importmap works will show the fingerprinted URL. + + The test runs in Static, Server and WebAssembly render modes. +

+ + + +@code +{ + [SupplyParameterFromQuery(Name = "render-mode")] + public string SelectedRenderMode { get; set; } + + private IComponentRenderMode? _renderMode; + + protected override void OnInitialized() + { + var assets = Assets; + _renderMode = SelectedRenderMode switch + { + "Server" => RenderMode.InteractiveServer, + "WebAssembly" => RenderMode.InteractiveWebAssembly, + _ => null + }; + } +} diff --git a/src/Components/test/testassets/TestContentPackage/ResourceCollectionSample.razor b/src/Components/test/testassets/TestContentPackage/ResourceCollectionSample.razor new file mode 100644 index 000000000000..703d78fc3f9a --- /dev/null +++ b/src/Components/test/testassets/TestContentPackage/ResourceCollectionSample.razor @@ -0,0 +1,22 @@ +@using Microsoft.JSInterop +@using Microsoft.AspNetCore.Components.Web; +@inject IJSRuntime JSRuntime + +

@Platform.Name

+ +

@Assets["BasicTestApp.styles.css"]

+ +@if (!Platform.IsInteractive) +{ + +}else +{ + +} + +@code{ + private async Task ManualImport() + { + await JSRuntime.InvokeVoidAsync("import", "./Index.mjs"); + } +} From 28edc30ce045f93f1d56d7c33831979cc91d4f41 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Thu, 13 Jun 2024 23:12:31 +0200 Subject: [PATCH 17/30] Undo MVC samples --- src/Mvc/samples/MvcSandbox/Startup.cs | 9 +++------ src/Mvc/samples/MvcSandbox/Views/Shared/_Layout.cshtml | 3 --- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/Mvc/samples/MvcSandbox/Startup.cs b/src/Mvc/samples/MvcSandbox/Startup.cs index 3f5a178d3c92..2708a0c0b47c 100644 --- a/src/Mvc/samples/MvcSandbox/Startup.cs +++ b/src/Mvc/samples/MvcSandbox/Startup.cs @@ -16,23 +16,20 @@ public void ConfigureServices(IServiceCollection services) public void Configure(IApplicationBuilder app) { app.UseDeveloperExceptionPage(); + app.UseStaticFiles(); app.UseRouting(); static void ConfigureEndpoints(IEndpointRouteBuilder endpoints) { - endpoints.MapStaticAssets(); endpoints.MapGet("/MapGet", () => "MapGet"); - endpoints.MapControllers() - .WithResourceCollection(); - + endpoints.MapControllers(); endpoints.MapControllerRoute( Guid.NewGuid().ToString(), "{controller=Home}/{action=Index}/{id?}"); - endpoints.MapRazorPages() - .WithResourceCollection(); + endpoints.MapRazorPages(); } app.UseEndpoints(builder => diff --git a/src/Mvc/samples/MvcSandbox/Views/Shared/_Layout.cshtml b/src/Mvc/samples/MvcSandbox/Views/Shared/_Layout.cshtml index 84f8a9a3dddb..60569523afd5 100644 --- a/src/Mvc/samples/MvcSandbox/Views/Shared/_Layout.cshtml +++ b/src/Mvc/samples/MvcSandbox/Views/Shared/_Layout.cshtml @@ -4,10 +4,7 @@ @ViewData["Title"] - MvcSandbox - - -
From 0abbd206c6cb3dda86227b871f65aaebf53cc2c1 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Thu, 13 Jun 2024 23:14:45 +0200 Subject: [PATCH 18/30] Undo Blazor samples changes --- .../BlazorUnitedApp.Client/HelloWorld.razor | 4 +--- .../wwwroot/blazor-logo.png | Bin 13226 -> 0 bytes 2 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 src/Components/Samples/BlazorUnitedApp.Client/wwwroot/blazor-logo.png diff --git a/src/Components/Samples/BlazorUnitedApp.Client/HelloWorld.razor b/src/Components/Samples/BlazorUnitedApp.Client/HelloWorld.razor index 65954dbb3bc4..9bde6d3ba70b 100644 --- a/src/Components/Samples/BlazorUnitedApp.Client/HelloWorld.razor +++ b/src/Components/Samples/BlazorUnitedApp.Client/HelloWorld.razor @@ -1,3 +1 @@ -

Hello assets!

- -Blazor Logo +

Hello webassembly!

diff --git a/src/Components/Samples/BlazorUnitedApp.Client/wwwroot/blazor-logo.png b/src/Components/Samples/BlazorUnitedApp.Client/wwwroot/blazor-logo.png deleted file mode 100644 index af3641ce89b18015bdd3e36c76c14e20017082e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13226 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 Date: Fri, 14 Jun 2024 09:59:57 +0200 Subject: [PATCH 19/30] Fix E2ETest --- .../Endpoints/test/Assets/ImportMapDefinitionTest.cs | 2 +- .../test/testassets/Components.TestServer/wwwroot/Index.mjs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 src/Components/test/testassets/Components.TestServer/wwwroot/Index.mjs diff --git a/src/Components/Endpoints/test/Assets/ImportMapDefinitionTest.cs b/src/Components/Endpoints/test/Assets/ImportMapDefinitionTest.cs index 6b4c69105b44..3c3844bd4c93 100644 --- a/src/Components/Endpoints/test/Assets/ImportMapDefinitionTest.cs +++ b/src/Components/Endpoints/test/Assets/ImportMapDefinitionTest.cs @@ -112,7 +112,7 @@ public void CanBuildImportMap_FromResourceCollection() var expectedJson = """ { "imports": { - "jquery.js": "jquery.fingerprint.js" + "./jquery.js": "./jquery.fingerprint.js" }, "integrity": { "jquery.fingerprint.js": "sha384-abc123" diff --git a/src/Components/test/testassets/Components.TestServer/wwwroot/Index.mjs b/src/Components/test/testassets/Components.TestServer/wwwroot/Index.mjs new file mode 100644 index 000000000000..73cb5d9f8e0a --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/wwwroot/Index.mjs @@ -0,0 +1,4 @@ +var element = document.createElement('p'); +element.id = 'js-module'; +element.innerHTML = import.meta.url; +document.getElementById('import-module').after(element); From 87512d704ffb31c7b7e41d2c7c1c16429eb0de68 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Fri, 14 Jun 2024 13:44:03 +0200 Subject: [PATCH 20/30] Make tests cross-plat friendly --- .../test/Assets/ImportMapDefinitionTest.cs | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/Components/Endpoints/test/Assets/ImportMapDefinitionTest.cs b/src/Components/Endpoints/test/Assets/ImportMapDefinitionTest.cs index 3c3844bd4c93..3821373775c6 100644 --- a/src/Components/Endpoints/test/Assets/ImportMapDefinitionTest.cs +++ b/src/Components/Endpoints/test/Assets/ImportMapDefinitionTest.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Text.Json; using System.Threading.Tasks; namespace Microsoft.AspNetCore.Components.Endpoints.Assets; @@ -21,7 +22,7 @@ public void CanCreate_Basic_ImportMapDefinition() "jquery": "https://cdn.example.com/jquery.js" } } - """; + """.Replace("\r\n", "\n"); var importMapDefinition = new ImportMapDefinition( new Dictionary @@ -33,7 +34,7 @@ public void CanCreate_Basic_ImportMapDefinition() ); // Assert - Assert.Equal(expectedJson, importMapDefinition.ToJson()); + Assert.Equal(expectedJson, importMapDefinition.ToJson().Replace("\r\n", "\n")); } [Fact] @@ -48,7 +49,7 @@ public void CanCreate_Scoped_ImportMapDefinition() } } } - """; + """.Replace("\r\n", "\n"); var importMapDefinition = new ImportMapDefinition( null, @@ -62,7 +63,7 @@ public void CanCreate_Scoped_ImportMapDefinition() null); // Assert - Assert.Equal(expectedJson, importMapDefinition.ToJson()); + Assert.Equal(expectedJson, importMapDefinition.ToJson().Replace("\r\n", "\n")); } [Fact] @@ -78,7 +79,7 @@ public void CanCreate_ImportMap_WithIntegrity() "https://cdn.example.com/jquery.js": "sha384-abc123" } } - """; + """.Replace("\r\n", "\n"); var importMapDefinition = new ImportMapDefinition( new Dictionary @@ -92,7 +93,7 @@ public void CanCreate_ImportMap_WithIntegrity() }); // Assert - Assert.Equal(expectedJson, importMapDefinition.ToJson()); + Assert.Equal(expectedJson, importMapDefinition.ToJson().Replace("\r\n", "\n")); } [Fact] @@ -118,13 +119,13 @@ public void CanBuildImportMap_FromResourceCollection() "jquery.fingerprint.js": "sha384-abc123" } } - """; + """.Replace("\r\n", "\n"); // Act var importMapDefinition = ImportMapDefinition.FromResourceCollection(resourceAssetCollection); // Assert - Assert.Equal(expectedJson, importMapDefinition.ToJson()); + Assert.Equal(expectedJson, importMapDefinition.ToJson().Replace("\r\n", "\n")); } [Fact] @@ -189,12 +190,12 @@ public void CanCombine_ImportMaps() "https://cdn.example.com/react.js": "sha384-def456" } } - """; + """.Replace("\r\n", "\n"); // Act var combinedImportMap = ImportMapDefinition.Combine(firstImportMap, secondImportMap); // Assert - Assert.Equal(expectedImportMap, combinedImportMap.ToJson()); + Assert.Equal(expectedImportMap, combinedImportMap.ToJson().Replace("\r\n", "\n")); } } From de4c97a7284067752bcab200f114e99106ecadca Mon Sep 17 00:00:00 2001 From: jacalvar Date: Fri, 14 Jun 2024 14:30:54 +0200 Subject: [PATCH 21/30] WithResourceCollection -> WithStaticAssets --- ...entsEndpointConventionBuilderExtensions.cs | 2 +- ...omponentsEndpointRouteBuilderExtensions.cs | 4 +- .../Endpoints/src/PublicAPI.Unshipped.txt | 2 +- ...EndpointConventionBuilderExtensionsTest.cs | 54 +++++++++---------- ...tionBuilderResourceCollectionExtensions.cs | 2 +- .../src/PublicAPI.Unshipped.txt | 2 +- ...BuilderResourceCollectionExtensionsTest.cs | 52 +++++++++--------- .../Mvc.TagHelpers/test/ImageTagHelperTest.cs | 2 +- .../Mvc.TagHelpers/test/LinkTagHelperTest.cs | 4 +- .../test/ScriptTagHelperTest.cs | 4 +- ...tionBuilderResourceCollectionExtensions.cs | 2 +- .../src/PublicAPI.Unshipped.txt | 2 +- ...BuilderResourceCollectionExtensionsTest.cs | 52 +++++++++--------- .../RazorPagesWeb-CSharp/Program.Main.cs | 2 +- .../content/RazorPagesWeb-CSharp/Program.cs | 2 +- .../content/StarterWeb-CSharp/Program.Main.cs | 5 +- .../content/StarterWeb-CSharp/Program.cs | 4 +- 17 files changed, 99 insertions(+), 98 deletions(-) diff --git a/src/Components/Endpoints/src/Builder/RazorComponentsEndpointConventionBuilderExtensions.cs b/src/Components/Endpoints/src/Builder/RazorComponentsEndpointConventionBuilderExtensions.cs index 843fb30c61d5..fd0a6aec0c4d 100644 --- a/src/Components/Endpoints/src/Builder/RazorComponentsEndpointConventionBuilderExtensions.cs +++ b/src/Components/Endpoints/src/Builder/RazorComponentsEndpointConventionBuilderExtensions.cs @@ -47,7 +47,7 @@ public static RazorComponentsEndpointConventionBuilder AddAdditionalAssemblies( /// the /// call. /// - public static RazorComponentsEndpointConventionBuilder WithResourceCollection( + public static RazorComponentsEndpointConventionBuilder WithStaticAssets( this RazorComponentsEndpointConventionBuilder builder, string? manifestPath = null) { diff --git a/src/Components/Endpoints/src/Builder/RazorComponentsEndpointRouteBuilderExtensions.cs b/src/Components/Endpoints/src/Builder/RazorComponentsEndpointRouteBuilderExtensions.cs index bcd072b5ee90..80f760a519ee 100644 --- a/src/Components/Endpoints/src/Builder/RazorComponentsEndpointRouteBuilderExtensions.cs +++ b/src/Components/Endpoints/src/Builder/RazorComponentsEndpointRouteBuilderExtensions.cs @@ -37,9 +37,9 @@ public static class RazorComponentsEndpointRouteBuilderExtensions // Setup the convention to find the list of descriptors in the endpoint builder and // populate a resource collection out of them. - // The user can call WithResourceCollection with a manifest path to override the manifest + // The user can call WithStaticAssets with a manifest path to override the manifest // to use for the resource collection in case more than one has been mapped. - result.WithResourceCollection(); + result.WithStaticAssets(); return result; } diff --git a/src/Components/Endpoints/src/PublicAPI.Unshipped.txt b/src/Components/Endpoints/src/PublicAPI.Unshipped.txt index 867b5c2e5949..5f7be5fbce4d 100644 --- a/src/Components/Endpoints/src/PublicAPI.Unshipped.txt +++ b/src/Components/Endpoints/src/PublicAPI.Unshipped.txt @@ -16,7 +16,7 @@ Microsoft.AspNetCore.Components.Server.ServerAuthenticationStateProvider.ServerA Microsoft.AspNetCore.Components.Server.ServerAuthenticationStateProvider.SetAuthenticationState(System.Threading.Tasks.Task! authenticationStateTask) -> void override Microsoft.AspNetCore.Components.ImportMapDefinition.ToString() -> string! override Microsoft.AspNetCore.Components.Server.ServerAuthenticationStateProvider.GetAuthenticationStateAsync() -> System.Threading.Tasks.Task! -static Microsoft.AspNetCore.Builder.RazorComponentsEndpointConventionBuilderExtensions.WithResourceCollection(this Microsoft.AspNetCore.Builder.RazorComponentsEndpointConventionBuilder! builder, string? manifestPath = null) -> Microsoft.AspNetCore.Builder.RazorComponentsEndpointConventionBuilder! +static Microsoft.AspNetCore.Builder.RazorComponentsEndpointConventionBuilderExtensions.WithStaticAssets(this Microsoft.AspNetCore.Builder.RazorComponentsEndpointConventionBuilder! builder, string? manifestPath = null) -> Microsoft.AspNetCore.Builder.RazorComponentsEndpointConventionBuilder! static Microsoft.AspNetCore.Components.Endpoints.Infrastructure.ComponentEndpointConventionBuilderHelper.GetEndpointRouteBuilder(Microsoft.AspNetCore.Builder.RazorComponentsEndpointConventionBuilder! builder) -> Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! static Microsoft.AspNetCore.Components.ImportMapDefinition.Combine(params Microsoft.AspNetCore.Components.ImportMapDefinition![]! sources) -> Microsoft.AspNetCore.Components.ImportMapDefinition! static Microsoft.AspNetCore.Components.ImportMapDefinition.FromResourceCollection(Microsoft.AspNetCore.Components.ResourceAssetCollection! assets) -> Microsoft.AspNetCore.Components.ImportMapDefinition! diff --git a/src/Components/Endpoints/test/Builder/RazorComponentsEndpointConventionBuilderExtensionsTest.cs b/src/Components/Endpoints/test/Builder/RazorComponentsEndpointConventionBuilderExtensionsTest.cs index a33f1f2e7008..e630e76da863 100644 --- a/src/Components/Endpoints/test/Builder/RazorComponentsEndpointConventionBuilderExtensionsTest.cs +++ b/src/Components/Endpoints/test/Builder/RazorComponentsEndpointConventionBuilderExtensionsTest.cs @@ -16,14 +16,14 @@ namespace Microsoft.AspNetCore.Builder; public class RazorComponentsEndpointConventionBuilderExtensionsTest { [Fact] - public void WithResourceCollection_DoesNotAddResourceCollection_ToEndpoints_NoStaticAssetsMapped() + public void WithStaticAssets_DoesNotAddResourceCollection_ToEndpoints_NoStaticAssetsMapped() { - // Arrange + // Arrange var endpointBuilder = new TestEndpointRouteBuilder(); var builder = CreateRazorComponentsAppBuilder(endpointBuilder); // Act - builder.WithResourceCollection(); + builder.WithStaticAssets(); // Assert Assert.All(endpointBuilder.DataSources.Skip(1).First().Endpoints, e => @@ -39,15 +39,15 @@ public void WithResourceCollection_DoesNotAddResourceCollection_ToEndpoints_NoSt } [Fact] - public void WithResourceCollection_DoesNotAddResourceCollection_ToEndpoints_NoMatchingStaticAssetsMapped() + public void WithStaticAssets_DoesNotAddResourceCollection_ToEndpoints_NoMatchingStaticAssetsMapped() { - // Arrange + // Arrange var endpointBuilder = new TestEndpointRouteBuilder(); endpointBuilder.MapStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); var builder = CreateRazorComponentsAppBuilder(endpointBuilder); // Act - builder.WithResourceCollection(); + builder.WithStaticAssets(); // Assert Assert.All(endpointBuilder.DataSources.Skip(2).First().Endpoints, e => @@ -58,15 +58,15 @@ public void WithResourceCollection_DoesNotAddResourceCollection_ToEndpoints_NoMa } [Fact] - public void WithResourceCollection_AddsResourceCollection_ToEndpoints_NamedManifest() + public void WithStaticAssets_AddsResourceCollection_ToEndpoints_NamedManifest() { - // Arrange + // Arrange var endpointBuilder = new TestEndpointRouteBuilder(); endpointBuilder.MapStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); var builder = CreateRazorComponentsAppBuilder(endpointBuilder); // Act - builder.WithResourceCollection("TestManifests/Test.staticwebassets.endpoints.json"); + builder.WithStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); // Assert Assert.All(endpointBuilder.DataSources.Skip(2).First().Endpoints, e => @@ -80,9 +80,9 @@ public void WithResourceCollection_AddsResourceCollection_ToEndpoints_NamedManif } [Fact] - public void WithResourceCollection_AddsDefaultResourceCollection_ToEndpoints_ByDefault() + public void WithStaticAssets_AddsDefaultResourceCollection_ToEndpoints_ByDefault() { - // Arrange + // Arrange var endpointBuilder = new TestEndpointRouteBuilder(); endpointBuilder.MapStaticAssets(); @@ -101,15 +101,15 @@ public void WithResourceCollection_AddsDefaultResourceCollection_ToEndpoints_ByD } [Fact] - public void WithResourceCollection_AddsResourceCollection_ToEndpoints_DefaultManifest() + public void WithStaticAssets_AddsResourceCollection_ToEndpoints_DefaultManifest() { - // Arrange + // Arrange var endpointBuilder = new TestEndpointRouteBuilder(); endpointBuilder.MapStaticAssets(); var builder = CreateRazorComponentsAppBuilder(endpointBuilder); // Act - builder.WithResourceCollection(); + builder.WithStaticAssets(); // Assert Assert.All(endpointBuilder.DataSources.Skip(2).First().Endpoints, e => @@ -123,9 +123,9 @@ public void WithResourceCollection_AddsResourceCollection_ToEndpoints_DefaultMan } [Fact] - public void WithResourceCollection_AddsDefaultResourceCollectionToEndpoints_WhenNoManifestProvided_EvenIfManyAvailable() + public void WithStaticAssets_AddsDefaultResourceCollectionToEndpoints_WhenNoManifestProvided_EvenIfManyAvailable() { - // Arrange + // Arrange var endpointBuilder = new TestEndpointRouteBuilder(); endpointBuilder.MapStaticAssets(); endpointBuilder.MapStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); @@ -133,7 +133,7 @@ public void WithResourceCollection_AddsDefaultResourceCollectionToEndpoints_When // Act var builder = CreateRazorComponentsAppBuilder(endpointBuilder); - // Assert + // Assert Assert.All(endpointBuilder.DataSources.Skip(3).First().Endpoints, e => { var metadata = e.Metadata.GetMetadata(); @@ -145,18 +145,18 @@ public void WithResourceCollection_AddsDefaultResourceCollectionToEndpoints_When } [Fact] - public void WithResourceCollection_AddsMatchingResourceCollectionToEndpoints_WhenExplicitManifestProvided_EvenIfManyAvailable() + public void WithStaticAssets_AddsMatchingResourceCollectionToEndpoints_WhenExplicitManifestProvided_EvenIfManyAvailable() { - // Arrange + // Arrange var endpointBuilder = new TestEndpointRouteBuilder(); endpointBuilder.MapStaticAssets(); endpointBuilder.MapStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); var builder = CreateRazorComponentsAppBuilder(endpointBuilder); // Act - builder.WithResourceCollection("TestManifests/Test.staticwebassets.endpoints.json"); + builder.WithStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); - // Assert + // Assert Assert.All(endpointBuilder.DataSources.Skip(3).First().Endpoints, e => { var metadata = e.Metadata.GetMetadata(); @@ -168,9 +168,9 @@ public void WithResourceCollection_AddsMatchingResourceCollectionToEndpoints_Whe } [Fact] - public void WithResourceCollection_AddsCollectionFromGroup_WhenMappedInsideAnEndpointGroup() + public void WithStaticAssets_AddsCollectionFromGroup_WhenMappedInsideAnEndpointGroup() { - // Arrange + // Arrange var endpointBuilder = new TestEndpointRouteBuilder(); endpointBuilder.MapStaticAssets(); @@ -179,7 +179,7 @@ public void WithResourceCollection_AddsCollectionFromGroup_WhenMappedInsideAnEnd var builder = CreateRazorComponentsAppBuilder(group); // Act - builder.WithResourceCollection("TestManifests/Test.staticwebassets.endpoints.json"); + builder.WithStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); // Assert var groupEndpoints = Assert.IsAssignableFrom(group).DataSources; @@ -194,9 +194,9 @@ public void WithResourceCollection_AddsCollectionFromGroup_WhenMappedInsideAnEnd } [Fact] - public void WithResourceCollection_DoesNotAddResourceCollectionFromGroup_WhenMappingNotFound_InsideGroup() + public void WithStaticAssets_DoesNotAddResourceCollectionFromGroup_WhenMappingNotFound_InsideGroup() { - // Arrange + // Arrange var endpointBuilder = new TestEndpointRouteBuilder(); endpointBuilder.MapStaticAssets(); @@ -205,7 +205,7 @@ public void WithResourceCollection_DoesNotAddResourceCollectionFromGroup_WhenMap var builder = CreateRazorComponentsAppBuilder(group); // Act - builder.WithResourceCollection(); + builder.WithStaticAssets(); // Assert var groupEndpoints = Assert.IsAssignableFrom(group).DataSources; diff --git a/src/Mvc/Mvc.RazorPages/src/Builder/PageActionEndpointConventionBuilderResourceCollectionExtensions.cs b/src/Mvc/Mvc.RazorPages/src/Builder/PageActionEndpointConventionBuilderResourceCollectionExtensions.cs index f0e351fa692e..18d5c965b664 100644 --- a/src/Mvc/Mvc.RazorPages/src/Builder/PageActionEndpointConventionBuilderResourceCollectionExtensions.cs +++ b/src/Mvc/Mvc.RazorPages/src/Builder/PageActionEndpointConventionBuilderResourceCollectionExtensions.cs @@ -18,7 +18,7 @@ public static class PageActionEndpointConventionBuilderResourceCollectionExtensi /// The . /// The manifest associated with the assets. /// - public static PageActionEndpointConventionBuilder WithResourceCollection( + public static PageActionEndpointConventionBuilder WithStaticAssets( this PageActionEndpointConventionBuilder builder, string? manifestPath = null) { diff --git a/src/Mvc/Mvc.RazorPages/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.RazorPages/src/PublicAPI.Unshipped.txt index 048b7fc544c1..3dc85b737c41 100644 --- a/src/Mvc/Mvc.RazorPages/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.RazorPages/src/PublicAPI.Unshipped.txt @@ -1,3 +1,3 @@ #nullable enable Microsoft.AspNetCore.Builder.PageActionEndpointConventionBuilderResourceCollectionExtensions -static Microsoft.AspNetCore.Builder.PageActionEndpointConventionBuilderResourceCollectionExtensions.WithResourceCollection(this Microsoft.AspNetCore.Builder.PageActionEndpointConventionBuilder! builder, string? manifestPath = null) -> Microsoft.AspNetCore.Builder.PageActionEndpointConventionBuilder! +static Microsoft.AspNetCore.Builder.PageActionEndpointConventionBuilderResourceCollectionExtensions.WithStaticAssets(this Microsoft.AspNetCore.Builder.PageActionEndpointConventionBuilder! builder, string? manifestPath = null) -> Microsoft.AspNetCore.Builder.PageActionEndpointConventionBuilder! diff --git a/src/Mvc/Mvc.RazorPages/test/Builder/PageActionEndpointConventionBuilderResourceCollectionExtensionsTest.cs b/src/Mvc/Mvc.RazorPages/test/Builder/PageActionEndpointConventionBuilderResourceCollectionExtensionsTest.cs index 7e24b4dca64d..c2837e8d6d29 100644 --- a/src/Mvc/Mvc.RazorPages/test/Builder/PageActionEndpointConventionBuilderResourceCollectionExtensionsTest.cs +++ b/src/Mvc/Mvc.RazorPages/test/Builder/PageActionEndpointConventionBuilderResourceCollectionExtensionsTest.cs @@ -18,14 +18,14 @@ namespace Microsoft.AspNetCore.Builder; public class PageActionEndpointConventionBuilderResourceCollectionExtensionsTest { [Fact] - public void WithResourceCollection_AddsEmptyResourceCollection_ToEndpoints_NoStaticAssetsMapped() + public void WithStaticAssets_AddsEmptyResourceCollection_ToEndpoints_NoStaticAssetsMapped() { - // Arrange + // Arrange var endpointBuilder = new TestEndpointRouteBuilder(); var builder = endpointBuilder.MapRazorPages(); // Act - builder.WithResourceCollection(); + builder.WithStaticAssets(); // Assert Assert.All(endpointBuilder.DataSources.First().Endpoints, e => @@ -38,15 +38,15 @@ public void WithResourceCollection_AddsEmptyResourceCollection_ToEndpoints_NoSta } [Fact] - public void WithResourceCollection_AddsEmptyResourceCollection_ToEndpoints_NoMatchingStaticAssetsMapped() + public void WithStaticAssets_AddsEmptyResourceCollection_ToEndpoints_NoMatchingStaticAssetsMapped() { - // Arrange + // Arrange var endpointBuilder = new TestEndpointRouteBuilder(); endpointBuilder.MapStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); var builder = endpointBuilder.MapRazorPages(); // Act - builder.WithResourceCollection(); + builder.WithStaticAssets(); // Assert Assert.All(endpointBuilder.DataSources.Skip(1).First().Endpoints, e => @@ -59,15 +59,15 @@ public void WithResourceCollection_AddsEmptyResourceCollection_ToEndpoints_NoMat } [Fact] - public void WithResourceCollection_AddsResourceCollection_ToEndpoints_NamedManifest() + public void WithStaticAssets_AddsResourceCollection_ToEndpoints_NamedManifest() { - // Arrange + // Arrange var endpointBuilder = new TestEndpointRouteBuilder(); endpointBuilder.MapStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); var builder = endpointBuilder.MapRazorPages(); // Act - builder.WithResourceCollection("TestManifests/Test.staticwebassets.endpoints.json"); + builder.WithStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); // Assert Assert.All(endpointBuilder.DataSources.Skip(1).First().Endpoints, e => @@ -81,15 +81,15 @@ public void WithResourceCollection_AddsResourceCollection_ToEndpoints_NamedManif } [Fact] - public void WithResourceCollection_AddsResourceCollection_ToEndpoints_DefaultManifest() + public void WithStaticAssets_AddsResourceCollection_ToEndpoints_DefaultManifest() { - // Arrange + // Arrange var endpointBuilder = new TestEndpointRouteBuilder(); endpointBuilder.MapStaticAssets(); var builder = endpointBuilder.MapRazorPages(); // Act - builder.WithResourceCollection(); + builder.WithStaticAssets(); // Assert Assert.All(endpointBuilder.DataSources.Skip(1).First().Endpoints, e => @@ -103,18 +103,18 @@ public void WithResourceCollection_AddsResourceCollection_ToEndpoints_DefaultMan } [Fact] - public void WithResourceCollection_AddsDefaultResourceCollectionToEndpoints_WhenNoManifestProvided_EvenIfManyAvailable() + public void WithStaticAssets_AddsDefaultResourceCollectionToEndpoints_WhenNoManifestProvided_EvenIfManyAvailable() { - // Arrange + // Arrange var endpointBuilder = new TestEndpointRouteBuilder(); endpointBuilder.MapStaticAssets(); endpointBuilder.MapStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); var builder = endpointBuilder.MapRazorPages(); // Act - builder.WithResourceCollection(); + builder.WithStaticAssets(); - // Assert + // Assert Assert.All(endpointBuilder.DataSources.Skip(2).First().Endpoints, e => { var metadata = e.Metadata.GetMetadata(); @@ -126,18 +126,18 @@ public void WithResourceCollection_AddsDefaultResourceCollectionToEndpoints_When } [Fact] - public void WithResourceCollection_AddsMatchingResourceCollectionToEndpoints_WhenExplicitManifestProvided_EvenIfManyAvailable() + public void WithStaticAssets_AddsMatchingResourceCollectionToEndpoints_WhenExplicitManifestProvided_EvenIfManyAvailable() { - // Arrange + // Arrange var endpointBuilder = new TestEndpointRouteBuilder(); endpointBuilder.MapStaticAssets(); endpointBuilder.MapStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); var builder = endpointBuilder.MapRazorPages(); // Act - builder.WithResourceCollection("TestManifests/Test.staticwebassets.endpoints.json"); + builder.WithStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); - // Assert + // Assert Assert.All(endpointBuilder.DataSources.Skip(2).First().Endpoints, e => { var metadata = e.Metadata.GetMetadata(); @@ -149,9 +149,9 @@ public void WithResourceCollection_AddsMatchingResourceCollectionToEndpoints_Whe } [Fact] - public void WithResourceCollection_AddsCollectionFromGroup_WhenMappedInsideAnEndpointGroup() + public void WithStaticAssets_AddsCollectionFromGroup_WhenMappedInsideAnEndpointGroup() { - // Arrange + // Arrange var endpointBuilder = new TestEndpointRouteBuilder(); endpointBuilder.MapStaticAssets(); @@ -160,7 +160,7 @@ public void WithResourceCollection_AddsCollectionFromGroup_WhenMappedInsideAnEnd var builder = group.MapRazorPages(); // Act - builder.WithResourceCollection("TestManifests/Test.staticwebassets.endpoints.json"); + builder.WithStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); // Assert var groupEndpoints = Assert.IsAssignableFrom(group).DataSources; @@ -175,9 +175,9 @@ public void WithResourceCollection_AddsCollectionFromGroup_WhenMappedInsideAnEnd } [Fact] - public void WithResourceCollection_AddsEmptyCollectionFromGroup_WhenMappingNotFound_InsideGroup() + public void WithStaticAssets_AddsEmptyCollectionFromGroup_WhenMappingNotFound_InsideGroup() { - // Arrange + // Arrange var endpointBuilder = new TestEndpointRouteBuilder(); endpointBuilder.MapStaticAssets(); @@ -186,7 +186,7 @@ public void WithResourceCollection_AddsEmptyCollectionFromGroup_WhenMappingNotFo var builder = group.MapRazorPages(); // Act - builder.WithResourceCollection(); + builder.WithStaticAssets(); // Assert var groupEndpoints = Assert.IsAssignableFrom(group).DataSources; diff --git a/src/Mvc/Mvc.TagHelpers/test/ImageTagHelperTest.cs b/src/Mvc/Mvc.TagHelpers/test/ImageTagHelperTest.cs index 9199d1d09001..b4af99e52609 100644 --- a/src/Mvc/Mvc.TagHelpers/test/ImageTagHelperTest.cs +++ b/src/Mvc/Mvc.TagHelpers/test/ImageTagHelperTest.cs @@ -165,7 +165,7 @@ public void RendersImageTag_AddsFileVersion() [InlineData("~/images/test-image.png", "/bar", "/bar/images/test-image.fingerprint.png")] [InlineData("/images/test-image.png", null, "/images/test-image.fingerprint.png")] [InlineData("images/test-image.png", null, "images/test-image.fingerprint.png")] - public void RendersImageTag_AddsFileVersion_WithResourceCollection(string src, string pathBase, string expectedValue) + public void RendersImageTag_AddsFileVersion_WithStaticAssets(string src, string pathBase, string expectedValue) { // Arrange var context = MakeTagHelperContext( diff --git a/src/Mvc/Mvc.TagHelpers/test/LinkTagHelperTest.cs b/src/Mvc/Mvc.TagHelpers/test/LinkTagHelperTest.cs index 7668d461604a..0ce563ec4fe4 100644 --- a/src/Mvc/Mvc.TagHelpers/test/LinkTagHelperTest.cs +++ b/src/Mvc/Mvc.TagHelpers/test/LinkTagHelperTest.cs @@ -886,7 +886,7 @@ public void RenderLinkTags_FallbackHref_WithFileVersion() } [Fact] - public void RenderLinkTags_FallbackHref_WithFileVersion_WithResourceCollection() + public void RenderLinkTags_FallbackHref_WithFileVersion_WithStaticAssets() { // Arrange var expectedPostElement = Environment.NewLine + @@ -1054,7 +1054,7 @@ public void RendersLinkTags_GlobbedHref_WithFileVersion() } [Fact] - public void RendersLinkTags_GlobbedHref_WithFileVersion_WithResourceCollection() + public void RendersLinkTags_GlobbedHref_WithFileVersion_WithStaticAssets() { // Arrange var context = MakeTagHelperContext( diff --git a/src/Mvc/Mvc.TagHelpers/test/ScriptTagHelperTest.cs b/src/Mvc/Mvc.TagHelpers/test/ScriptTagHelperTest.cs index b8bac882436d..e6877cf7e8b3 100644 --- a/src/Mvc/Mvc.TagHelpers/test/ScriptTagHelperTest.cs +++ b/src/Mvc/Mvc.TagHelpers/test/ScriptTagHelperTest.cs @@ -865,7 +865,7 @@ public void RenderScriptTags_FallbackSrc_WithFileVersion() } [Fact] - public void RenderScriptTags_FallbackSrc_AppendVersion_WithResourceCollection() + public void RenderScriptTags_FallbackSrc_AppendVersion_WithStaticAssets() { // Arrange var context = MakeTagHelperContext( @@ -987,7 +987,7 @@ public void RenderScriptTags_GlobbedSrc_WithFileVersion() } [Fact] - public void RenderScriptTags_GlobbedSrc_WithFileVersion_WithResourceCollection() + public void RenderScriptTags_GlobbedSrc_WithFileVersion_WithStaticAssets() { // Arrange var expectedContent = "