From c0a3fb5e46a6596149ee6dafb229d2baac9815fb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Sep 2025 11:38:08 +0000 Subject: [PATCH 1/2] Initial plan From 82829a4e2f4c4ed6936c0992f5505f350aee7962 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Sep 2025 11:51:18 +0000 Subject: [PATCH 2/2] Add EndpointConventionBuilderResourceCollectionExtensions helper Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- ...outeBuilderResourceCollectionExtensions.cs | 62 ++++++ .../Endpoints/src/PublicAPI.Unshipped.txt | 2 + ...BuilderResourceCollectionExtensionsTest.cs | 187 ++++++++++++++++++ 3 files changed, 251 insertions(+) create mode 100644 src/Components/Endpoints/src/Builder/EndpointRouteBuilderResourceCollectionExtensions.cs create mode 100644 src/Components/Endpoints/test/Builder/EndpointConventionBuilderResourceCollectionExtensionsTest.cs diff --git a/src/Components/Endpoints/src/Builder/EndpointRouteBuilderResourceCollectionExtensions.cs b/src/Components/Endpoints/src/Builder/EndpointRouteBuilderResourceCollectionExtensions.cs new file mode 100644 index 000000000000..3ae033a72223 --- /dev/null +++ b/src/Components/Endpoints/src/Builder/EndpointRouteBuilderResourceCollectionExtensions.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.Linq; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Endpoints; +using Microsoft.AspNetCore.Routing; + +namespace Microsoft.AspNetCore.Builder; + +/// +/// Extensions for to add resource collection metadata. +/// +public static class EndpointConventionBuilderResourceCollectionExtensions +{ + /// + /// Provides a helper to attach ResourceCollection metadata to endpoints. + /// + /// The . + /// The to resolve static assets from. + /// The manifest associated with the assets. + /// The that can be used to further configure the endpoints. + /// + /// This method attaches static asset metadata to endpoints. It provides a simplified way to add + /// resource collection metadata to any endpoint convention builder. + /// The must match the path of the manifest file provided to + /// the call. + /// + public static TBuilder WithStaticAssets( + this TBuilder builder, + IEndpointRouteBuilder endpoints, + string? manifestPath = null) + where TBuilder : IEndpointConventionBuilder + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(endpoints); + + var resolver = new ResourceCollectionResolver(endpoints); + + builder.Add(endpointBuilder => + { + // Check if there's already a resource collection on the metadata + if (endpointBuilder.Metadata.OfType().Any()) + { + return; + } + + // Only add metadata if static assets are registered + if (resolver.IsRegistered(manifestPath)) + { + var collection = resolver.ResolveResourceCollection(manifestPath); + var importMap = ImportMapDefinition.FromResourceCollection(collection); + + endpointBuilder.Metadata.Add(collection); + endpointBuilder.Metadata.Add(new ResourcePreloadCollection(collection)); + endpointBuilder.Metadata.Add(importMap); + } + }); + + return builder; + } +} \ No newline at end of file diff --git a/src/Components/Endpoints/src/PublicAPI.Unshipped.txt b/src/Components/Endpoints/src/PublicAPI.Unshipped.txt index 424ca155339b..3034f18f89aa 100644 --- a/src/Components/Endpoints/src/PublicAPI.Unshipped.txt +++ b/src/Components/Endpoints/src/PublicAPI.Unshipped.txt @@ -1,4 +1,6 @@ #nullable enable +Microsoft.AspNetCore.Builder.EndpointConventionBuilderResourceCollectionExtensions +static Microsoft.AspNetCore.Builder.EndpointConventionBuilderResourceCollectionExtensions.WithStaticAssets(this TBuilder builder, Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string? manifestPath = null) -> TBuilder Microsoft.AspNetCore.Components.ResourcePreloader Microsoft.AspNetCore.Components.ResourcePreloader.ResourcePreloader() -> void Microsoft.Extensions.DependencyInjection.RazorComponentsRazorComponentBuilderExtensions diff --git a/src/Components/Endpoints/test/Builder/EndpointConventionBuilderResourceCollectionExtensionsTest.cs b/src/Components/Endpoints/test/Builder/EndpointConventionBuilderResourceCollectionExtensionsTest.cs new file mode 100644 index 000000000000..2ce2cf100ad3 --- /dev/null +++ b/src/Components/Endpoints/test/Builder/EndpointConventionBuilderResourceCollectionExtensionsTest.cs @@ -0,0 +1,187 @@ +// 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; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; + +namespace Microsoft.AspNetCore.Components.Endpoints.Tests.Builder; + +public class EndpointConventionBuilderResourceCollectionExtensionsTest +{ + [Fact] + public void WithStaticAssets_DoesNotAddResourceCollection_ToEndpoints_NoStaticAssetsMapped() + { + // Arrange + var endpointBuilder = new TestEndpointRouteBuilder(); + var conventionBuilder = new TestEndpointConventionBuilder(); + + // Act + conventionBuilder.WithStaticAssets(endpointBuilder); + + // Assert + var endpointBuilderInstance = new TestEndpointBuilder(); + conventionBuilder.ApplyConventions(endpointBuilderInstance); + + var metadata = endpointBuilderInstance.Metadata.OfType().FirstOrDefault(); + Assert.Null(metadata); + } + + [Fact] + public void WithStaticAssets_AddsResourceCollection_ToEndpoints_WithMatchingManifest() + { + // Arrange + var endpointBuilder = new TestEndpointRouteBuilder(); + endpointBuilder.MapStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); + var conventionBuilder = new TestEndpointConventionBuilder(); + + // Act + conventionBuilder.WithStaticAssets(endpointBuilder, "TestManifests/Test.staticwebassets.endpoints.json"); + + // Assert + var endpointBuilderInstance = new TestEndpointBuilder(); + conventionBuilder.ApplyConventions(endpointBuilderInstance); + + var collection = endpointBuilderInstance.Metadata.OfType().FirstOrDefault(); + Assert.NotNull(collection); + + var list = Assert.IsAssignableFrom>(collection); + Assert.Single(list); + Assert.Equal("named.css", list[0].Url); + + // Verify other metadata is also added + var preloadCollection = endpointBuilderInstance.Metadata.OfType().FirstOrDefault(); + Assert.NotNull(preloadCollection); + + var importMap = endpointBuilderInstance.Metadata.OfType().FirstOrDefault(); + Assert.NotNull(importMap); + } + + [Fact] + public void WithStaticAssets_DoesNotAddResourceCollection_WhenAlreadyExists() + { + // Arrange + var endpointBuilder = new TestEndpointRouteBuilder(); + endpointBuilder.MapStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); + var conventionBuilder = new TestEndpointConventionBuilder(); + + var existingCollection = new ResourceAssetCollection([]); + var endpointBuilderInstance = new TestEndpointBuilder(); + endpointBuilderInstance.Metadata.Add(existingCollection); + + // Act + conventionBuilder.WithStaticAssets(endpointBuilder, "TestManifests/Test.staticwebassets.endpoints.json"); + conventionBuilder.ApplyConventions(endpointBuilderInstance); + + // Assert + var collections = endpointBuilderInstance.Metadata.OfType().ToList(); + Assert.Single(collections); + Assert.Same(existingCollection, collections[0]); + } + + [Fact] + public void WithStaticAssets_AddsResourceCollection_ToEndpoints_DefaultManifest() + { + // Arrange + var endpointBuilder = new TestEndpointRouteBuilder(); + endpointBuilder.MapStaticAssets(); + var conventionBuilder = new TestEndpointConventionBuilder(); + + // Act + conventionBuilder.WithStaticAssets(endpointBuilder); + + // Assert + var endpointBuilderInstance = new TestEndpointBuilder(); + conventionBuilder.ApplyConventions(endpointBuilderInstance); + + var collection = endpointBuilderInstance.Metadata.OfType().FirstOrDefault(); + Assert.NotNull(collection); + + var list = Assert.IsAssignableFrom>(collection); + Assert.Single(list); + Assert.Equal("default.css", list[0].Url); + } + + private class TestEndpointConventionBuilder : IEndpointConventionBuilder + { + private readonly List> _conventions = []; + + public void Add(Action convention) + { + ArgumentNullException.ThrowIfNull(convention); + _conventions.Add(convention); + } + + public void ApplyConventions(EndpointBuilder endpointBuilder) + { + foreach (var convention in _conventions) + { + convention(endpointBuilder); + } + } + } + + private class TestEndpointBuilder : EndpointBuilder + { + public TestEndpointBuilder() + { + ApplicationServices = TestEndpointRouteBuilder.CreateServiceProvider(); + } + + public override Endpoint Build() + { + throw new NotImplementedException(); + } + } + + private class TestEndpointRouteBuilder : IEndpointRouteBuilder + { + private readonly ApplicationBuilder _applicationBuilder; + + public TestEndpointRouteBuilder() + { + _applicationBuilder = new ApplicationBuilder(ServiceProvider); + } + + public IServiceProvider ServiceProvider { get; } = CreateServiceProvider(); + + public 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; + } + } + } +} \ No newline at end of file