Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;

/// <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="endpoints">The <see cref="IEndpointRouteBuilder"/> to resolve static assets from.</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 TBuilder WithStaticAssets<TBuilder>(
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<ResourceAssetCollection>().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;
}
}
2 changes: 2 additions & 0 deletions src/Components/Endpoints/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#nullable enable
Microsoft.AspNetCore.Builder.EndpointConventionBuilderResourceCollectionExtensions
static Microsoft.AspNetCore.Builder.EndpointConventionBuilderResourceCollectionExtensions.WithStaticAssets<TBuilder>(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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ResourceAssetCollection>().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<ResourceAssetCollection>().FirstOrDefault();
Assert.NotNull(collection);

var list = Assert.IsAssignableFrom<IReadOnlyList<ResourceAsset>>(collection);
Assert.Single(list);
Assert.Equal("named.css", list[0].Url);

// Verify other metadata is also added
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()
{
// 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<ResourceAssetCollection>().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<ResourceAssetCollection>().FirstOrDefault();
Assert.NotNull(collection);

var list = Assert.IsAssignableFrom<IReadOnlyList<ResourceAsset>>(collection);
Assert.Single(list);
Assert.Equal("default.css", list[0].Url);
}

private class TestEndpointConventionBuilder : IEndpointConventionBuilder
{
private readonly List<Action<EndpointBuilder>> _conventions = [];

public void Add(Action<EndpointBuilder> 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<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;
}
}
}
}
Loading