diff --git a/src/Components/Endpoints/src/Builder/EndpointConventionBuilderResourceCollectionExtensions.cs b/src/Components/Endpoints/src/Builder/EndpointConventionBuilderResourceCollectionExtensions.cs
new file mode 100644
index 000000000000..5e215bac7a58
--- /dev/null
+++ b/src/Components/Endpoints/src/Builder/EndpointConventionBuilderResourceCollectionExtensions.cs
@@ -0,0 +1,84 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Linq;
+using Microsoft.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 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 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().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!);
+ }
+ }
+ }
+}
\ 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 ac1780ba883e..0f575d8b05a9 100644
--- a/src/Components/Endpoints/src/PublicAPI.Unshipped.txt
+++ b/src/Components/Endpoints/src/PublicAPI.Unshipped.txt
@@ -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
diff --git a/src/Components/Endpoints/test/Builder/EndpointConventionBuilderResourceCollectionExtensionsTest.cs b/src/Components/Endpoints/test/Builder/EndpointConventionBuilderResourceCollectionExtensionsTest.cs
new file mode 100644
index 000000000000..75ea374002aa
--- /dev/null
+++ b/src/Components/Endpoints/test/Builder/EndpointConventionBuilderResourceCollectionExtensionsTest.cs
@@ -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");
+
+ group.WithStaticAssets();
+
+ var endpointBuilder = new TestEndpointBuilder(routeBuilder);
+ ApplyConventions(group, endpointBuilder);
+
+ var metadata = endpointBuilder.Metadata.OfType().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().FirstOrDefault();
+ Assert.NotNull(collection);
+
+ var list = Assert.IsAssignableFrom>(collection);
+ Assert.Single(list);
+ Assert.Equal("named.css", list[0].Url);
+
+ var preloadCollection = endpointBuilder.Metadata.OfType().FirstOrDefault();
+ Assert.NotNull(preloadCollection);
+
+ var importMap = endpointBuilder.Metadata.OfType().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().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().FirstOrDefault();
+ Assert.NotNull(collection);
+
+ var list = Assert.IsAssignableFrom>(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().FirstOrDefault();
+ Assert.NotNull(collection);
+
+ var list = Assert.IsAssignableFrom>(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().FirstOrDefault();
+ Assert.NotNull(collection);
+
+ var list = Assert.IsAssignableFrom>(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>)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>)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 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(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