-
Notifications
You must be signed in to change notification settings - Fork 10.5k
API proposal for NavigateTo and NavLink with relative path
#64670
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds a PathRelative navigation option to Blazor's NavigationManager, enabling developers to navigate to URIs relative to the current page's directory path rather than the application's base URI. This addresses a common pain point where navigating to sibling pages required manual path resolution.
Key Changes:
- Adds
NavigationOptions.PathRelativeproperty for path-relative navigation - Implements
ResolveRelativeToCurrentPathmethod using zero-allocation span-based operations - Adds TypeScript interface definition for JavaScript interop
- Includes 7 unit tests covering basic scenarios and edge cases
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Components/Components/src/NavigationOptions.cs | Adds PathRelative boolean property to NavigationOptions struct with XML documentation |
| src/Components/Components/src/NavigationManager.cs | Implements path-relative resolution logic in NavigateTo and new internal ResolveRelativeToCurrentPath method |
| src/Components/Components/src/PublicAPI.Unshipped.txt | Registers the new public API property and its init accessor |
| src/Components/Components/test/NavigationManagerTest.cs | Adds unit tests for path-relative navigation scenarios and helper class for tracking navigations |
| src/Components/Web.JS/src/Services/NavigationManager.ts | Adds optional pathRelative field to TypeScript NavigationOptions interface |
| var currentUri = _uri!.AsSpan(); | ||
|
|
||
| // Find the last slash in the path portion (before any query or fragment) | ||
| var queryOrFragmentIndex = currentUri.IndexOfAny('?', '#'); | ||
| var pathOnlyLength = queryOrFragmentIndex >= 0 ? queryOrFragmentIndex : currentUri.Length; | ||
| var lastSlashIndex = currentUri[..pathOnlyLength].LastIndexOf('/'); | ||
|
|
||
| if (lastSlashIndex < 0) | ||
| { | ||
| // No slash found - this shouldn't happen for valid absolute URIs | ||
| // In this edge case, just append to the current URI | ||
| return string.Concat(_uri, relativeUri); | ||
| } | ||
|
|
||
| // Keep everything up to and including the last slash, then append the relative URI | ||
| var basePathLength = lastSlashIndex + 1; | ||
| return string.Concat(currentUri[..basePathLength], relativeUri.AsSpan()); |
Copilot
AI
Dec 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The implementation of PathRelative navigation doesn't handle several important relative path patterns that developers might reasonably expect to work:
- Absolute paths (e.g.,
"/about.html") - These start with/and should navigate to the root, but currently would be concatenated to the current directory path - Parent directory navigation (e.g.,
"../sibling.html") - The../pattern to navigate up directories is not handled - Current directory references (e.g.,
"./page.html") - The./prefix is not explicitly handled
Consider either:
- Documenting these limitations explicitly in the XML comments for
PathRelativeproperty - Or implementing proper relative URI resolution using standard URI path resolution rules (which would handle
../,/, and./correctly)
Standard URI path resolution is typically done using new Uri(baseUri, relativeUri) which automatically handles these cases. If you want to avoid allocating Uri objects for performance, you could implement the standard path resolution algorithm manually using spans.
| var currentUri = _uri!.AsSpan(); | |
| // Find the last slash in the path portion (before any query or fragment) | |
| var queryOrFragmentIndex = currentUri.IndexOfAny('?', '#'); | |
| var pathOnlyLength = queryOrFragmentIndex >= 0 ? queryOrFragmentIndex : currentUri.Length; | |
| var lastSlashIndex = currentUri[..pathOnlyLength].LastIndexOf('/'); | |
| if (lastSlashIndex < 0) | |
| { | |
| // No slash found - this shouldn't happen for valid absolute URIs | |
| // In this edge case, just append to the current URI | |
| return string.Concat(_uri, relativeUri); | |
| } | |
| // Keep everything up to and including the last slash, then append the relative URI | |
| var basePathLength = lastSlashIndex + 1; | |
| return string.Concat(currentUri[..basePathLength], relativeUri.AsSpan()); | |
| // Use standard URI resolution to handle absolute paths, ../, ./, etc. | |
| // _uri is the current absolute URI (including scheme, host, path, query, fragment) | |
| var baseUri = new Uri(_uri!, UriKind.Absolute); | |
| var resolvedUri = new Uri(baseUri, relativeUri); | |
| return resolvedUri.ToString(); |
| [Fact] | ||
| public void NavigateTo_WithPathRelative_ResolvesRelativeToCurrentPath() | ||
| { | ||
| var baseUri = "scheme://host/"; | ||
| var currentUri = "scheme://host/folder1/folder2/page.html"; | ||
| var testNavManager = new TestNavigationManagerWithNavigationTracking(baseUri, currentUri); | ||
|
|
||
| testNavManager.NavigateTo("sibling.html", new NavigationOptions { PathRelative = true }); | ||
|
|
||
| Assert.Single(testNavManager.Navigations); | ||
| Assert.Equal("scheme://host/folder1/folder2/sibling.html", testNavManager.Navigations[0].uri); | ||
| Assert.True(testNavManager.Navigations[0].options.PathRelative); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void NavigateTo_WithPathRelative_HandlesQueryAndFragmentInCurrentUri() | ||
| { | ||
| var baseUri = "scheme://host/"; | ||
| var currentUri = "scheme://host/folder1/page.html?query=value#hash"; | ||
| var testNavManager = new TestNavigationManagerWithNavigationTracking(baseUri, currentUri); | ||
|
|
||
| testNavManager.NavigateTo("other.html", new NavigationOptions { PathRelative = true }); | ||
|
|
||
| Assert.Single(testNavManager.Navigations); | ||
| Assert.Equal("scheme://host/folder1/other.html", testNavManager.Navigations[0].uri); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void NavigateTo_WithPathRelativeFalse_DoesNotResolve() | ||
| { | ||
| var baseUri = "scheme://host/base/"; | ||
| var currentUri = "scheme://host/base/folder1/page.html"; | ||
| var testNavManager = new TestNavigationManagerWithNavigationTracking(baseUri, currentUri); | ||
|
|
||
| testNavManager.NavigateTo("relative.html", new NavigationOptions { PathRelative = false }); | ||
|
|
||
| Assert.Single(testNavManager.Navigations); | ||
| // When PathRelative is false, the URI is passed directly to NavigateToCore without resolution | ||
| Assert.Equal("relative.html", testNavManager.Navigations[0].uri); | ||
| Assert.False(testNavManager.Navigations[0].options.PathRelative); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void NavigateTo_WithPathRelative_AtRootLevel() | ||
| { | ||
| var baseUri = "scheme://host/"; | ||
| var currentUri = "scheme://host/page.html"; | ||
| var testNavManager = new TestNavigationManagerWithNavigationTracking(baseUri, currentUri); | ||
|
|
||
| testNavManager.NavigateTo("other.html", new NavigationOptions { PathRelative = true }); | ||
|
|
||
| Assert.Single(testNavManager.Navigations); | ||
| Assert.Equal("scheme://host/other.html", testNavManager.Navigations[0].uri); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void NavigateTo_WithPathRelative_NestedPaths() | ||
| { | ||
| var baseUri = "scheme://host/"; | ||
| var currentUri = "scheme://host/a/b/c/d/page.html"; | ||
| var testNavManager = new TestNavigationManagerWithNavigationTracking(baseUri, currentUri); | ||
|
|
||
| testNavManager.NavigateTo("sibling.html", new NavigationOptions { PathRelative = true }); | ||
|
|
||
| Assert.Single(testNavManager.Navigations); | ||
| Assert.Equal("scheme://host/a/b/c/d/sibling.html", testNavManager.Navigations[0].uri); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void NavigateTo_WithPathRelative_WithQueryStringPreservesPath() | ||
| { | ||
| var baseUri = "scheme://host/"; | ||
| var currentUri = "scheme://host/folder/page.html?param=value"; | ||
| var testNavManager = new TestNavigationManagerWithNavigationTracking(baseUri, currentUri); | ||
|
|
||
| testNavManager.NavigateTo("other.html?new=param", new NavigationOptions { PathRelative = true }); | ||
|
|
||
| Assert.Single(testNavManager.Navigations); | ||
| Assert.Equal("scheme://host/folder/other.html?new=param", testNavManager.Navigations[0].uri); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ResolveRelativeToCurrentPath_NoSlashFound_EdgeCase() | ||
| { | ||
| // This tests the defensive edge case where no slash is found in the URI | ||
| // We use reflection to set _uri to an invalid value (bypassing validation) | ||
| var baseUri = "scheme://host/"; | ||
| var testNavManager = new TestNavigationManager(baseUri, "scheme://host/page.html"); | ||
|
|
||
| // Use reflection to set _uri to an invalid value that has no slash | ||
| var uriField = typeof(NavigationManager).GetField("_uri", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); | ||
| uriField.SetValue(testNavManager, "invaliduri"); | ||
|
|
||
| // Call the internal method directly (accessible because of InternalsVisibleTo) | ||
| var result = testNavManager.ResolveRelativeToCurrentPath("page.html"); | ||
|
|
||
| // When no slash is found, it concatenates to the current URI | ||
| Assert.Equal("invaliduripage.html", result); | ||
| } | ||
|
|
Copilot
AI
Dec 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test coverage is missing for important edge cases in path-relative navigation:
- Absolute paths: What happens when navigating with
PathRelative = trueto"/about.html"? Currently this would incorrectly concatenate to the current path. - Parent directory navigation: What happens with
"../parent.html"or"../../grandparent.html"? - Current directory references: What happens with
"./sibling.html"? - Current URI ending in directory: What happens when current URI is
"scheme://host/folder/"(ends with slash) instead of a file? - Fragment-only navigation: What happens with
"#section"? - Query-only navigation: What happens with
"?param=value"?
These test cases are critical to validate the behavior and ensure developers understand what patterns are supported by this feature.
| [Fact] | ||
| public void ResolveRelativeToCurrentPath_NoSlashFound_EdgeCase() | ||
| { | ||
| // This tests the defensive edge case where no slash is found in the URI | ||
| // We use reflection to set _uri to an invalid value (bypassing validation) | ||
| var baseUri = "scheme://host/"; | ||
| var testNavManager = new TestNavigationManager(baseUri, "scheme://host/page.html"); | ||
|
|
||
| // Use reflection to set _uri to an invalid value that has no slash | ||
| var uriField = typeof(NavigationManager).GetField("_uri", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); | ||
| uriField.SetValue(testNavManager, "invaliduri"); | ||
|
|
||
| // Call the internal method directly (accessible because of InternalsVisibleTo) | ||
| var result = testNavManager.ResolveRelativeToCurrentPath("page.html"); | ||
|
|
||
| // When no slash is found, it concatenates to the current URI | ||
| Assert.Equal("invaliduripage.html", result); | ||
| } | ||
|
|
Copilot
AI
Dec 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using reflection to directly test the internal ResolveRelativeToCurrentPath method and manipulate internal state (_uri field) creates a brittle test that:
- Depends on internal implementation details that could change
- Tests an edge case (
"invaliduri"with no slashes) that cannot occur in real usage sinceInitialize()validates that URIs are absolute - Would break if the internal field name or accessibility changes
Consider instead testing this edge case behavior through the public NavigateTo API, or remove this test entirely since the "no slash found" case is defensive programming for an impossible state in production code.
| [Fact] | |
| public void ResolveRelativeToCurrentPath_NoSlashFound_EdgeCase() | |
| { | |
| // This tests the defensive edge case where no slash is found in the URI | |
| // We use reflection to set _uri to an invalid value (bypassing validation) | |
| var baseUri = "scheme://host/"; | |
| var testNavManager = new TestNavigationManager(baseUri, "scheme://host/page.html"); | |
| // Use reflection to set _uri to an invalid value that has no slash | |
| var uriField = typeof(NavigationManager).GetField("_uri", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); | |
| uriField.SetValue(testNavManager, "invaliduri"); | |
| // Call the internal method directly (accessible because of InternalsVisibleTo) | |
| var result = testNavManager.ResolveRelativeToCurrentPath("page.html"); | |
| // When no slash is found, it concatenates to the current URI | |
| Assert.Equal("invaliduripage.html", result); | |
| } |
|
I don't think we should take this change.
This is not a thing in the web. The original issue refers to users hosting inside MVC and not adding a This change doesn't address that and introduces a way of composing a path that doesn't make sense. There is a way of creating relative URLs which is using Absolute URL means a complete URI with scheme and authority (for example
When a browser sees a non‑absolute string it runs the URL resolution algorithm: if a Relationship of
|
|
Let's discuss it a bit more.
I was inspired by this API: https://angular.dev/guide/routing/navigate-to-routes#routernavigate. Blazor would not be a pioneer in allowing
I don't believe so, the original issue is about blazor treating nested paths not as dirs but like a route, so not recognizing that in What do you think? |
NavigateTo with relative pathNavigateTo and NavLink with relative path
Summary
This change implements the
PathRelativeparameter for Blazor'sNavigationManager.NavigateTo()andNavLinkcomponent, allowing developers to navigate to URIs relative to the current page path rather than the application's base URI.Background
Previously, Blazor only supported navigation relative to the application's base URI. For example, calling
NavigateTo("sibling.html")from/folder1/page.htmlwould navigate to/sibling.html(at the app root), not/folder1/sibling.html. The same limitation affectedNavLinkcomponents in SSR scenarios.Fixes #23615
Changes
Public API
Implementation Details
NavigationManager: When
PathRelativeistrue,NavigateTo()callsResolveRelativeToCurrentPath()to compute the absolute URI using zero-allocation span-based operations before passing it toNavigateToCore().NavLink: When
PathRelativeistrue,OnParametersSet()resolves the relative href server-side during rendering, ensuring the anchor tag contains the correct absolute path for SSR scenarios.Performance: The implementation avoids creating
Uriobjects, using onlyReadOnlySpan<char>operations andstring.Concat()for efficiency.Tests
Added 7 NavigationManager tests and 6 NavLink tests covering path-relative navigation, query/fragment handling, nested paths, and edge cases.
Usage
NavigationManager:
NavLink: