diff --git a/src/Components/Endpoints/src/PublicAPI.Unshipped.txt b/src/Components/Endpoints/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..ac1780ba883e 100644 --- a/src/Components/Endpoints/src/PublicAPI.Unshipped.txt +++ b/src/Components/Endpoints/src/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +Microsoft.AspNetCore.Components.Endpoints.BasePath +Microsoft.AspNetCore.Components.Endpoints.BasePath.BasePath() -> void diff --git a/src/Components/Endpoints/src/Routing/BasePath.cs b/src/Components/Endpoints/src/Routing/BasePath.cs new file mode 100644 index 000000000000..1e7435d2613e --- /dev/null +++ b/src/Components/Endpoints/src/Routing/BasePath.cs @@ -0,0 +1,46 @@ +// 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.Rendering; + +namespace Microsoft.AspNetCore.Components.Endpoints; + +/// +/// Renders a <base> element whose href value matches the current request path base. +/// +public sealed class BasePath : IComponent +{ + private RenderHandle _renderHandle; + + [Inject] + private NavigationManager NavigationManager { get; set; } = default!; + + void IComponent.Attach(RenderHandle renderHandle) + { + _renderHandle = renderHandle; + } + + Task IComponent.SetParametersAsync(ParameterView parameters) + { + _renderHandle.Render(Render); + return Task.CompletedTask; + } + + private void Render(RenderTreeBuilder builder) + { + builder.OpenElement(0, "base"); + builder.AddAttribute(1, "href", ComputeHref()); + builder.CloseElement(); + } + + private string ComputeHref() + { + var baseUri = NavigationManager.BaseUri; + if (Uri.TryCreate(baseUri, UriKind.Absolute, out var absoluteUri)) + { + return absoluteUri.AbsolutePath; + } + + return "/"; + } +} diff --git a/src/Components/Endpoints/test/Microsoft.AspNetCore.Components.Endpoints.Tests.csproj b/src/Components/Endpoints/test/Microsoft.AspNetCore.Components.Endpoints.Tests.csproj index 3fe362d5d236..3d7bced6b251 100644 --- a/src/Components/Endpoints/test/Microsoft.AspNetCore.Components.Endpoints.Tests.csproj +++ b/src/Components/Endpoints/test/Microsoft.AspNetCore.Components.Endpoints.Tests.csproj @@ -6,8 +6,8 @@ - + diff --git a/src/Components/Endpoints/test/Routing/BasePathTest.cs b/src/Components/Endpoints/test/Routing/BasePathTest.cs new file mode 100644 index 000000000000..ec2c4fa02fc6 --- /dev/null +++ b/src/Components/Endpoints/test/Routing/BasePathTest.cs @@ -0,0 +1,93 @@ +// 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.RenderTree; +using Microsoft.AspNetCore.Components.Test.Helpers; + +#nullable enable + +namespace Microsoft.AspNetCore.Components.Endpoints; + +public class BasePathTest +{ + [Fact] + public void PreservesCasingFromNavigationManagerBaseUri() + { + _ = CreateServices(out var renderer, "https://example.com/Dashboard/"); + var componentId = RenderBasePath(renderer); + + Assert.Equal("/Dashboard/", GetHref(renderer, componentId)); + } + + [Theory] + [InlineData("https://example.com/a/b/", "/a/b/")] + [InlineData("https://example.com/a/b", "/a/")] + public void RendersBaseUriPathExactly(string baseUri, string expected) + { + _ = CreateServices(out var renderer, baseUri); + + var componentId = RenderBasePath(renderer); + + Assert.Equal(expected, GetHref(renderer, componentId)); + } + + private static TestServiceProvider CreateServices(out TestRenderer renderer, string baseUri = "https://example.com/app/") + { + var services = new TestServiceProvider(); + var uri = baseUri.EndsWith('/') ? baseUri + "dashboard" : baseUri + "/dashboard"; + var navigationManager = new TestNavigationManager(baseUri, uri); + services.AddService(navigationManager); + services.AddService(services); + + renderer = new TestRenderer(services); + return services; + } + + private static int RenderBasePath(TestRenderer renderer) + { + var component = (BasePath)renderer.InstantiateComponent(); + var componentId = renderer.AssignRootComponentId(component); + renderer.RenderRootComponent(componentId); + return componentId; + } + + private static string? GetHref(TestRenderer renderer, int componentId) + { + var frames = renderer.GetCurrentRenderTreeFrames(componentId); + for (var i = 0; i < frames.Count; i++) + { + ref readonly var frame = ref frames.Array[i]; + if (frame.FrameType == RenderTreeFrameType.Element && frame.ElementName == "base") + { + for (var j = i + 1; j < frames.Count; j++) + { + ref readonly var attribute = ref frames.Array[j]; + if (attribute.FrameType == RenderTreeFrameType.Attribute && attribute.AttributeName == "href") + { + return attribute.AttributeValue?.ToString(); + } + + if (attribute.FrameType != RenderTreeFrameType.Attribute) + { + break; + } + } + } + } + + return null; + } + + private sealed class TestNavigationManager : NavigationManager + { + public TestNavigationManager(string baseUri, string uri) + { + Initialize(baseUri, uri); + } + + protected override void NavigateToCore(string uri, bool forceLoad) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Components/Samples/BlazorUnitedApp/App.razor b/src/Components/Samples/BlazorUnitedApp/App.razor index e04f7fc9e8e4..17c3f20485ec 100644 --- a/src/Components/Samples/BlazorUnitedApp/App.razor +++ b/src/Components/Samples/BlazorUnitedApp/App.razor @@ -3,7 +3,7 @@ - + diff --git a/src/Components/Samples/BlazorUnitedApp/_Imports.razor b/src/Components/Samples/BlazorUnitedApp/_Imports.razor index 1be7a7e9a5bf..8cacc98a9e57 100644 --- a/src/Components/Samples/BlazorUnitedApp/_Imports.razor +++ b/src/Components/Samples/BlazorUnitedApp/_Imports.razor @@ -3,6 +3,7 @@ @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Endpoints @using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.JSInterop @using BlazorUnitedApp diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor index 94a9a5a597e5..fba24956b11b 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor @@ -4,6 +4,7 @@ @using TestContentPackage.NotFound @using Components.TestServer.RazorComponents @using Microsoft.AspNetCore.Components +@using Microsoft.AspNetCore.Components.Endpoints @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web @using System.Threading.Tasks @@ -99,7 +100,7 @@ - + diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/NamedFormContextNoFormContextApp.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/NamedFormContextNoFormContextApp.razor index fc8a291d9821..cc1a416596bf 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/NamedFormContextNoFormContextApp.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/NamedFormContextNoFormContextApp.razor @@ -1,10 +1,11 @@ @using Components.TestServer.RazorComponents.Pages.Forms +@using Microsoft.AspNetCore.Components.Endpoints - + diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/RemoteAuthenticationApp.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/RemoteAuthenticationApp.razor index c8f89e587f8f..057376591785 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/RemoteAuthenticationApp.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/RemoteAuthenticationApp.razor @@ -1,9 +1,11 @@ - +@using Microsoft.AspNetCore.Components.Endpoints + + - + diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Root.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Root.razor index 9238fb858c7b..82bed6851f37 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Root.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Root.razor @@ -1,11 +1,12 @@ @using Components.TestServer.RazorComponents.Pages.Forms +@using Microsoft.AspNetCore.Components.Endpoints @using Microsoft.AspNetCore.Components.Web - + diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/App.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/App.razor index 347861a467ff..c7e72eb2f460 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/App.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/App.razor @@ -4,7 +4,7 @@ - + @*#if (SampleContent) diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/_Imports.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/_Imports.razor index 192e7b16edfe..ec70fa0106f3 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/_Imports.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/_Imports.razor @@ -1,5 +1,6 @@ @using System.Net.Http @using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Endpoints @*#if (IndividualLocalAuth) @using Microsoft.AspNetCore.Components.Authorization ##endif*@