diff --git a/src/Components/Components/src/NavigationManagerExtensions.cs b/src/Components/Components/src/NavigationManagerExtensions.cs
new file mode 100644
index 000000000000..c8a04d28878c
--- /dev/null
+++ b/src/Components/Components/src/NavigationManagerExtensions.cs
@@ -0,0 +1,940 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections;
+using System.Globalization;
+using System.Text;
+using Microsoft.AspNetCore.Components.Routing;
+using Microsoft.AspNetCore.Internal;
+
+namespace Microsoft.AspNetCore.Components
+{
+ ///
+ /// Provides extension methods for the type.
+ ///
+ public static class NavigationManagerExtensions
+ {
+ private const string EmptyQueryParameterNameExceptionMessage = "Cannot have empty query parameter names.";
+
+ private delegate string? QueryParameterFormatter(TValue value);
+
+ // We don't include mappings for Nullable types because we explicitly check for null values
+ // to see if the parameter should be excluded from the querystring. Therefore, we will only
+ // invoke these formatters for non-null values. We also get the underlying type of any Nullable
+ // types before performing lookups in this dictionary.
+ private static readonly Dictionary> _queryParameterFormatters = new()
+ {
+ [typeof(string)] = value => Format((string)value)!,
+ [typeof(bool)] = value => Format((bool)value),
+ [typeof(DateTime)] = value => Format((DateTime)value),
+ [typeof(decimal)] = value => Format((decimal)value),
+ [typeof(double)] = value => Format((double)value),
+ [typeof(float)] = value => Format((float)value),
+ [typeof(Guid)] = value => Format((Guid)value),
+ [typeof(int)] = value => Format((int)value),
+ [typeof(long)] = value => Format((long)value),
+ };
+
+ private static string? Format(string? value)
+ => value;
+
+ private static string Format(bool value)
+ => value.ToString(CultureInfo.InvariantCulture);
+
+ private static string? Format(bool? value)
+ => value?.ToString(CultureInfo.InvariantCulture);
+
+ private static string Format(DateTime value)
+ => value.ToString(CultureInfo.InvariantCulture);
+
+ private static string? Format(DateTime? value)
+ => value?.ToString(CultureInfo.InvariantCulture);
+
+ private static string Format(decimal value)
+ => value.ToString(CultureInfo.InvariantCulture);
+
+ private static string? Format(decimal? value)
+ => value?.ToString(CultureInfo.InvariantCulture);
+
+ private static string Format(double value)
+ => value.ToString(CultureInfo.InvariantCulture);
+
+ private static string? Format(double? value)
+ => value?.ToString(CultureInfo.InvariantCulture);
+
+ private static string Format(float value)
+ => value.ToString(CultureInfo.InvariantCulture);
+
+ private static string? Format(float? value)
+ => value?.ToString(CultureInfo.InvariantCulture);
+
+ private static string Format(Guid value)
+ => value.ToString(null, CultureInfo.InvariantCulture);
+
+ private static string? Format(Guid? value)
+ => value?.ToString(null, CultureInfo.InvariantCulture);
+
+ private static string Format(int value)
+ => value.ToString(CultureInfo.InvariantCulture);
+
+ private static string? Format(int? value)
+ => value?.ToString(CultureInfo.InvariantCulture);
+
+ private static string Format(long value)
+ => value.ToString(CultureInfo.InvariantCulture);
+
+ private static string? Format(long? value)
+ => value?.ToString(CultureInfo.InvariantCulture);
+
+ // Used for constructing a URI with a new querystring from an existing URI.
+ private struct QueryStringBuilder
+ {
+ private readonly StringBuilder _builder;
+
+ private bool _hasNewParameters;
+
+ public string UriWithQueryString => _builder.ToString();
+
+ public QueryStringBuilder(ReadOnlySpan uriWithoutQueryString, int additionalCapacity = 0)
+ {
+ _builder = new(uriWithoutQueryString.Length + additionalCapacity);
+ _builder.Append(uriWithoutQueryString);
+
+ _hasNewParameters = false;
+ }
+
+ public void AppendParameter(ReadOnlySpan encodedName, ReadOnlySpan encodedValue)
+ {
+ if (!_hasNewParameters)
+ {
+ _hasNewParameters = true;
+ _builder.Append('?');
+ }
+ else
+ {
+ _builder.Append('&');
+ }
+
+ _builder.Append(encodedName);
+ _builder.Append('=');
+ _builder.Append(encodedValue);
+ }
+ }
+
+ // A utility for feeding a collection of parameter values into a QueryStringBuilder.
+ // This is used when generating a querystring with a query parameter that has multiple values.
+ private readonly struct QueryParameterSource
+ {
+ private readonly IEnumerator? _enumerator;
+ private readonly QueryParameterFormatter? _formatter;
+
+ public string EncodedName { get; }
+
+ // Creates an empty instance to simulate a source without any elements.
+ public QueryParameterSource(string name)
+ {
+ if (string.IsNullOrEmpty(name))
+ {
+ throw new InvalidOperationException(EmptyQueryParameterNameExceptionMessage);
+ }
+
+ EncodedName = Uri.EscapeDataString(name);
+
+ _enumerator = default;
+ _formatter = default;
+ }
+
+ public QueryParameterSource(string name, IEnumerable values, QueryParameterFormatter formatter)
+ : this(name)
+ {
+ _enumerator = values.GetEnumerator();
+ _formatter = formatter;
+ }
+
+ public bool TryAppendNextParameter(ref QueryStringBuilder builder)
+ {
+ if (_enumerator is null || !_enumerator.MoveNext())
+ {
+ return false;
+ }
+
+ var currentValue = _enumerator.Current;
+
+ if (currentValue is null)
+ {
+ // No-op to simulate appending a null parameter.
+ return true;
+ }
+
+ var formattedValue = _formatter!(currentValue);
+ var encodedValue = Uri.EscapeDataString(formattedValue!);
+ builder.AppendParameter(EncodedName, encodedValue);
+ return true;
+ }
+ }
+
+ // A utility for feeding an object of unknown type as one or more parameter values into
+ // a QueryStringBuilder.
+ private struct QueryParameterSource
+ {
+ private readonly QueryParameterSource