-
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 10 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,90 @@ | ||
| // 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); | ||
|
|
||
| // Check if builder also implements IEndpointRouteBuilder (like RouteGroupBuilder does) | ||
| if (builder is IEndpointRouteBuilder routeBuilder) | ||
| { | ||
| var convention = new ResourceCollectionConvention(routeBuilder, manifestPath); | ||
| builder.Add(convention.Apply); | ||
| } | ||
|
||
|
|
||
| return builder; | ||
| } | ||
|
|
||
| private sealed class ResourceCollectionConvention | ||
| { | ||
| private readonly IEndpointRouteBuilder _routeBuilder; | ||
| private readonly string? _manifestPath; | ||
| private ResourceAssetCollection? _collection; | ||
| private ResourcePreloadCollection? _preloadCollection; | ||
| private ImportMapDefinition? _importMap; | ||
| private bool _initialized; | ||
|
|
||
| public ResourceCollectionConvention(IEndpointRouteBuilder routeBuilder, string? manifestPath) | ||
| { | ||
| _routeBuilder = routeBuilder; | ||
| _manifestPath = manifestPath; | ||
| } | ||
|
|
||
| public void Apply(EndpointBuilder endpointBuilder) | ||
| { | ||
| // Check if there's already a resource collection on the metadata | ||
| if (endpointBuilder.Metadata.OfType<ResourceAssetCollection>().Any()) | ||
| { | ||
| return; | ||
ilonatommy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // Lazy initialization: resolve collection once on first endpoint | ||
| if (!_initialized) | ||
| { | ||
| _initialized = true; | ||
| var resolver = new ResourceCollectionResolver(_routeBuilder); | ||
|
|
||
| if (resolver.IsRegistered(_manifestPath)) | ||
| { | ||
| _collection = resolver.ResolveResourceCollection(_manifestPath); | ||
| _preloadCollection = new ResourcePreloadCollection(_collection); | ||
| _importMap = ImportMapDefinition.FromResourceCollection(_collection); | ||
| } | ||
| } | ||
|
|
||
| // If collection was resolved, add it to the endpoint | ||
| 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,185 @@ | ||
| // 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 conventionBuilder = new TestRouteGroupBuilder(routeBuilder); | ||
|
|
||
| conventionBuilder.WithStaticAssets(); | ||
|
|
||
| var endpointBuilderInstance = new TestEndpointBuilder(); | ||
| conventionBuilder.ApplyConventions(endpointBuilderInstance); | ||
|
|
||
| var metadata = endpointBuilderInstance.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 conventionBuilder = new TestRouteGroupBuilder(routeBuilder); | ||
|
|
||
| conventionBuilder.WithStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); | ||
|
|
||
| var endpointBuilderInstance = new TestEndpointBuilder(); | ||
| conventionBuilder.ApplyConventions(endpointBuilderInstance); | ||
|
|
||
| var collection = endpointBuilderInstance.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 = endpointBuilderInstance.Metadata.OfType<ResourcePreloadCollection>().FirstOrDefault(); | ||
| Assert.NotNull(preloadCollection); | ||
|
|
||
| var importMap = endpointBuilderInstance.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 conventionBuilder = new TestRouteGroupBuilder(routeBuilder); | ||
|
|
||
| var existingCollection = new ResourceAssetCollection([]); | ||
| var endpointBuilderInstance = new TestEndpointBuilder(); | ||
| endpointBuilderInstance.Metadata.Add(existingCollection); | ||
|
|
||
| conventionBuilder.WithStaticAssets("TestManifests/Test.staticwebassets.endpoints.json"); | ||
| conventionBuilder.ApplyConventions(endpointBuilderInstance); | ||
|
|
||
| var collections = endpointBuilderInstance.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 conventionBuilder = new TestRouteGroupBuilder(routeBuilder); | ||
|
|
||
| conventionBuilder.WithStaticAssets(); | ||
|
|
||
| var endpointBuilderInstance = new TestEndpointBuilder(); | ||
| conventionBuilder.ApplyConventions(endpointBuilderInstance); | ||
|
|
||
| var collection = endpointBuilderInstance.Metadata.OfType<ResourceAssetCollection>().FirstOrDefault(); | ||
| Assert.NotNull(collection); | ||
|
|
||
| var list = Assert.IsAssignableFrom<IReadOnlyList<ResourceAsset>>(collection); | ||
| Assert.Single(list); | ||
| Assert.Equal("default.css", list[0].Url); | ||
| } | ||
|
|
||
| // Test builder that implements both IEndpointConventionBuilder and IEndpointRouteBuilder | ||
| private class TestRouteGroupBuilder : IEndpointConventionBuilder, IEndpointRouteBuilder | ||
| { | ||
| private readonly TestEndpointRouteBuilder _routeBuilder; | ||
| private readonly List<Action<EndpointBuilder>> _conventions = []; | ||
|
|
||
| public TestRouteGroupBuilder(TestEndpointRouteBuilder routeBuilder) | ||
| { | ||
| _routeBuilder = routeBuilder; | ||
| } | ||
|
|
||
| public void Add(Action<EndpointBuilder> convention) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(convention); | ||
| _conventions.Add(convention); | ||
| } | ||
|
|
||
| public void ApplyConventions(EndpointBuilder endpointBuilder) | ||
| { | ||
| foreach (var convention in _conventions) | ||
| { | ||
| convention(endpointBuilder); | ||
| } | ||
| } | ||
|
|
||
| public IServiceProvider ServiceProvider => _routeBuilder.ServiceProvider; | ||
| public ICollection<EndpointDataSource> DataSources => _routeBuilder.DataSources; | ||
| public IApplicationBuilder CreateApplicationBuilder() => _routeBuilder.CreateApplicationBuilder(); | ||
| } | ||
|
|
||
| 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<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.