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,13 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Components.Endpoints;

/// <summary>
/// Metadata that identifies infrastructure endpoints for Blazor framework functionality.
/// This marker is used to distinguish framework endpoints (like opaque redirection,
/// disconnect, and JavaScript initializers) from regular component endpoints.
/// </summary>
public sealed class ComponentFrameworkEndpointMetadata
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public static EndpointBuilder GetBlazorOpaqueRedirectionEndpoint()
};

routeEndpointBuidler.Metadata.Add(new HttpMethodMetadata([HttpMethods.Get]));
routeEndpointBuidler.Metadata.Add(new ComponentFrameworkEndpointMetadata());

return routeEndpointBuidler;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// 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 System.Reflection;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Endpoints;
Expand Down Expand Up @@ -63,4 +64,35 @@ public static RazorComponentsEndpointConventionBuilder WithStaticAssets(

return builder;
}

/// <summary>
/// Configures framework endpoints for the Blazor application. Framework endpoints are internal
/// infrastructure endpoints such as opaque redirection, disconnect, and initializers.
/// </summary>
/// <param name="builder">The <see cref="RazorComponentsEndpointConventionBuilder"/>.</param>
/// <param name="configureEndpoints">A callback to configure framework endpoints.</param>
/// <returns>The <see cref="RazorComponentsEndpointConventionBuilder"/>.</returns>
/// <remarks>
/// This method provides a way to apply specific conventions or metadata to Blazor's infrastructure
/// endpoints without affecting regular component endpoints. Framework endpoints are identified by
/// the presence of <see cref="ComponentFrameworkEndpointMetadata"/> in their metadata collection.
/// </remarks>
public static RazorComponentsEndpointConventionBuilder ConfigureFrameworkEndpoints(
this RazorComponentsEndpointConventionBuilder builder,
Action<EndpointBuilder> configureEndpoints)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(configureEndpoints);

builder.Add(endpointBuilder =>
{
// Only apply configuration to endpoints that have ComponentFrameworkEndpointMetadata
if (endpointBuilder.Metadata.OfType<ComponentFrameworkEndpointMetadata>().Any())
{
configureEndpoints(endpointBuilder);
}
});

return builder;
}
}
3 changes: 3 additions & 0 deletions src/Components/Endpoints/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
#nullable enable
Microsoft.AspNetCore.Components.Endpoints.ComponentFrameworkEndpointMetadata
Microsoft.AspNetCore.Components.Endpoints.ComponentFrameworkEndpointMetadata.ComponentFrameworkEndpointMetadata() -> void
Microsoft.AspNetCore.Components.ResourcePreloader
Microsoft.AspNetCore.Components.ResourcePreloader.ResourcePreloader() -> void
Microsoft.Extensions.DependencyInjection.RazorComponentsRazorComponentBuilderExtensions
static Microsoft.AspNetCore.Builder.RazorComponentsEndpointConventionBuilderExtensions.ConfigureFrameworkEndpoints(this Microsoft.AspNetCore.Builder.RazorComponentsEndpointConventionBuilder! builder, System.Action<Microsoft.AspNetCore.Builder.EndpointBuilder!>! configureEndpoints) -> Microsoft.AspNetCore.Builder.RazorComponentsEndpointConventionBuilder!
static Microsoft.Extensions.DependencyInjection.RazorComponentsRazorComponentBuilderExtensions.RegisterPersistentService<TPersistentService>(this Microsoft.Extensions.DependencyInjection.IRazorComponentsBuilder! builder, Microsoft.AspNetCore.Components.IComponentRenderMode! renderMode) -> Microsoft.Extensions.DependencyInjection.IRazorComponentsBuilder!
48 changes: 48 additions & 0 deletions src/Components/Endpoints/test/Builder/OpaqueRedirectionTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// 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.Components.Endpoints;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Xunit;

namespace Microsoft.AspNetCore.Components.Tests.Builder;

public class OpaqueRedirectionTest
{
[Fact]
public void GetBlazorOpaqueRedirectionEndpoint_ContainsComponentFrameworkEndpointMetadata()
{
// Arrange & Act
var endpointBuilder = OpaqueRedirection.GetBlazorOpaqueRedirectionEndpoint();
var endpoint = endpointBuilder.Build();

// Assert
var metadata = endpoint.Metadata.GetMetadata<ComponentFrameworkEndpointMetadata>();
Assert.NotNull(metadata);
}

[Fact]
public void GetBlazorOpaqueRedirectionEndpoint_HasCorrectDisplayName()
{
// Arrange & Act
var endpointBuilder = OpaqueRedirection.GetBlazorOpaqueRedirectionEndpoint();
var endpoint = endpointBuilder.Build();

// Assert
Assert.Equal("Blazor Opaque Redirection", endpoint.DisplayName);
}

[Fact]
public void GetBlazorOpaqueRedirectionEndpoint_HasHttpGetMethod()
{
// Arrange & Act
var endpointBuilder = OpaqueRedirection.GetBlazorOpaqueRedirectionEndpoint();
var endpoint = endpointBuilder.Build();

// Assert
var httpMethodMetadata = endpoint.Metadata.GetMetadata<HttpMethodMetadata>();
Assert.NotNull(httpMethodMetadata);
Assert.Contains(HttpMethods.Get, httpMethodMetadata.HttpMethods);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,59 @@ public override void Write(string name, object value) { }
}
}

