-
Notifications
You must be signed in to change notification settings - Fork 10.5k
[Blazor] Add WithStaticAssets extension for IEndpointConventionBuilder #63820
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c0a3fb5
82829a4
9265687
b9968c5
29aa220
c8cb876
37575d5
8ee7f08
919baf5
3b229a1
a65ade6
c2be237
a563dfa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.Linq; | ||
| using Microsoft.AspNetCore.Components; | ||
| using Microsoft.AspNetCore.Components.Endpoints; | ||
| using Microsoft.AspNetCore.Routing; | ||
|
|
||
| namespace Microsoft.AspNetCore.Builder; | ||
|
|
||
| /// <summary> | ||
| /// Extensions for <see cref="IEndpointConventionBuilder"/> to add resource collection metadata. | ||
| /// </summary> | ||
| public static class EndpointConventionBuilderResourceCollectionExtensions | ||
| { | ||
| /// <summary> | ||
| /// Provides a helper to attach ResourceCollection metadata to endpoints. | ||
| /// </summary> | ||
| /// <param name="builder">The <see cref="IEndpointConventionBuilder"/>.</param> | ||
| /// <param name="manifestPath">The manifest associated with the assets.</param> | ||
| /// <returns>The <see cref="IEndpointConventionBuilder"/> that can be used to further configure the endpoints.</returns> | ||
| /// <remarks> | ||
| /// This method attaches static asset metadata to endpoints. It provides a simplified way to add | ||
| /// resource collection metadata to any endpoint convention builder. | ||
| /// The <paramref name="manifestPath"/> must match the path of the manifest file provided to | ||
| /// the <see cref="StaticAssetsEndpointRouteBuilderExtensions.MapStaticAssets(IEndpointRouteBuilder, string?)"/> call. | ||
|
||
| /// </remarks> | ||
| public static IEndpointConventionBuilder WithStaticAssets( | ||
| this IEndpointConventionBuilder builder, | ||
| string? manifestPath = null) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(builder); | ||
|
|
||
| ResourceCollectionConvention convention = new ResourceCollectionConvention(manifestPath); | ||
|
|
||
| builder.Add(convention.Apply); | ||
|
|
||
| return builder; | ||
| } | ||
|
|
||
| private sealed class ResourceCollectionConvention | ||
| { | ||
| private readonly string? _manifestPath; | ||
| private ResourceAssetCollection? _collection; | ||
| private ResourcePreloadCollection? _preloadCollection; | ||
| private ImportMapDefinition? _importMap; | ||
|
|
||
| public ResourceCollectionConvention(string? manifestPath) | ||
| { | ||
| _manifestPath = manifestPath; | ||
| } | ||
|
|
||
| public void Apply(EndpointBuilder endpointBuilder) | ||
| { | ||
| // Check if there's already a resource collection on the metadata | ||
| if (endpointBuilder.Metadata.OfType<ResourceAssetCollection>().Any() || | ||
| endpointBuilder is not IEndpointRouteBuilder routeEndpointBuilder) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| if (_collection == null) | ||
| { | ||
| // We only use the resolver to get to the datasources so we can cache the results for | ||
| // all endpoints in the collection | ||
| var resolver = new ResourceCollectionResolver(routeEndpointBuilder); | ||
|
|
||
| if (resolver.IsRegistered(_manifestPath)) | ||
| { | ||
| _collection = resolver.ResolveResourceCollection(_manifestPath); | ||
| _preloadCollection = new ResourcePreloadCollection(_collection); | ||
| _importMap = ImportMapDefinition.FromResourceCollection(_collection); | ||
| } | ||
| } | ||
|
|
||
| if (_collection != null) | ||
| { | ||
| endpointBuilder.Metadata.Add(_collection); | ||
| endpointBuilder.Metadata.Add(_preloadCollection!); | ||
| endpointBuilder.Metadata.Add(_importMap!); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| #nullable enable | ||
| Microsoft.AspNetCore.Builder.EndpointConventionBuilderResourceCollectionExtensions | ||
| static Microsoft.AspNetCore.Builder.EndpointConventionBuilderResourceCollectionExtensions.WithStaticAssets(this Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! builder, string? manifestPath = null) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! | ||
| Microsoft.AspNetCore.Components.Endpoints.BasePath | ||
| Microsoft.AspNetCore.Components.Endpoints.BasePath.BasePath() -> void |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,234 @@ | ||
| // 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() | ||
| { | ||
| var routeBuilder = new TestEndpointRouteBuilder(); | ||
| var group = routeBuilder.MapGroup("/test"); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @copilot Add some tests that use MapGet too
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added 2 new MapGet tests in commit a563dfa:
All 6 tests pass. |
||
|
|
||
| group.WithStaticAssets(); | ||
|
|
||
| var endpointBuilder = new TestEndpointBuilder(routeBuilder); | ||
| ApplyConventions(group, endpointBuilder); | ||
|
|
||
| var metadata = endpointBuilder.Metadata.OfType<ResourceAssetCollection>().FirstOrDefault(); | ||
| Assert.Null(metadata); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void WithStaticAssets_AddsResourceCollection_ToEndpoints_WithMatchingManifest() | ||
| { | ||
| var routeBuilder = new TestEndpointRouteBuilder(); | ||
| routeBuilder.MapStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); | ||
| var group = routeBuilder.MapGroup("/test"); | ||
|
|
||
| group.WithStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); | ||
|
|
||
| var endpointBuilder = new TestEndpointBuilder(routeBuilder); | ||
| ApplyConventions(group, endpointBuilder); | ||
|
|
||
| var collection = endpointBuilder.Metadata.OfType<ResourceAssetCollection>().FirstOrDefault(); | ||
| Assert.NotNull(collection); | ||
|
|
||
| var list = Assert.IsAssignableFrom<IReadOnlyList<ResourceAsset>>(collection); | ||
| Assert.Single(list); | ||
| Assert.Equal("named.css", list[0].Url); | ||
|
|
||
| var preloadCollection = endpointBuilder.Metadata.OfType<ResourcePreloadCollection>().FirstOrDefault(); | ||
| Assert.NotNull(preloadCollection); | ||
|
|
||
| var importMap = endpointBuilder.Metadata.OfType<ImportMapDefinition>().FirstOrDefault(); | ||
| Assert.NotNull(importMap); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void WithStaticAssets_DoesNotAddResourceCollection_WhenAlreadyExists() | ||
| { | ||
| var routeBuilder = new TestEndpointRouteBuilder(); | ||
| routeBuilder.MapStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); | ||
| var group = routeBuilder.MapGroup("/test"); | ||
|
|
||
| var existingCollection = new ResourceAssetCollection([]); | ||
| var endpointBuilder = new TestEndpointBuilder(routeBuilder); | ||
| endpointBuilder.Metadata.Add(existingCollection); | ||
|
|
||
| group.WithStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); | ||
| ApplyConventions(group, endpointBuilder); | ||
|
|
||
| var collections = endpointBuilder.Metadata.OfType<ResourceAssetCollection>().ToList(); | ||
| Assert.Single(collections); | ||
| Assert.Same(existingCollection, collections[0]); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void WithStaticAssets_AddsResourceCollection_ToEndpoints_DefaultManifest() | ||
| { | ||
| var routeBuilder = new TestEndpointRouteBuilder(); | ||
| routeBuilder.MapStaticAssets(); | ||
| var group = routeBuilder.MapGroup("/test"); | ||
|
|
||
| group.WithStaticAssets(); | ||
|
|
||
| var endpointBuilder = new TestEndpointBuilder(routeBuilder); | ||
| ApplyConventions(group, endpointBuilder); | ||
|
|
||
| var collection = endpointBuilder.Metadata.OfType<ResourceAssetCollection>().FirstOrDefault(); | ||
| Assert.NotNull(collection); | ||
|
|
||
| var list = Assert.IsAssignableFrom<IReadOnlyList<ResourceAsset>>(collection); | ||
| Assert.Single(list); | ||
| Assert.Equal("default.css", list[0].Url); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void WithStaticAssets_OnMapGet_AddsResourceCollection_WhenEndpointBuilderImplementsIEndpointRouteBuilder() | ||
| { | ||
| var routeBuilder = new TestEndpointRouteBuilder(); | ||
| routeBuilder.MapStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); | ||
|
|
||
| var mapGet = routeBuilder.MapGet("/endpoint", () => "test"); | ||
| mapGet.WithStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); | ||
|
|
||
| // When conventions are applied to an endpoint builder that implements IEndpointRouteBuilder, | ||
| // the metadata is added even if the convention was added via MapGet | ||
| var endpointBuilder = new TestEndpointBuilder(routeBuilder); | ||
| ApplyConventions(mapGet, endpointBuilder); | ||
|
|
||
| var collection = endpointBuilder.Metadata.OfType<ResourceAssetCollection>().FirstOrDefault(); | ||
| Assert.NotNull(collection); | ||
|
|
||
| var list = Assert.IsAssignableFrom<IReadOnlyList<ResourceAsset>>(collection); | ||
| Assert.Single(list); | ||
| Assert.Equal("named.css", list[0].Url); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void WithStaticAssets_OnMapGetInGroup_AddsResourceCollection() | ||
| { | ||
| var routeBuilder = new TestEndpointRouteBuilder(); | ||
| routeBuilder.MapStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); | ||
|
|
||
| var group = routeBuilder.MapGroup("/test"); | ||
| group.WithStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); | ||
|
|
||
| var mapGet = group.MapGet("/endpoint", () => "test"); | ||
|
|
||
| var endpointBuilder = new TestEndpointBuilder(routeBuilder); | ||
| ApplyConventions(group, endpointBuilder); | ||
|
|
||
| // When MapGet is inside a group that has WithStaticAssets, the metadata is added | ||
| var collection = endpointBuilder.Metadata.OfType<ResourceAssetCollection>().FirstOrDefault(); | ||
| Assert.NotNull(collection); | ||
|
|
||
| var list = Assert.IsAssignableFrom<IReadOnlyList<ResourceAsset>>(collection); | ||
| Assert.Single(list); | ||
| Assert.Equal("named.css", list[0].Url); | ||
| } | ||
|
|
||
| private static void ApplyConventions(RouteGroupBuilder group, EndpointBuilder endpointBuilder) | ||
| { | ||
| // Access conventions via reflection since they're private | ||
| var conventionsField = typeof(RouteGroupBuilder).GetField("_conventions", | ||
| System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); | ||
| var conventions = (List<Action<EndpointBuilder>>)conventionsField!.GetValue(group)!; | ||
|
|
||
| foreach (var convention in conventions) | ||
| { | ||
| convention(endpointBuilder); | ||
| } | ||
| } | ||
|
|
||
| private static void ApplyConventions(RouteHandlerBuilder handler, EndpointBuilder endpointBuilder) | ||
| { | ||
| // Access conventions via reflection since they're private | ||
| var conventionsField = typeof(RouteHandlerBuilder).GetField("_conventions", | ||
| System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); | ||
| var conventions = (List<Action<EndpointBuilder>>)conventionsField!.GetValue(handler)!; | ||
|
|
||
| foreach (var convention in conventions) | ||
| { | ||
| convention(endpointBuilder); | ||
| } | ||
| } | ||
|
|
||
| // Test builder that implements both EndpointBuilder and IEndpointRouteBuilder | ||
| private class TestEndpointBuilder : EndpointBuilder, IEndpointRouteBuilder | ||
| { | ||
| private readonly IEndpointRouteBuilder _routeBuilder; | ||
|
|
||
| public TestEndpointBuilder(IEndpointRouteBuilder routeBuilder) | ||
| { | ||
| _routeBuilder = routeBuilder; | ||
| ApplicationServices = TestEndpointRouteBuilder.CreateServiceProvider(); | ||
| } | ||
|
|
||
| public override Endpoint Build() | ||
| { | ||
| throw new NotImplementedException(); | ||
| } | ||
|
|
||
| public IServiceProvider ServiceProvider => _routeBuilder.ServiceProvider; | ||
| public ICollection<EndpointDataSource> DataSources => _routeBuilder.DataSources; | ||
| public IApplicationBuilder CreateApplicationBuilder() => _routeBuilder.CreateApplicationBuilder(); | ||
| } | ||
|
|
||
| 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<IConfiguration>(new ConfigurationBuilder().Build()); | ||
| collection.AddSingleton<IWebHostEnvironment>(new TestWebHostEnvironment()); | ||
| collection.AddRazorComponents(); | ||
| return collection.BuildServiceProvider(); | ||
| } | ||
|
|
||
| public ICollection<EndpointDataSource> 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; | ||
| } | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The file is named
EndpointRouteBuilderResourceCollectionExtensions.csbut contains a class namedEndpointConventionBuilderResourceCollectionExtensions. The file name should match the class name. Consider renaming the file toEndpointConventionBuilderResourceCollectionExtensions.csto follow C# conventions where file names match the primary type they contain.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in commit 37575d5. Renamed file from
EndpointRouteBuilderResourceCollectionExtensions.cstoEndpointConventionBuilderResourceCollectionExtensions.csto match the class name.