From b17b4a5edddc5d4d9d96630dfb727b73cfeda277 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Sep 2025 11:47:10 +0000 Subject: [PATCH 1/3] Initial plan From 12c12a1ad5fae04f99c6493eaf962f3b97c2ffa0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Sep 2025 11:59:46 +0000 Subject: [PATCH 2/3] Add ComponentFrameworkEndpointMetadata to identify Blazor infrastructure endpoints Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- .../ComponentFrameworkEndpointMetadata.cs | 13 +++++ .../src/Builder/OpaqueRedirection.cs | 1 + .../Endpoints/src/PublicAPI.Unshipped.txt | 2 + .../ComponentFrameworkEndpointMetadataTest.cs | 41 ++++++++++++++++ .../test/Builder/OpaqueRedirectionTest.cs | 48 +++++++++++++++++++ ...ComponentEndpointRouteBuilderExtensions.cs | 7 ++- ...onentEndpointRouteBuilderExtensionsTest.cs | 32 +++++++++++++ .../Server/src/WebAssemblyEndpointProvider.cs | 20 +++++++- 8 files changed, 161 insertions(+), 3 deletions(-) create mode 100644 src/Components/Endpoints/src/Builder/ComponentFrameworkEndpointMetadata.cs create mode 100644 src/Components/Endpoints/test/Builder/ComponentFrameworkEndpointMetadataTest.cs create mode 100644 src/Components/Endpoints/test/Builder/OpaqueRedirectionTest.cs diff --git a/src/Components/Endpoints/src/Builder/ComponentFrameworkEndpointMetadata.cs b/src/Components/Endpoints/src/Builder/ComponentFrameworkEndpointMetadata.cs new file mode 100644 index 000000000000..2bd084c0e564 --- /dev/null +++ b/src/Components/Endpoints/src/Builder/ComponentFrameworkEndpointMetadata.cs @@ -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; + +/// +/// 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. +/// +public sealed class ComponentFrameworkEndpointMetadata +{ +} \ No newline at end of file diff --git a/src/Components/Endpoints/src/Builder/OpaqueRedirection.cs b/src/Components/Endpoints/src/Builder/OpaqueRedirection.cs index 7e747aca96ac..97e39f605f28 100644 --- a/src/Components/Endpoints/src/Builder/OpaqueRedirection.cs +++ b/src/Components/Endpoints/src/Builder/OpaqueRedirection.cs @@ -59,6 +59,7 @@ public static EndpointBuilder GetBlazorOpaqueRedirectionEndpoint() }; routeEndpointBuidler.Metadata.Add(new HttpMethodMetadata([HttpMethods.Get])); + routeEndpointBuidler.Metadata.Add(new ComponentFrameworkEndpointMetadata()); return routeEndpointBuidler; } diff --git a/src/Components/Endpoints/src/PublicAPI.Unshipped.txt b/src/Components/Endpoints/src/PublicAPI.Unshipped.txt index 424ca155339b..931da44ec99c 100644 --- a/src/Components/Endpoints/src/PublicAPI.Unshipped.txt +++ b/src/Components/Endpoints/src/PublicAPI.Unshipped.txt @@ -1,4 +1,6 @@ #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 diff --git a/src/Components/Endpoints/test/Builder/ComponentFrameworkEndpointMetadataTest.cs b/src/Components/Endpoints/test/Builder/ComponentFrameworkEndpointMetadataTest.cs new file mode 100644 index 000000000000..50542b31cfe4 --- /dev/null +++ b/src/Components/Endpoints/test/Builder/ComponentFrameworkEndpointMetadataTest.cs @@ -0,0 +1,41 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Components.Tests.Builder; + +public class ComponentFrameworkEndpointMetadataTest +{ + [Fact] + public void ComponentFrameworkEndpointMetadata_CanBeCreated() + { + // Arrange & Act + var metadata = new ComponentFrameworkEndpointMetadata(); + + // Assert + Assert.NotNull(metadata); + } + + [Fact] + public void ComponentFrameworkEndpointMetadata_IsSealed() + { + // Arrange & Act + var type = typeof(ComponentFrameworkEndpointMetadata); + + // Assert + Assert.True(type.IsSealed); + } + + [Fact] + public void ComponentFrameworkEndpointMetadata_HasNoPublicProperties() + { + // Arrange & Act + var type = typeof(ComponentFrameworkEndpointMetadata); + var properties = type.GetProperties(); + + // Assert + Assert.Empty(properties); + } +} \ No newline at end of file diff --git a/src/Components/Endpoints/test/Builder/OpaqueRedirectionTest.cs b/src/Components/Endpoints/test/Builder/OpaqueRedirectionTest.cs new file mode 100644 index 000000000000..538fcf7f42b5 --- /dev/null +++ b/src/Components/Endpoints/test/Builder/OpaqueRedirectionTest.cs @@ -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(); + 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(); + Assert.NotNull(httpMethodMetadata); + Assert.Contains(HttpMethods.Get, httpMethodMetadata.HttpMethods); + } +} \ No newline at end of file diff --git a/src/Components/Server/src/Builder/ComponentEndpointRouteBuilderExtensions.cs b/src/Components/Server/src/Builder/ComponentEndpointRouteBuilderExtensions.cs index 26cca2322d0f..c5950981ea29 100644 --- a/src/Components/Server/src/Builder/ComponentEndpointRouteBuilderExtensions.cs +++ b/src/Components/Server/src/Builder/ComponentEndpointRouteBuilderExtensions.cs @@ -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; @@ -78,12 +79,14 @@ public static ComponentEndpointConventionBuilder MapBlazorHub( var disconnectEndpoint = endpoints.Map( (path.EndsWith('/') ? path : path + "/") + "disconnect/", endpoints.CreateApplicationBuilder().UseMiddleware().Build()) - .WithDisplayName("Blazor disconnect"); + .WithDisplayName("Blazor disconnect") + .WithMetadata(new ComponentFrameworkEndpointMetadata()); var jsInitializersEndpoint = endpoints.Map( (path.EndsWith('/') ? path : path + "/") + "initializers/", endpoints.CreateApplicationBuilder().UseMiddleware().Build()) - .WithDisplayName("Blazor initializers"); + .WithDisplayName("Blazor initializers") + .WithMetadata(new ComponentFrameworkEndpointMetadata()); return new ComponentEndpointConventionBuilder(hubEndpoint, disconnectEndpoint, jsInitializersEndpoint); } diff --git a/src/Components/Server/test/ComponentEndpointRouteBuilderExtensionsTest.cs b/src/Components/Server/test/ComponentEndpointRouteBuilderExtensionsTest.cs index 5d28dae0be1a..30c75962172a 100644 --- a/src/Components/Server/test/ComponentEndpointRouteBuilderExtensionsTest.cs +++ b/src/Components/Server/test/ComponentEndpointRouteBuilderExtensionsTest.cs @@ -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; @@ -122,6 +123,37 @@ 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(); + + // Act + var app = applicationBuilder + .UseRouting() + .UseEndpoints(endpoints => + { + endpoints + .MapBlazorHub() + .Finally(builder => + { + if (builder.Metadata.GetMetadata() is not null) + { + 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(); diff --git a/src/Components/WebAssembly/Server/src/WebAssemblyEndpointProvider.cs b/src/Components/WebAssembly/Server/src/WebAssemblyEndpointProvider.cs index 7176462b7998..2f5186014a73 100644 --- a/src/Components/WebAssembly/Server/src/WebAssemblyEndpointProvider.cs +++ b/src/Components/WebAssembly/Server/src/WebAssemblyEndpointProvider.cs @@ -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; @@ -41,7 +42,8 @@ public override IEnumerable GetEndpointBuilders(IComponent context.SetEndpoint(null); return app(context); - }); + }) + .WithMetadata(new ComponentFrameworkEndpointMetadata()); return endpointRouteBuilder.GetEndpoints(); } @@ -75,6 +77,22 @@ internal IEnumerable 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; } } From 1c1c0b9e9940b36b9271a4faddda1d67197e3e29 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Sep 2025 15:36:28 +0000 Subject: [PATCH 3/3] Address feedback: Remove test, fix compilation error, add ConfigureFrameworkEndpoints method Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- ...entsEndpointConventionBuilderExtensions.cs | 32 +++++++++++ .../Endpoints/src/PublicAPI.Unshipped.txt | 1 + .../ComponentFrameworkEndpointMetadataTest.cs | 41 -------------- ...EndpointConventionBuilderExtensionsTest.cs | 53 +++++++++++++++++++ ...onentEndpointRouteBuilderExtensionsTest.cs | 3 +- 5 files changed, 88 insertions(+), 42 deletions(-) delete mode 100644 src/Components/Endpoints/test/Builder/ComponentFrameworkEndpointMetadataTest.cs diff --git a/src/Components/Endpoints/src/Builder/RazorComponentsEndpointConventionBuilderExtensions.cs b/src/Components/Endpoints/src/Builder/RazorComponentsEndpointConventionBuilderExtensions.cs index fd0a6aec0c4d..73941a78a32d 100644 --- a/src/Components/Endpoints/src/Builder/RazorComponentsEndpointConventionBuilderExtensions.cs +++ b/src/Components/Endpoints/src/Builder/RazorComponentsEndpointConventionBuilderExtensions.cs @@ -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; @@ -63,4 +64,35 @@ public static RazorComponentsEndpointConventionBuilder WithStaticAssets( return builder; } + + /// + /// Configures framework endpoints for the Blazor application. Framework endpoints are internal + /// infrastructure endpoints such as opaque redirection, disconnect, and initializers. + /// + /// The . + /// A callback to configure framework endpoints. + /// The . + /// + /// 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 in their metadata collection. + /// + public static RazorComponentsEndpointConventionBuilder ConfigureFrameworkEndpoints( + this RazorComponentsEndpointConventionBuilder builder, + Action configureEndpoints) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configureEndpoints); + + builder.Add(endpointBuilder => + { + // Only apply configuration to endpoints that have ComponentFrameworkEndpointMetadata + if (endpointBuilder.Metadata.OfType().Any()) + { + configureEndpoints(endpointBuilder); + } + }); + + return builder; + } } diff --git a/src/Components/Endpoints/src/PublicAPI.Unshipped.txt b/src/Components/Endpoints/src/PublicAPI.Unshipped.txt index 931da44ec99c..18a9e656a63f 100644 --- a/src/Components/Endpoints/src/PublicAPI.Unshipped.txt +++ b/src/Components/Endpoints/src/PublicAPI.Unshipped.txt @@ -4,4 +4,5 @@ Microsoft.AspNetCore.Components.Endpoints.ComponentFrameworkEndpointMetadata.Com 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! configureEndpoints) -> Microsoft.AspNetCore.Builder.RazorComponentsEndpointConventionBuilder! static Microsoft.Extensions.DependencyInjection.RazorComponentsRazorComponentBuilderExtensions.RegisterPersistentService(this Microsoft.Extensions.DependencyInjection.IRazorComponentsBuilder! builder, Microsoft.AspNetCore.Components.IComponentRenderMode! renderMode) -> Microsoft.Extensions.DependencyInjection.IRazorComponentsBuilder! diff --git a/src/Components/Endpoints/test/Builder/ComponentFrameworkEndpointMetadataTest.cs b/src/Components/Endpoints/test/Builder/ComponentFrameworkEndpointMetadataTest.cs deleted file mode 100644 index 50542b31cfe4..000000000000 --- a/src/Components/Endpoints/test/Builder/ComponentFrameworkEndpointMetadataTest.cs +++ /dev/null @@ -1,41 +0,0 @@ -// 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 Xunit; - -namespace Microsoft.AspNetCore.Components.Tests.Builder; - -public class ComponentFrameworkEndpointMetadataTest -{ - [Fact] - public void ComponentFrameworkEndpointMetadata_CanBeCreated() - { - // Arrange & Act - var metadata = new ComponentFrameworkEndpointMetadata(); - - // Assert - Assert.NotNull(metadata); - } - - [Fact] - public void ComponentFrameworkEndpointMetadata_IsSealed() - { - // Arrange & Act - var type = typeof(ComponentFrameworkEndpointMetadata); - - // Assert - Assert.True(type.IsSealed); - } - - [Fact] - public void ComponentFrameworkEndpointMetadata_HasNoPublicProperties() - { - // Arrange & Act - var type = typeof(ComponentFrameworkEndpointMetadata); - var properties = type.GetProperties(); - - // Assert - Assert.Empty(properties); - } -} \ No newline at end of file diff --git a/src/Components/Endpoints/test/Builder/RazorComponentsEndpointConventionBuilderExtensionsTest.cs b/src/Components/Endpoints/test/Builder/RazorComponentsEndpointConventionBuilderExtensionsTest.cs index 70fa780fcbaf..6c7afea60c7d 100644 --- a/src/Components/Endpoints/test/Builder/RazorComponentsEndpointConventionBuilderExtensionsTest.cs +++ b/src/Components/Endpoints/test/Builder/RazorComponentsEndpointConventionBuilderExtensionsTest.cs @@ -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(); + + // 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(); + + 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(); diff --git a/src/Components/Server/test/ComponentEndpointRouteBuilderExtensionsTest.cs b/src/Components/Server/test/ComponentEndpointRouteBuilderExtensionsTest.cs index 30c75962172a..b965d794b532 100644 --- a/src/Components/Server/test/ComponentEndpointRouteBuilderExtensionsTest.cs +++ b/src/Components/Server/test/ComponentEndpointRouteBuilderExtensionsTest.cs @@ -139,7 +139,8 @@ public void MapBlazorHub_AddsComponentFrameworkEndpointMetadata() .MapBlazorHub() .Finally(builder => { - if (builder.Metadata.GetMetadata() is not null) + // Check if metadata collection contains ComponentFrameworkEndpointMetadata + if (builder.Metadata.OfType().Any()) { frameworkEndpoints.Add(builder.DisplayName); }