[Fact]
public void ConfigureFrameworkEndpoints_AppliesConfigurationOnlyToFrameworkEndpoints()
{
// Arrange
var endpointBuilder = new TestEndpointRouteBuilder();
var builder = CreateRazorComponentsAppBuilder(endpointBuilder);
var configuredEndpoints = new List<string>();

// Act
builder.ConfigureFrameworkEndpoints(endpointBuilder =>
{
configuredEndpoints.Add(endpointBuilder.DisplayName ?? "Unknown");
});

// Create endpoints to trigger configuration
var endpoints = endpointBuilder.DataSources.First().Endpoints.ToList();

// Assert - no endpoints should be configured since we haven't added framework metadata
Assert.Empty(configuredEndpoints);
}

[Fact]
public void ConfigureFrameworkEndpoints_OnlyConfiguresEndpointsWithFrameworkMetadata()
{
// Arrange
var endpointBuilder = new TestEndpointRouteBuilder();
var builder = CreateRazorComponentsAppBuilder(endpointBuilder);
var configuredEndpoints = new List<string>();

builder.Add(endpoint =>
{
// Simulate framework endpoint by adding the metadata
if (endpoint.DisplayName == "TestFrameworkEndpoint")
{
endpoint.Metadata.Add(new ComponentFrameworkEndpointMetadata());
}
endpoint.DisplayName ??= "TestFrameworkEndpoint";
});

// Act
builder.ConfigureFrameworkEndpoints(endpointBuilder =>
{
configuredEndpoints.Add(endpointBuilder.DisplayName ?? "Unknown");
});

// Create endpoints to trigger configuration
var endpoints = endpointBuilder.DataSources.First().Endpoints.ToList();

// Assert
Assert.Single(configuredEndpoints);
Assert.Contains("TestFrameworkEndpoint", configuredEndpoints);
}

private class App : IComponent
{
void IComponent.Attach(RenderHandle renderHandle) => throw new NotImplementedException();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// 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.Components.Endpoints;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.AspNetCore.Routing;
Expand Down Expand Up @@ -78,12 +79,14 @@ public static ComponentEndpointConventionBuilder MapBlazorHub(
var disconnectEndpoint = endpoints.Map(
(path.EndsWith('/') ? path : path + "/") + "disconnect/",
endpoints.CreateApplicationBuilder().UseMiddleware<CircuitDisconnectMiddleware>().Build())
.WithDisplayName("Blazor disconnect");
.WithDisplayName("Blazor disconnect")
.WithMetadata(new ComponentFrameworkEndpointMetadata());

var jsInitializersEndpoint = endpoints.Map(
(path.EndsWith('/') ? path : path + "/") + "initializers/",
endpoints.CreateApplicationBuilder().UseMiddleware<CircuitJavaScriptInitializationMiddleware>().Build())
.WithDisplayName("Blazor initializers");
.WithDisplayName("Blazor initializers")
.WithMetadata(new ComponentFrameworkEndpointMetadata());

return new ComponentEndpointConventionBuilder(hubEndpoint, disconnectEndpoint, jsInitializersEndpoint);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Diagnostics;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components.Endpoints;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
Expand Down Expand Up @@ -122,6 +123,38 @@ public void MapBlazorHub_AppliesFinalConventionsInFIFOOrder()
Assert.Equal(new[] { "first-in", "last-in" }, populatedMetadata);
}

[Fact]
public void MapBlazorHub_AddsComponentFrameworkEndpointMetadata()
{
// Arrange
var applicationBuilder = CreateAppBuilder();
var frameworkEndpoints = new List<string>();

// Act
var app = applicationBuilder
.UseRouting()
.UseEndpoints(endpoints =>
{
endpoints
.MapBlazorHub()
.Finally(builder =>
{
// Check if metadata collection contains ComponentFrameworkEndpointMetadata
if (builder.Metadata.OfType<ComponentFrameworkEndpointMetadata>().Any())
{
frameworkEndpoints.Add(builder.DisplayName);
}
});
}).Build();

// Trigger endpoint construction
app.Invoke(new DefaultHttpContext());

// Assert
Assert.Contains("Blazor disconnect", frameworkEndpoints);
Assert.Contains("Blazor initializers", frameworkEndpoints);
}

private IApplicationBuilder CreateAppBuilder()
{
var environment = new Mock<IWebHostEnvironment>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Endpoints;
using Microsoft.AspNetCore.Components.Endpoints.Infrastructure;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Http;
Expand Down Expand Up @@ -41,7 +42,8 @@ public override IEnumerable<RouteEndpointBuilder> GetEndpointBuilders(IComponent
context.SetEndpoint(null);

return app(context);
});
})
.WithMetadata(new ComponentFrameworkEndpointMetadata());

return endpointRouteBuilder.GetEndpoints();
}
Expand Down Expand Up @@ -75,6 +77,22 @@ internal IEnumerable<RouteEndpointBuilder> GetEndpoints()
builder.Metadata.Add(metadata);
}

// Add framework endpoint metadata if not already present
var hasFrameworkMetadata = false;
for (var i = 0; i < builder.Metadata.Count; i++)
{
if (builder.Metadata[i] is ComponentFrameworkEndpointMetadata)
{
hasFrameworkMetadata = true;
break;
}
}

if (!hasFrameworkMetadata)
{
builder.Metadata.Add(new ComponentFrameworkEndpointMetadata());
}

yield return builder;
}
}
Expand Down
Loading