From 1e6cf9be7db348f71044bfcd46d16f3fc92f5e5e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 2 Dec 2025 22:31:17 +0000
Subject: [PATCH 1/3] Initial plan
From 9dc30b9f1fec5c8d3344311fd988e96824148a9f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 2 Dec 2025 22:40:14 +0000
Subject: [PATCH 2/3] Add GetUriWithHash() extension method to
NavigationManagerExtensions
Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
---
.../src/NavigationManagerExtensions.cs | 47 +++++++++++++++++++
.../Components/src/PublicAPI.Unshipped.txt | 1 +
.../Components/test/NavigationManagerTest.cs | 42 +++++++++++++++++
3 files changed, 90 insertions(+)
diff --git a/src/Components/Components/src/NavigationManagerExtensions.cs b/src/Components/Components/src/NavigationManagerExtensions.cs
index 22b555b0b425..43ca2648f75a 100644
--- a/src/Components/Components/src/NavigationManagerExtensions.cs
+++ b/src/Components/Components/src/NavigationManagerExtensions.cs
@@ -762,4 +762,51 @@ private static bool TryRebuildExistingQueryFromUri(
return true;
}
+
+ ///
+ /// Returns a URI constructed from with a hash
+ /// added, updated, or removed.
+ ///
+ /// The .
+ /// The hash string. If empty or null, the hash will be removed from the URI.
+ /// The URI with the specified hash.
+ ///
+ ///
+ /// If does not start with #, then # will be prepended.
+ ///
+ ///
+ /// This method is useful when the document's baseURI differs from its location,
+ /// such as when a <base> element is used, since relative hash URLs are resolved
+ /// relative to the baseURI.
+ ///
+ ///
+ ///
+ /// @inject NavigationManager Nav
+ /// <a href="@Nav.GetUriWithHash("section1")">Go to section 1</a>
+ ///
+ ///
+ ///
+ public static string GetUriWithHash(this NavigationManager navigationManager, string? hash)
+ {
+ ArgumentNullException.ThrowIfNull(navigationManager);
+
+ var uri = navigationManager.Uri;
+ var hashStartIndex = uri.IndexOf('#');
+
+ // Get URI without the existing hash
+ var uriWithoutHash = hashStartIndex < 0 ? uri : uri.Substring(0, hashStartIndex);
+
+ if (string.IsNullOrEmpty(hash))
+ {
+ return uriWithoutHash;
+ }
+
+ // Ensure hash starts with '#'
+ if (hash[0] != '#')
+ {
+ return string.Concat(uriWithoutHash, "#", hash);
+ }
+
+ return string.Concat(uriWithoutHash, hash);
+ }
}
diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt
index 7dc5c58110bf..4c6d4b628eb8 100644
--- a/src/Components/Components/src/PublicAPI.Unshipped.txt
+++ b/src/Components/Components/src/PublicAPI.Unshipped.txt
@@ -1 +1,2 @@
#nullable enable
+static Microsoft.AspNetCore.Components.NavigationManagerExtensions.GetUriWithHash(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string? hash) -> string!
diff --git a/src/Components/Components/test/NavigationManagerTest.cs b/src/Components/Components/test/NavigationManagerTest.cs
index 3f6e680d0c8b..d05c5b85249f 100644
--- a/src/Components/Components/test/NavigationManagerTest.cs
+++ b/src/Components/Components/test/NavigationManagerTest.cs
@@ -213,6 +213,48 @@ public void GetUriWithQueryParameters_ThrowsWhenAnyParameterNameIsEmpty(string b
Assert.StartsWith("Cannot have empty query parameter names.", exception.Message);
}
+ [Theory]
+ [InlineData("scheme://host/", "section1", "scheme://host/#section1")]
+ [InlineData("scheme://host/", "#section1", "scheme://host/#section1")]
+ [InlineData("scheme://host/path", "section1", "scheme://host/path#section1")]
+ [InlineData("scheme://host/path/", "section1", "scheme://host/path/#section1")]
+ [InlineData("scheme://host/path?query=value", "section1", "scheme://host/path?query=value#section1")]
+ [InlineData("scheme://host/path?query=value#oldHash", "section1", "scheme://host/path?query=value#section1")]
+ [InlineData("scheme://host/path#oldHash", "newHash", "scheme://host/path#newHash")]
+ [InlineData("scheme://host/path#old", "#new", "scheme://host/path#new")]
+ public void GetUriWithHash_AddsOrReplacesHash(string baseUri, string hash, string expectedUri)
+ {
+ var navigationManager = new TestNavigationManager(baseUri);
+ var actualUri = navigationManager.GetUriWithHash(hash);
+
+ Assert.Equal(expectedUri, actualUri);
+ }
+
+ [Theory]
+ [InlineData("scheme://host/", "scheme://host/")]
+ [InlineData("scheme://host/path", "scheme://host/path")]
+ [InlineData("scheme://host/path#hash", "scheme://host/path")]
+ [InlineData("scheme://host/path?query=value#hash", "scheme://host/path?query=value")]
+ public void GetUriWithHash_RemovesHashWhenHashIsNullOrEmpty(string baseUri, string expectedUri)
+ {
+ var navigationManager = new TestNavigationManager(baseUri);
+
+ var actualUriWithNull = navigationManager.GetUriWithHash(null);
+ Assert.Equal(expectedUri, actualUriWithNull);
+
+ var actualUriWithEmpty = navigationManager.GetUriWithHash(string.Empty);
+ Assert.Equal(expectedUri, actualUriWithEmpty);
+ }
+
+ [Fact]
+ public void GetUriWithHash_ThrowsWhenNavigationManagerIsNull()
+ {
+ NavigationManager navigationManager = null;
+
+ var exception = Assert.Throws(() => navigationManager.GetUriWithHash("hash"));
+ Assert.Equal("navigationManager", exception.ParamName);
+ }
+
[Fact]
public void LocationChangingHandlers_CanContinueTheNavigationSynchronously_WhenOneHandlerIsRegistered()
{
From e49d7097fe61c1f40389092a8d67888ca8eee31f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 3 Dec 2025 14:23:07 +0000
Subject: [PATCH 3/3] Optimize GetUriWithHash to use string.Create for minimal
allocations
Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
---
.../src/NavigationManagerExtensions.cs | 40 ++++++++++++++-----
1 file changed, 31 insertions(+), 9 deletions(-)
diff --git a/src/Components/Components/src/NavigationManagerExtensions.cs b/src/Components/Components/src/NavigationManagerExtensions.cs
index 43ca2648f75a..f3e2cba720bb 100644
--- a/src/Components/Components/src/NavigationManagerExtensions.cs
+++ b/src/Components/Components/src/NavigationManagerExtensions.cs
@@ -791,22 +791,44 @@ public static string GetUriWithHash(this NavigationManager navigationManager, st
ArgumentNullException.ThrowIfNull(navigationManager);
var uri = navigationManager.Uri;
- var hashStartIndex = uri.IndexOf('#');
+ var existingHashIndex = uri.IndexOf('#');
- // Get URI without the existing hash
- var uriWithoutHash = hashStartIndex < 0 ? uri : uri.Substring(0, hashStartIndex);
+ // Determine the length of the URI without the existing hash
+ var uriWithoutHashLength = existingHashIndex < 0 ? uri.Length : existingHashIndex;
if (string.IsNullOrEmpty(hash))
{
- return uriWithoutHash;
+ // If removing hash and there wasn't one, return original URI
+ if (existingHashIndex < 0)
+ {
+ return uri;
+ }
+
+ // Return just the URI part without hash
+ return uri.Substring(0, uriWithoutHashLength);
}
- // Ensure hash starts with '#'
- if (hash[0] != '#')
+ var hashStartsWithSymbol = hash[0] == '#';
+
+ // Calculate the total length needed
+ var totalLength = uriWithoutHashLength + (hashStartsWithSymbol ? hash.Length : hash.Length + 1);
+
+ return string.Create(totalLength, (uri, hash, uriWithoutHashLength, hashStartsWithSymbol), static (chars, state) =>
{
- return string.Concat(uriWithoutHash, "#", hash);
- }
+ var (uriValue, hashValue, uriLength, startsWithSymbol) = state;
+
+ // Copy URI without hash
+ uriValue.AsSpan(0, uriLength).CopyTo(chars);
+ var position = uriLength;
+
+ // Add '#' if hash doesn't start with one
+ if (!startsWithSymbol)
+ {
+ chars[position++] = '#';
+ }
- return string.Concat(uriWithoutHash, hash);
+ // Copy hash
+ hashValue.AsSpan().CopyTo(chars[position..]);
+ });
}
}