From df5b1b5ea4625a50a4f6eae0c292187d358e28e8 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 28 Jul 2021 10:53:07 -0700 Subject: [PATCH 01/14] Prototype for new querystring building APIs. --- .../src/NavigationManagerExtensions.cs | 524 ++++++++++++++++++ .../Components/test/NavigationManagerTest.cs | 56 ++ 2 files changed, 580 insertions(+) create mode 100644 src/Components/Components/src/NavigationManagerExtensions.cs diff --git a/src/Components/Components/src/NavigationManagerExtensions.cs b/src/Components/Components/src/NavigationManagerExtensions.cs new file mode 100644 index 000000000000..bbda3615414f --- /dev/null +++ b/src/Components/Components/src/NavigationManagerExtensions.cs @@ -0,0 +1,524 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Internal; + +namespace Microsoft.AspNetCore.Components +{ + /// + /// Provides extension methods for the type. + /// + public static class NavigationManagerExtensions + { + private delegate string? QueryParameterForamtter(object 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. + private static readonly Dictionary _queryParameterFormatters = new() + { + [typeof(string)] = value => (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(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); + + private struct QueryStringBuilder + { + private readonly StringBuilder _builder; + + private bool _hasNewParameters; + + public string UriWithQueryString => _builder.ToString(); + + public QueryStringBuilder(ReadOnlySpan uriWithoutQuery) + { + _builder = new(); + _builder.Append(uriWithoutQuery); + + _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); + } + } + + private class ParameterData + { + public string? EncodedValue { get; private set; } + public bool DidReplace { get; set; } + + public ParameterData(string? encodedValue) + { + EncodedValue = encodedValue; + DidReplace = false; + } + } + + private struct EncodedParameterNameEqualityComparer : IEqualityComparer> + { + public bool Equals(ReadOnlyMemory x, ReadOnlyMemory y) + => x.Span.SequenceEqual(y.Span); + + public int GetHashCode([DisallowNull] ReadOnlyMemory obj) + => string.GetHashCode(obj.Span); + } + + /// + /// Returns a equal to except with a single parameter added or updated. + /// + /// The . + /// The name of the paramter to add or update. + /// The value of the parameter to add or update. + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, bool value) + => UriWithQueryParameter(navigationManager, name, Format(value)); + + /// + /// Returns a equal to except with a single parameter added, updated, or removed. + /// + /// The . + /// The name of the paramter to add or update. + /// The value of the parameter to add or update. + /// + /// If is null, the parameter will be removed if it exists in the URI. Otherwise, it will be added or updated. + /// + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, bool? value) + => UriWithQueryParameter(navigationManager, name, Format(value)); + + /// + /// Returns a equal to except with a single parameter added or updated. + /// + /// The . + /// The name of the paramter to add or update. + /// The value of the parameter to add or update. + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, DateTime value) + => UriWithQueryParameter(navigationManager, name, Format(value)); + + /// + /// Returns a equal to except with a single parameter added, updated, or removed. + /// + /// The . + /// The name of the paramter to add or update. + /// The value of the parameter to add or update. + /// + /// If is null, the parameter will be removed if it exists in the URI. Otherwise, it will be added or updated. + /// + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, DateTime? value) + => UriWithQueryParameter(navigationManager, name, Format(value)); + + /// + /// Returns a equal to except with a single parameter added or updated. + /// + /// The . + /// The name of the paramter to add or update. + /// The value of the parameter to add or update. + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, decimal value) + => UriWithQueryParameter(navigationManager, name, Format(value)); + + /// + /// Returns a equal to except with a single parameter added, updated, or removed. + /// + /// The . + /// The name of the paramter to add or update. + /// The value of the parameter to add or update. + /// + /// If is null, the parameter will be removed if it exists in the URI. Otherwise, it will be added or updated. + /// + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, decimal? value) + => UriWithQueryParameter(navigationManager, name, Format(value)); + + /// + /// Returns a equal to except with a single parameter added or updated. + /// + /// The . + /// The name of the paramter to add or update. + /// The value of the parameter to add or update. + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, double value) + => UriWithQueryParameter(navigationManager, name, Format(value)); + + /// + /// Returns a equal to except with a single parameter added, updated, or removed. + /// + /// The . + /// The name of the paramter to add or update. + /// The value of the parameter to add or update. + /// + /// If is null, the parameter will be removed if it exists in the URI. Otherwise, it will be added or updated. + /// + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, double? value) + => UriWithQueryParameter(navigationManager, name, Format(value)); + + /// + /// Returns a equal to except with a single parameter added or updated. + /// + /// The . + /// The name of the paramter to add or update. + /// The value of the parameter to add or update. + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, float value) + => UriWithQueryParameter(navigationManager, name, Format(value)); + + /// + /// Returns a equal to except with a single parameter added, updated, or removed. + /// + /// The . + /// The name of the paramter to add or update. + /// The value of the parameter to add or update. + /// + /// If is null, the parameter will be removed if it exists in the URI. Otherwise, it will be added or updated. + /// + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, float? value) + => UriWithQueryParameter(navigationManager, name, Format(value)); + + /// + /// Returns a equal to except with a single parameter added or updated. + /// + /// The . + /// The name of the paramter to add or update. + /// The value of the parameter to add or update. + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, Guid value) + => UriWithQueryParameter(navigationManager, name, Format(value)); + + /// + /// Returns a equal to except with a single parameter added, updated, or removed. + /// + /// The . + /// The name of the paramter to add or update. + /// The value of the parameter to add or update. + /// + /// If is null, the parameter will be removed if it exists in the URI. Otherwise, it will be added or updated. + /// + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, Guid? value) + => UriWithQueryParameter(navigationManager, name, Format(value)); + + /// + /// Returns a equal to except with a single parameter added or updated. + /// + /// The . + /// The name of the paramter to add or update. + /// The value of the parameter to add or update. + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, int value) + => UriWithQueryParameter(navigationManager, name, Format(value)); + + /// + /// Returns a equal to except with a single parameter added, updated, or removed. + /// + /// The . + /// The name of the paramter to add or update. + /// The value of the parameter to add or update. + /// + /// If is null, the parameter will be removed if it exists in the URI. Otherwise, it will be added or updated. + /// + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, int? value) + => UriWithQueryParameter(navigationManager, name, Format(value)); + + /// + /// Returns a equal to except with a single parameter added or updated. + /// + /// The . + /// The name of the paramter to add or update. + /// The value of the parameter to add or update. + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, long value) + => UriWithQueryParameter(navigationManager, name, Format(value)); + + /// + /// Returns a equal to except with a single parameter added, updated, or removed. + /// + /// The . + /// The name of the paramter to add or update. + /// The value of the parameter to add or update. + /// + /// If is null, the parameter will be removed if it exists in the URI. Otherwise, it will be added or updated. + /// + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, long? value) + => UriWithQueryParameter(navigationManager, name, Format(value)); + + /// + /// Returns a equal to except with a single parameter added, updated, or removed. + /// + /// The . + /// The name of the paramter to add or update. + /// The value of the parameter to add or update. + /// + /// If is null, the parameter will be removed if it exists in the URI. Otherwise, it will be added or updated. + /// + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, string? value) + { + if (navigationManager is null) + { + throw new ArgumentNullException(nameof(navigationManager)); + } + + if (name is null) + { + throw new ArgumentNullException(nameof(name)); + } + + var uri = navigationManager.Uri; + + return value is null + ? UriWithoutQueryParameter(uri, name) + : UriWithQueryParameterCore(uri, name, value); + } + + private static string UriWithQueryParameterCore(string uri, string name, string value) + { + var encodedName = System.Uri.EscapeDataString(name); + var encodedValue = System.Uri.EscapeDataString(value); + + if (!TryRebuildExistingQueryFromUri(uri, out var oldQueryEnumerable, out var newQueryBuilder)) + { + // There was no existing query, so we can generate the new URI immediately. + return $"{uri}?{encodedName}={encodedValue}"; + } + + var didReplace = false; + foreach (var pair in oldQueryEnumerable) + { + if (pair.EncodedName.Span.SequenceEqual(encodedName)) + { + didReplace = true; + newQueryBuilder.AppendParameter(pair.EncodedName.Span, encodedValue); + } + else + { + newQueryBuilder.AppendParameter(pair.EncodedName.Span, pair.EncodedValue.Span); + } + } + + // If there was no matching parameter, add it to the end of the query. + if (!didReplace) + { + newQueryBuilder.AppendParameter(encodedName, encodedValue); + } + + return newQueryBuilder.UriWithQueryString; + } + + private static string UriWithoutQueryParameter(string uri, string name) + { + if (!TryRebuildExistingQueryFromUri(uri, out var oldQueryEnumerable, out var newQueryBuilder)) + { + // There was no existing query, so the URI remains unchanged. + return uri; + } + + var encodedName = System.Uri.EscapeDataString(name); + + // Rebuild the query omitting parameters with a matching name. + foreach (var pair in oldQueryEnumerable) + { + if (!pair.EncodedName.Span.SequenceEqual(encodedName)) + { + newQueryBuilder.AppendParameter(pair.EncodedName.Span, pair.EncodedValue.Span); + } + } + + return newQueryBuilder.UriWithQueryString; + } + + /// + /// Returns a equal to except with multiple parameters added, updated, or removed. + /// + /// The . + /// The values to add, update, or remove. + public static string UriWithQueryParameters(this NavigationManager navigationManager, IReadOnlyDictionary parameters) + => UriWithQueryParameters(navigationManager, navigationManager.Uri, parameters); + + /// + /// Returns a equal to except with multiple parameters added, updated, or removed. + /// + /// The . + /// The URI with the query to modify. + /// The values to add, update, or remove. + public static string UriWithQueryParameters(this NavigationManager navigationManager, string uri, IReadOnlyDictionary parameters) + { + if (navigationManager is null) + { + throw new ArgumentNullException(nameof(navigationManager)); + } + + if (uri is null) + { + throw new ArgumentNullException(nameof(uri)); + } + + if (!TryRebuildExistingQueryFromUri(uri, out var oldQueryEnumerable, out var newQueryBuilder)) + { + // There was no existing query, so there is no need to allocate a new dictionary to cache + // encoded parameter values and track which parameters have been added. + return UriWithAppendedQueryParameters(uri, parameters); + } + + // Build a dictionary mapping encoded parameter names to a class containing their encoded values + // and whether they've replaced an existing parameter. + var parameterDataByEncodedName = new Dictionary, ParameterData>(new EncodedParameterNameEqualityComparer()); + foreach (var (name, value) in parameters) + { + var encodedName = System.Uri.EscapeDataString(name).AsMemory(); + var encodedValue = GetEncodedParameterValue(value); + + parameterDataByEncodedName.Add(encodedName, new ParameterData(encodedValue)); + } + + // Rebuild the query, updating or removing parameters. + foreach (var pair in oldQueryEnumerable) + { + if (parameterDataByEncodedName.TryGetValue(pair.EncodedName, out var parameterData)) + { + parameterData.DidReplace = true; + + if (parameterData.EncodedValue is not null) + { + newQueryBuilder.AppendParameter(pair.EncodedName.Span, parameterData.EncodedValue); + } + } + else + { + newQueryBuilder.AppendParameter(pair.EncodedName.Span, pair.EncodedValue.Span); + } + } + + // Append any parameters with non-null values that did not replace existing parameters. + foreach (var (encodedName, data) in parameterDataByEncodedName) + { + if (!data.DidReplace && data.EncodedValue is not null) + { + newQueryBuilder.AppendParameter(encodedName.Span, data.EncodedValue); + } + } + + return newQueryBuilder.UriWithQueryString; + } + + private static string UriWithAppendedQueryParameters(string uriWithoutQuery, IReadOnlyDictionary parameters) + { + var builder = new QueryStringBuilder(uriWithoutQuery); + + // Build a new query from the existing URI, appending all parameters with non-null values. + foreach (var (name, value) in parameters) + { + var encodedName = System.Uri.EscapeDataString(name); + var encodedValue = GetEncodedParameterValue(value); + + if (encodedValue is not null) + { + builder.AppendParameter(encodedName, encodedValue); + } + } + + return builder.UriWithQueryString; + } + + private static string? GetEncodedParameterValue(object? value) + { + if (value is null) + { + return null; + } + + var parameterType = value.GetType(); + + if (!_queryParameterFormatters.TryGetValue(parameterType, out var formatter)) + { + throw new InvalidOperationException($"Cannot add query parameter of type '{parameterType}'."); + } + + var formattedValue = formatter(value); + return formattedValue is null ? null : System.Uri.EscapeDataString(formattedValue); + } + + private static bool TryRebuildExistingQueryFromUri(string uri, out QueryStringEnumerable oldQueryEnumerable, out QueryStringBuilder newQueryBuilder) + { + var queryStartIndex = uri.IndexOf('?'); + + if (queryStartIndex < 0) + { + oldQueryEnumerable = default; + newQueryBuilder = default; + return false; + } + + var query = uri.AsMemory(queryStartIndex); + oldQueryEnumerable = new(query); + + var uriWithoutQuery = uri.AsSpan(0, queryStartIndex); + newQueryBuilder = new(uriWithoutQuery); + + return true; + } + } +} diff --git a/src/Components/Components/test/NavigationManagerTest.cs b/src/Components/Components/test/NavigationManagerTest.cs index da506f429dfc..f373eaf91bec 100644 --- a/src/Components/Components/test/NavigationManagerTest.cs +++ b/src/Components/Components/test/NavigationManagerTest.cs @@ -92,6 +92,62 @@ public void ToBaseRelativePath_ThrowsForInvalidBaseRelativePaths(string baseUri, ex.Message); } + [Theory] + [InlineData("scheme://host/?name=Bob%20Joe&age=42", "scheme://host/?name=John%20Doe&age=42")] + [InlineData("scheme://host/?name=Sally%Smith&age=42&name=Emily", "scheme://host/?name=John%20Doe&age=42&name=John%20Doe")] + [InlineData("scheme://host/?name=&age=42", "scheme://host/?name=John%20Doe&age=42")] + [InlineData("scheme://host/?name=", "scheme://host/?name=John%20Doe")] + public void UriWithQueryParameter_ReplacesWhenParameterExists(string baseUri, string expectedUri) + { + var navigationManager = new TestNavigationManager(baseUri); + var actualUri = navigationManager.UriWithQueryParameter("name", "John Doe"); + + Assert.Equal(expectedUri, actualUri); + } + + [Theory] + [InlineData("scheme://host/?age=42", "scheme://host/?age=42&name=John%20Doe")] + [InlineData("scheme://host/", "scheme://host/?name=John%20Doe")] + [InlineData("scheme://host/?", "scheme://host/?name=John%20Doe")] + public void UriWithQueryParameter_AppendsWhenParamterDoesNotExist(string baseUri, string expectedUri) + { + var navigationManager = new TestNavigationManager(baseUri); + var actualUri = navigationManager.UriWithQueryParameter("name", "John Doe"); + + Assert.Equal(expectedUri, actualUri); + } + + [Theory] + [InlineData("scheme://host/?name=Bob%20Joe&age=42", "scheme://host/?age=42")] + [InlineData("scheme://host/?name=Sally%Smith&age=42&name=Emily", "scheme://host/?age=42")] + [InlineData("scheme://host/?name=&age=42", "scheme://host/?age=42")] + [InlineData("scheme://host/?name=", "scheme://host/")] + public void UriWithQueryParameter_RemovesWhenParameterValueIsNull(string baseUri, string expectedUri) + { + var navigationManager = new TestNavigationManager(baseUri); + var actualUri = navigationManager.UriWithQueryParameter("name", (string)null); + + Assert.Equal(expectedUri, actualUri); + } + + [Theory] + [InlineData("scheme://host/?name=Bob%20Joe&age=42", "scheme://host/?age=25&eye-color=green")] + [InlineData("scheme://host/?age=42&eye-color=87", "scheme://host/?age=25&eye-color=green")] + [InlineData("scheme://host/?", "scheme://host/?age=25&eye-color=green")] + [InlineData("scheme://host/", "scheme://host/?age=25&eye-color=green")] + public void UriWithQueryParameters_CanAddUpdateAndRemove(string baseUri, string expectedUri) + { + var navigationManager = new TestNavigationManager(baseUri); + var actualUri = navigationManager.UriWithQueryParameters(new Dictionary + { + ["name"] = null, // Remove + ["age"] = (int?)25, // Add/update + ["eye-color"] = "green",// Add/update + }); + + Assert.Equal(expectedUri, actualUri); + } + private class TestNavigationManager : NavigationManager { public TestNavigationManager() From 86d510d3f515a2b0add7d61a47f25698fb4bb910 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 28 Jul 2021 11:10:33 -0700 Subject: [PATCH 02/14] Formatting improvements. --- .../src/NavigationManagerExtensions.cs | 105 ++++++++++++------ 1 file changed, 72 insertions(+), 33 deletions(-) diff --git a/src/Components/Components/src/NavigationManagerExtensions.cs b/src/Components/Components/src/NavigationManagerExtensions.cs index bbda3615414f..3cb14dc01d04 100644 --- a/src/Components/Components/src/NavigationManagerExtensions.cs +++ b/src/Components/Components/src/NavigationManagerExtensions.cs @@ -139,7 +139,8 @@ public int GetHashCode([DisallowNull] ReadOnlyMemory obj) } /// - /// Returns a equal to except with a single parameter added or updated. + /// Returns a equal to except with a single parameter + /// added or updated. /// /// The . /// The name of the paramter to add or update. @@ -148,19 +149,22 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana => UriWithQueryParameter(navigationManager, name, Format(value)); /// - /// Returns a equal to except with a single parameter added, updated, or removed. + /// Returns a equal to except with a single parameter + /// added, updated, or removed. /// /// The . /// The name of the paramter to add or update. /// The value of the parameter to add or update. /// - /// If is null, the parameter will be removed if it exists in the URI. Otherwise, it will be added or updated. + /// If is null, the parameter will be removed if it exists in the URI. + /// Otherwise, it will be added or updated. /// public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, bool? value) => UriWithQueryParameter(navigationManager, name, Format(value)); /// - /// Returns a equal to except with a single parameter added or updated. + /// Returns a equal to except with a single parameter + /// added or updated. /// /// The . /// The name of the paramter to add or update. @@ -169,19 +173,22 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana => UriWithQueryParameter(navigationManager, name, Format(value)); /// - /// Returns a equal to except with a single parameter added, updated, or removed. + /// Returns a equal to except with a single parameter + /// added, updated, or removed. /// /// The . /// The name of the paramter to add or update. /// The value of the parameter to add or update. /// - /// If is null, the parameter will be removed if it exists in the URI. Otherwise, it will be added or updated. + /// If is null, the parameter will be removed if it exists in the URI. + /// Otherwise, it will be added or updated. /// public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, DateTime? value) => UriWithQueryParameter(navigationManager, name, Format(value)); /// - /// Returns a equal to except with a single parameter added or updated. + /// Returns a equal to except with a single parameter + /// added or updated. /// /// The . /// The name of the paramter to add or update. @@ -190,19 +197,22 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana => UriWithQueryParameter(navigationManager, name, Format(value)); /// - /// Returns a equal to except with a single parameter added, updated, or removed. + /// Returns a equal to except with a single parameter + /// added, updated, or removed. /// /// The . /// The name of the paramter to add or update. /// The value of the parameter to add or update. /// - /// If is null, the parameter will be removed if it exists in the URI. Otherwise, it will be added or updated. + /// If is null, the parameter will be removed if it exists in the URI. + /// Otherwise, it will be added or updated. /// public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, decimal? value) => UriWithQueryParameter(navigationManager, name, Format(value)); /// - /// Returns a equal to except with a single parameter added or updated. + /// Returns a equal to except with a single parameter + /// added or updated. /// /// The . /// The name of the paramter to add or update. @@ -211,19 +221,22 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana => UriWithQueryParameter(navigationManager, name, Format(value)); /// - /// Returns a equal to except with a single parameter added, updated, or removed. + /// Returns a equal to except with a single parameter + /// added, updated, or removed. /// /// The . /// The name of the paramter to add or update. /// The value of the parameter to add or update. /// - /// If is null, the parameter will be removed if it exists in the URI. Otherwise, it will be added or updated. + /// If is null, the parameter will be removed if it exists in the URI. + /// Otherwise, it will be added or updated. /// public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, double? value) => UriWithQueryParameter(navigationManager, name, Format(value)); /// - /// Returns a equal to except with a single parameter added or updated. + /// Returns a equal to except with a single parameter + /// added or updated. /// /// The . /// The name of the paramter to add or update. @@ -232,19 +245,22 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana => UriWithQueryParameter(navigationManager, name, Format(value)); /// - /// Returns a equal to except with a single parameter added, updated, or removed. + /// Returns a equal to except with a single parameter + /// added, updated, or removed. /// /// The . /// The name of the paramter to add or update. /// The value of the parameter to add or update. /// - /// If is null, the parameter will be removed if it exists in the URI. Otherwise, it will be added or updated. + /// If is null, the parameter will be removed if it exists in the URI. + /// Otherwise, it will be added or updated. /// public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, float? value) => UriWithQueryParameter(navigationManager, name, Format(value)); /// - /// Returns a equal to except with a single parameter added or updated. + /// Returns a equal to except with a single parameter + /// added or updated. /// /// The . /// The name of the paramter to add or update. @@ -253,19 +269,22 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana => UriWithQueryParameter(navigationManager, name, Format(value)); /// - /// Returns a equal to except with a single parameter added, updated, or removed. + /// Returns a equal to except with a single parameter + /// added, updated, or removed. /// /// The . /// The name of the paramter to add or update. /// The value of the parameter to add or update. /// - /// If is null, the parameter will be removed if it exists in the URI. Otherwise, it will be added or updated. + /// If is null, the parameter will be removed if it exists in the URI. + /// Otherwise, it will be added or updated. /// public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, Guid? value) => UriWithQueryParameter(navigationManager, name, Format(value)); /// - /// Returns a equal to except with a single parameter added or updated. + /// Returns a equal to except with a single parameter + /// added or updated. /// /// The . /// The name of the paramter to add or update. @@ -274,19 +293,22 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana => UriWithQueryParameter(navigationManager, name, Format(value)); /// - /// Returns a equal to except with a single parameter added, updated, or removed. + /// Returns a equal to except with a single parameter + /// added, updated, or removed. /// /// The . /// The name of the paramter to add or update. /// The value of the parameter to add or update. /// - /// If is null, the parameter will be removed if it exists in the URI. Otherwise, it will be added or updated. + /// If is null, the parameter will be removed if it exists in the URI. + /// Otherwise, it will be added or updated. /// public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, int? value) => UriWithQueryParameter(navigationManager, name, Format(value)); /// - /// Returns a equal to except with a single parameter added or updated. + /// Returns a equal to except with a single parameter + /// added or updated. /// /// The . /// The name of the paramter to add or update. @@ -295,25 +317,29 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana => UriWithQueryParameter(navigationManager, name, Format(value)); /// - /// Returns a equal to except with a single parameter added, updated, or removed. + /// Returns a equal to except with a single parameter + /// added, updated, or removed. /// /// The . /// The name of the paramter to add or update. /// The value of the parameter to add or update. /// - /// If is null, the parameter will be removed if it exists in the URI. Otherwise, it will be added or updated. + /// If is null, the parameter will be removed if it exists in the URI. + /// Otherwise, it will be added or updated. /// public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, long? value) => UriWithQueryParameter(navigationManager, name, Format(value)); /// - /// Returns a equal to except with a single parameter added, updated, or removed. + /// Returns a equal to except with a single parameter + /// added, updated, or removed. /// /// The . /// The name of the paramter to add or update. /// The value of the parameter to add or update. /// - /// If is null, the parameter will be removed if it exists in the URI. Otherwise, it will be added or updated. + /// If is null, the parameter will be removed if it exists in the URI. + /// Otherwise, it will be added or updated. /// public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, string? value) { @@ -391,20 +417,27 @@ private static string UriWithoutQueryParameter(string uri, string name) } /// - /// Returns a equal to except with multiple parameters added, updated, or removed. + /// Returns a equal to except with multiple parameters + /// added, updated, or removed. /// /// The . /// The values to add, update, or remove. - public static string UriWithQueryParameters(this NavigationManager navigationManager, IReadOnlyDictionary parameters) + public static string UriWithQueryParameters( + this NavigationManager navigationManager, + IReadOnlyDictionary parameters) => UriWithQueryParameters(navigationManager, navigationManager.Uri, parameters); /// - /// Returns a equal to except with multiple parameters added, updated, or removed. + /// Returns a equal to except with multiple parameters + /// added, updated, or removed. /// /// The . /// The URI with the query to modify. /// The values to add, update, or remove. - public static string UriWithQueryParameters(this NavigationManager navigationManager, string uri, IReadOnlyDictionary parameters) + public static string UriWithQueryParameters( + this NavigationManager navigationManager, + string uri, + IReadOnlyDictionary parameters) { if (navigationManager is null) { @@ -425,7 +458,8 @@ public static string UriWithQueryParameters(this NavigationManager navigationMan // Build a dictionary mapping encoded parameter names to a class containing their encoded values // and whether they've replaced an existing parameter. - var parameterDataByEncodedName = new Dictionary, ParameterData>(new EncodedParameterNameEqualityComparer()); + var parameterDataByEncodedName = new Dictionary, ParameterData>( + new EncodedParameterNameEqualityComparer()); foreach (var (name, value) in parameters) { var encodedName = System.Uri.EscapeDataString(name).AsMemory(); @@ -464,7 +498,9 @@ public static string UriWithQueryParameters(this NavigationManager navigationMan return newQueryBuilder.UriWithQueryString; } - private static string UriWithAppendedQueryParameters(string uriWithoutQuery, IReadOnlyDictionary parameters) + private static string UriWithAppendedQueryParameters( + string uriWithoutQuery, + IReadOnlyDictionary parameters) { var builder = new QueryStringBuilder(uriWithoutQuery); @@ -501,7 +537,10 @@ private static string UriWithAppendedQueryParameters(string uriWithoutQuery, IRe return formattedValue is null ? null : System.Uri.EscapeDataString(formattedValue); } - private static bool TryRebuildExistingQueryFromUri(string uri, out QueryStringEnumerable oldQueryEnumerable, out QueryStringBuilder newQueryBuilder) + private static bool TryRebuildExistingQueryFromUri( + string uri, + out QueryStringEnumerable oldQueryEnumerable, + out QueryStringBuilder newQueryBuilder) { var queryStartIndex = uri.IndexOf('?'); From 19337e231145b0078ed9dc9375af9959fd733586 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 28 Jul 2021 11:17:57 -0700 Subject: [PATCH 03/14] Naming improvements. --- .../src/NavigationManagerExtensions.cs | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/Components/Components/src/NavigationManagerExtensions.cs b/src/Components/Components/src/NavigationManagerExtensions.cs index 3cb14dc01d04..b1447085ca47 100644 --- a/src/Components/Components/src/NavigationManagerExtensions.cs +++ b/src/Components/Components/src/NavigationManagerExtensions.cs @@ -91,10 +91,10 @@ private struct QueryStringBuilder public string UriWithQueryString => _builder.ToString(); - public QueryStringBuilder(ReadOnlySpan uriWithoutQuery) + public QueryStringBuilder(ReadOnlySpan uriWithoutQueryString) { _builder = new(); - _builder.Append(uriWithoutQuery); + _builder.Append(uriWithoutQueryString); _hasNewParameters = false; } @@ -365,38 +365,38 @@ private static string UriWithQueryParameterCore(string uri, string name, string var encodedName = System.Uri.EscapeDataString(name); var encodedValue = System.Uri.EscapeDataString(value); - if (!TryRebuildExistingQueryFromUri(uri, out var oldQueryEnumerable, out var newQueryBuilder)) + if (!TryRebuildExistingQueryFromUri(uri, out var existingQueryStringEnumerable, out var newQueryStringBuilder)) { // There was no existing query, so we can generate the new URI immediately. return $"{uri}?{encodedName}={encodedValue}"; } var didReplace = false; - foreach (var pair in oldQueryEnumerable) + foreach (var pair in existingQueryStringEnumerable) { if (pair.EncodedName.Span.SequenceEqual(encodedName)) { didReplace = true; - newQueryBuilder.AppendParameter(pair.EncodedName.Span, encodedValue); + newQueryStringBuilder.AppendParameter(pair.EncodedName.Span, encodedValue); } else { - newQueryBuilder.AppendParameter(pair.EncodedName.Span, pair.EncodedValue.Span); + newQueryStringBuilder.AppendParameter(pair.EncodedName.Span, pair.EncodedValue.Span); } } // If there was no matching parameter, add it to the end of the query. if (!didReplace) { - newQueryBuilder.AppendParameter(encodedName, encodedValue); + newQueryStringBuilder.AppendParameter(encodedName, encodedValue); } - return newQueryBuilder.UriWithQueryString; + return newQueryStringBuilder.UriWithQueryString; } private static string UriWithoutQueryParameter(string uri, string name) { - if (!TryRebuildExistingQueryFromUri(uri, out var oldQueryEnumerable, out var newQueryBuilder)) + if (!TryRebuildExistingQueryFromUri(uri, out var existingQueryStringEnumerable, out var newQueryStringBuilder)) { // There was no existing query, so the URI remains unchanged. return uri; @@ -405,15 +405,15 @@ private static string UriWithoutQueryParameter(string uri, string name) var encodedName = System.Uri.EscapeDataString(name); // Rebuild the query omitting parameters with a matching name. - foreach (var pair in oldQueryEnumerable) + foreach (var pair in existingQueryStringEnumerable) { if (!pair.EncodedName.Span.SequenceEqual(encodedName)) { - newQueryBuilder.AppendParameter(pair.EncodedName.Span, pair.EncodedValue.Span); + newQueryStringBuilder.AppendParameter(pair.EncodedName.Span, pair.EncodedValue.Span); } } - return newQueryBuilder.UriWithQueryString; + return newQueryStringBuilder.UriWithQueryString; } /// @@ -449,7 +449,7 @@ public static string UriWithQueryParameters( throw new ArgumentNullException(nameof(uri)); } - if (!TryRebuildExistingQueryFromUri(uri, out var oldQueryEnumerable, out var newQueryBuilder)) + if (!TryRebuildExistingQueryFromUri(uri, out var existingQueryStringEnumerable, out var newQueryStringBuilder)) { // There was no existing query, so there is no need to allocate a new dictionary to cache // encoded parameter values and track which parameters have been added. @@ -469,7 +469,7 @@ public static string UriWithQueryParameters( } // Rebuild the query, updating or removing parameters. - foreach (var pair in oldQueryEnumerable) + foreach (var pair in existingQueryStringEnumerable) { if (parameterDataByEncodedName.TryGetValue(pair.EncodedName, out var parameterData)) { @@ -477,12 +477,12 @@ public static string UriWithQueryParameters( if (parameterData.EncodedValue is not null) { - newQueryBuilder.AppendParameter(pair.EncodedName.Span, parameterData.EncodedValue); + newQueryStringBuilder.AppendParameter(pair.EncodedName.Span, parameterData.EncodedValue); } } else { - newQueryBuilder.AppendParameter(pair.EncodedName.Span, pair.EncodedValue.Span); + newQueryStringBuilder.AppendParameter(pair.EncodedName.Span, pair.EncodedValue.Span); } } @@ -491,18 +491,18 @@ public static string UriWithQueryParameters( { if (!data.DidReplace && data.EncodedValue is not null) { - newQueryBuilder.AppendParameter(encodedName.Span, data.EncodedValue); + newQueryStringBuilder.AppendParameter(encodedName.Span, data.EncodedValue); } } - return newQueryBuilder.UriWithQueryString; + return newQueryStringBuilder.UriWithQueryString; } private static string UriWithAppendedQueryParameters( - string uriWithoutQuery, + string uriWithoutQueryString, IReadOnlyDictionary parameters) { - var builder = new QueryStringBuilder(uriWithoutQuery); + var builder = new QueryStringBuilder(uriWithoutQueryString); // Build a new query from the existing URI, appending all parameters with non-null values. foreach (var (name, value) in parameters) @@ -539,23 +539,23 @@ private static string UriWithAppendedQueryParameters( private static bool TryRebuildExistingQueryFromUri( string uri, - out QueryStringEnumerable oldQueryEnumerable, - out QueryStringBuilder newQueryBuilder) + out QueryStringEnumerable existingQueryStringEnumerable, + out QueryStringBuilder newQueryStringBuilder) { var queryStartIndex = uri.IndexOf('?'); if (queryStartIndex < 0) { - oldQueryEnumerable = default; - newQueryBuilder = default; + existingQueryStringEnumerable = default; + newQueryStringBuilder = default; return false; } var query = uri.AsMemory(queryStartIndex); - oldQueryEnumerable = new(query); + existingQueryStringEnumerable = new(query); - var uriWithoutQuery = uri.AsSpan(0, queryStartIndex); - newQueryBuilder = new(uriWithoutQuery); + var uriWithoutQueryString = uri.AsSpan(0, queryStartIndex); + newQueryStringBuilder = new(uriWithoutQueryString); return true; } From bf04828bc3ffc568c96f7113e8d8b46529aae579 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 28 Jul 2021 11:28:37 -0700 Subject: [PATCH 04/14] Spaces > tabs. --- .../Components/src/NavigationManagerExtensions.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Components/Components/src/NavigationManagerExtensions.cs b/src/Components/Components/src/NavigationManagerExtensions.cs index b1447085ca47..b79446db79d8 100644 --- a/src/Components/Components/src/NavigationManagerExtensions.cs +++ b/src/Components/Components/src/NavigationManagerExtensions.cs @@ -450,13 +450,13 @@ public static string UriWithQueryParameters( } if (!TryRebuildExistingQueryFromUri(uri, out var existingQueryStringEnumerable, out var newQueryStringBuilder)) - { + { // There was no existing query, so there is no need to allocate a new dictionary to cache // encoded parameter values and track which parameters have been added. return UriWithAppendedQueryParameters(uri, parameters); - } + } - // Build a dictionary mapping encoded parameter names to a class containing their encoded values + // Build a dictionary mapping encoded parameter names to an object containing their encoded values // and whether they've replaced an existing parameter. var parameterDataByEncodedName = new Dictionary, ParameterData>( new EncodedParameterNameEqualityComparer()); From e0796a35d46eca10cbdad096acb7b28ef1afb527 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 28 Jul 2021 11:54:39 -0700 Subject: [PATCH 05/14] Make QueryParameterNameComparer internal. --- .../src/NavigationManagerExtensions.cs | 12 ++------- .../src/Routing/QueryParameterNameComparer.cs | 26 +++++++++++++++++++ .../Routing/QueryParameterValueSupplier.cs | 14 ---------- 3 files changed, 28 insertions(+), 24 deletions(-) create mode 100644 src/Components/Components/src/Routing/QueryParameterNameComparer.cs diff --git a/src/Components/Components/src/NavigationManagerExtensions.cs b/src/Components/Components/src/NavigationManagerExtensions.cs index b79446db79d8..796179b6d189 100644 --- a/src/Components/Components/src/NavigationManagerExtensions.cs +++ b/src/Components/Components/src/NavigationManagerExtensions.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.Routing; using Microsoft.AspNetCore.Internal; namespace Microsoft.AspNetCore.Components @@ -129,15 +130,6 @@ public ParameterData(string? encodedValue) } } - private struct EncodedParameterNameEqualityComparer : IEqualityComparer> - { - public bool Equals(ReadOnlyMemory x, ReadOnlyMemory y) - => x.Span.SequenceEqual(y.Span); - - public int GetHashCode([DisallowNull] ReadOnlyMemory obj) - => string.GetHashCode(obj.Span); - } - /// /// Returns a equal to except with a single parameter /// added or updated. @@ -459,7 +451,7 @@ public static string UriWithQueryParameters( // Build a dictionary mapping encoded parameter names to an object containing their encoded values // and whether they've replaced an existing parameter. var parameterDataByEncodedName = new Dictionary, ParameterData>( - new EncodedParameterNameEqualityComparer()); + QueryParameterNameComparer.Instance); foreach (var (name, value) in parameters) { var encodedName = System.Uri.EscapeDataString(name).AsMemory(); diff --git a/src/Components/Components/src/Routing/QueryParameterNameComparer.cs b/src/Components/Components/src/Routing/QueryParameterNameComparer.cs new file mode 100644 index 000000000000..65e5771a26f4 --- /dev/null +++ b/src/Components/Components/src/Routing/QueryParameterNameComparer.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Components.Routing +{ + internal class QueryParameterNameComparer : IComparer>, IEqualityComparer> + { + public static readonly QueryParameterNameComparer Instance = new(); + + public int Compare(ReadOnlyMemory x, ReadOnlyMemory y) + => x.Span.CompareTo(y.Span, StringComparison.OrdinalIgnoreCase); + + public bool Equals(ReadOnlyMemory x, ReadOnlyMemory y) + => x.Span.Equals(y.Span, StringComparison.OrdinalIgnoreCase); + + public int GetHashCode([DisallowNull] ReadOnlyMemory obj) + => string.GetHashCode(obj.Span, StringComparison.OrdinalIgnoreCase); + } +} diff --git a/src/Components/Components/src/Routing/QueryParameterValueSupplier.cs b/src/Components/Components/src/Routing/QueryParameterValueSupplier.cs index 49b4e196922b..3243c41618d9 100644 --- a/src/Components/Components/src/Routing/QueryParameterValueSupplier.cs +++ b/src/Components/Components/src/Routing/QueryParameterValueSupplier.cs @@ -187,19 +187,5 @@ public QueryParameterDestination(string componentParameterName, UrlValueConstrai IsArray = isArray; } } - - private class QueryParameterNameComparer : IComparer>, IEqualityComparer> - { - public static readonly QueryParameterNameComparer Instance = new(); - - public int Compare(ReadOnlyMemory x, ReadOnlyMemory y) - => x.Span.CompareTo(y.Span, StringComparison.OrdinalIgnoreCase); - - public bool Equals(ReadOnlyMemory x, ReadOnlyMemory y) - => x.Span.Equals(y.Span, StringComparison.OrdinalIgnoreCase); - - public int GetHashCode([DisallowNull] ReadOnlyMemory obj) - => string.GetHashCode(obj.Span, StringComparison.OrdinalIgnoreCase); - } } } From 1fcc9e46c4076c1ebf7db38d228eef36bbf1120e Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 28 Jul 2021 14:52:39 -0700 Subject: [PATCH 06/14] Added support for IEnumerable parameter values. --- .../src/NavigationManagerExtensions.cs | 163 +++++++++++++++--- .../Components/test/NavigationManagerTest.cs | 34 +++- 2 files changed, 174 insertions(+), 23 deletions(-) diff --git a/src/Components/Components/src/NavigationManagerExtensions.cs b/src/Components/Components/src/NavigationManagerExtensions.cs index 796179b6d189..2371a3e77d26 100644 --- a/src/Components/Components/src/NavigationManagerExtensions.cs +++ b/src/Components/Components/src/NavigationManagerExtensions.cs @@ -18,12 +18,13 @@ namespace Microsoft.AspNetCore.Components /// public static class NavigationManagerExtensions { - private delegate string? QueryParameterForamtter(object value); + private delegate string QueryParameterFormatter(object value); - // We don't include mappings for Nullable types, because we explicitly check for null values + // 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. - private static readonly Dictionary _queryParameterFormatters = new() + // 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 => (string)value, [typeof(bool)] = value => Format((bool)value), @@ -36,49 +37,49 @@ public static class NavigationManagerExtensions [typeof(long)] = value => Format((long)value), }; - private static string? Format(bool 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) + 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) + 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) + 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) + 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) + 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) + 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) + private static string Format(long value) => value.ToString(CultureInfo.InvariantCulture); private static string? Format(long? value) @@ -352,10 +353,95 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana : UriWithQueryParameterCore(uri, name, value); } + /// + /// Returns a equal to except with a single parameter + /// updated with the provided . + /// + /// The . + /// The name of the paramter to add or update. + /// The value of the parameter to add or update. + /// + /// Any null entries in will be skipped. Existing querystring parameters not in + /// will be removed from the querystring in the returned URI. + /// + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) + { + if (navigationManager is null) + { + throw new ArgumentNullException(nameof(navigationManager)); + } + + if (name is null) + { + throw new ArgumentNullException(nameof(name)); + } + + var uri = navigationManager.Uri; + + if (!TryRebuildExistingQueryFromUri(uri, out var existingQueryStringEnumerable, out var newQueryStringBuilder)) + { + return UriWithAppendedQueryParameters(uri, name, values); + } + + var formatter = GetFormatterFromParameterValueType(typeof(TValue)); + var encodedName = Uri.EscapeDataString(name).AsSpan(); + var valueEnumerator = values.GetEnumerator(); + var hasNextValue = valueEnumerator.MoveNext(); + + foreach (var pair in existingQueryStringEnumerable) + { + if (pair.EncodedName.Span.SequenceEqual(encodedName)) + { + if (!hasNextValue) + { + // We've added all provided values, so we'll skip any parameters with the provided name from + // now on. + continue; + } + + hasNextValue = AppendNextValue(pair.EncodedName.Span, valueEnumerator, formatter, ref newQueryStringBuilder); + } + else + { + newQueryStringBuilder.AppendParameter(pair.EncodedName.Span, pair.EncodedValue.Span); + } + } + + while (hasNextValue) + { + hasNextValue = AppendNextValue(encodedName, valueEnumerator, formatter, ref newQueryStringBuilder); + } + + return newQueryStringBuilder.UriWithQueryString; + + static bool AppendNextValue( + ReadOnlySpan name, + IEnumerator valueEnumerator, + QueryParameterFormatter formatter, + ref QueryStringBuilder queryStringBuilder) + { + var currentValue = valueEnumerator.Current; + var hasNext = valueEnumerator.MoveNext(); + + if (currentValue is null) + { + // If we encounter a null value, we exclude it from the querystring, so we skip whatever + // the old value was. + return hasNext; + } + + var formattedValue = formatter(currentValue); + var encodedValue = Uri.EscapeDataString(formattedValue); + queryStringBuilder.AppendParameter(name, encodedValue); + + return hasNext; + } + } + private static string UriWithQueryParameterCore(string uri, string name, string value) { - var encodedName = System.Uri.EscapeDataString(name); - var encodedValue = System.Uri.EscapeDataString(value); + var encodedName = Uri.EscapeDataString(name); + var encodedValue = Uri.EscapeDataString(value); if (!TryRebuildExistingQueryFromUri(uri, out var existingQueryStringEnumerable, out var newQueryStringBuilder)) { @@ -394,7 +480,7 @@ private static string UriWithoutQueryParameter(string uri, string name) return uri; } - var encodedName = System.Uri.EscapeDataString(name); + var encodedName = Uri.EscapeDataString(name); // Rebuild the query omitting parameters with a matching name. foreach (var pair in existingQueryStringEnumerable) @@ -454,7 +540,7 @@ public static string UriWithQueryParameters( QueryParameterNameComparer.Instance); foreach (var (name, value) in parameters) { - var encodedName = System.Uri.EscapeDataString(name).AsMemory(); + var encodedName = Uri.EscapeDataString(name).AsMemory(); var encodedValue = GetEncodedParameterValue(value); parameterDataByEncodedName.Add(encodedName, new ParameterData(encodedValue)); @@ -490,6 +576,32 @@ public static string UriWithQueryParameters( return newQueryStringBuilder.UriWithQueryString; } + private static string UriWithAppendedQueryParameters( + string uriWithoutQueryString, + string name, + IEnumerable values) + { + var formatter = GetFormatterFromParameterValueType(typeof(TValue)); + var encodedName = Uri.EscapeDataString(name); + var builder = new QueryStringBuilder(uriWithoutQueryString); + + // Build a new query from the existing URI, appending all values. + foreach (var value in values) + { + if (value is null) + { + continue; + } + + var formattedValue = formatter(value); + var encodedValue = Uri.EscapeDataString(formattedValue); + + builder.AppendParameter(encodedName, encodedValue); + } + + return builder.UriWithQueryString; + } + private static string UriWithAppendedQueryParameters( string uriWithoutQueryString, IReadOnlyDictionary parameters) @@ -499,7 +611,7 @@ private static string UriWithAppendedQueryParameters( // Build a new query from the existing URI, appending all parameters with non-null values. foreach (var (name, value) in parameters) { - var encodedName = System.Uri.EscapeDataString(name); + var encodedName = Uri.EscapeDataString(name); var encodedValue = GetEncodedParameterValue(value); if (encodedValue is not null) @@ -518,15 +630,22 @@ private static string UriWithAppendedQueryParameters( return null; } - var parameterType = value.GetType(); + var formatter = GetFormatterFromParameterValueType(value.GetType()); + var formattedValue = formatter(value); + return formattedValue is null ? null : Uri.EscapeDataString(formattedValue); + } + + private static QueryParameterFormatter GetFormatterFromParameterValueType(Type parameterValueType) + { + var underlyingParameterValueType = Nullable.GetUnderlyingType(parameterValueType) ?? parameterValueType; - if (!_queryParameterFormatters.TryGetValue(parameterType, out var formatter)) + if (!_queryParameterFormatters.TryGetValue(underlyingParameterValueType, out var formatter)) { - throw new InvalidOperationException($"Cannot add query parameter of type '{parameterType}'."); + throw new InvalidOperationException( + $"Cannot format query parameters with values of type '{underlyingParameterValueType}'."); } - var formattedValue = formatter(value); - return formattedValue is null ? null : System.Uri.EscapeDataString(formattedValue); + return formatter; } private static bool TryRebuildExistingQueryFromUri( diff --git a/src/Components/Components/test/NavigationManagerTest.cs b/src/Components/Components/test/NavigationManagerTest.cs index f373eaf91bec..ba65a3d3812f 100644 --- a/src/Components/Components/test/NavigationManagerTest.cs +++ b/src/Components/Components/test/NavigationManagerTest.cs @@ -109,7 +109,7 @@ public void UriWithQueryParameter_ReplacesWhenParameterExists(string baseUri, st [InlineData("scheme://host/?age=42", "scheme://host/?age=42&name=John%20Doe")] [InlineData("scheme://host/", "scheme://host/?name=John%20Doe")] [InlineData("scheme://host/?", "scheme://host/?name=John%20Doe")] - public void UriWithQueryParameter_AppendsWhenParamterDoesNotExist(string baseUri, string expectedUri) + public void UriWithQueryParameter_AppendsWhenParameterDoesNotExist(string baseUri, string expectedUri) { var navigationManager = new TestNavigationManager(baseUri); var actualUri = navigationManager.UriWithQueryParameter("name", "John Doe"); @@ -130,6 +130,38 @@ public void UriWithQueryParameter_RemovesWhenParameterValueIsNull(string baseUri Assert.Equal(expectedUri, actualUri); } + [Theory] + [InlineData("scheme://host/?search=rugs&filter=price%3Ahigh", "scheme://host/?search=rugs&filter=price%3Alow&filter=shipping%3Afree&filter=category%3Arug")] + [InlineData("scheme://host/?filter=price%3Ahigh&search=rugs&filter=shipping%3A2day", "scheme://host/?filter=price%3Alow&search=rugs&filter=shipping%3Afree&filter=category%3Arug")] + [InlineData("scheme://host/?filter=price&filter=shipping%3A2day&filter=category%3Arug&filter=availability%3Atoday", "scheme://host/?filter=price%3Alow&filter=shipping%3Afree&filter=category%3Arug")] + public void UriWithQueryParameterOfTValue_ReplacesExistingQueryParameters(string baseUri, string expectedUri) + { + var navigationManager = new TestNavigationManager(baseUri); + var actualUri = navigationManager.UriWithQueryParameter("filter", new string[] + { + "price:low", + "shipping:free", + "category:rug", + }); + + Assert.Equal(expectedUri, actualUri); + } + + [Theory] + [InlineData("scheme://host/?search=rugs&items=8&items=42", "scheme://host/?search=rugs&items=5&items=13")] + public void UriWithQueryParameterOfTValue_SkipsNullValues(string baseUri, string expectedUri) + { + var navigationManager = new TestNavigationManager(baseUri); + var actualUri = navigationManager.UriWithQueryParameter("items", new int?[] + { + 5, + null, + 13, + }); + + Assert.Equal(expectedUri, actualUri); + } + [Theory] [InlineData("scheme://host/?name=Bob%20Joe&age=42", "scheme://host/?age=25&eye-color=green")] [InlineData("scheme://host/?age=42&eye-color=87", "scheme://host/?age=25&eye-color=green")] From c38c025b59b419c6f3b7220c105e0034b3272cf2 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 28 Jul 2021 14:54:48 -0700 Subject: [PATCH 07/14] Update NavigationManagerTest.cs --- src/Components/Components/test/NavigationManagerTest.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Components/Components/test/NavigationManagerTest.cs b/src/Components/Components/test/NavigationManagerTest.cs index ba65a3d3812f..e2a3805fd5ae 100644 --- a/src/Components/Components/test/NavigationManagerTest.cs +++ b/src/Components/Components/test/NavigationManagerTest.cs @@ -134,6 +134,7 @@ public void UriWithQueryParameter_RemovesWhenParameterValueIsNull(string baseUri [InlineData("scheme://host/?search=rugs&filter=price%3Ahigh", "scheme://host/?search=rugs&filter=price%3Alow&filter=shipping%3Afree&filter=category%3Arug")] [InlineData("scheme://host/?filter=price%3Ahigh&search=rugs&filter=shipping%3A2day", "scheme://host/?filter=price%3Alow&search=rugs&filter=shipping%3Afree&filter=category%3Arug")] [InlineData("scheme://host/?filter=price&filter=shipping%3A2day&filter=category%3Arug&filter=availability%3Atoday", "scheme://host/?filter=price%3Alow&filter=shipping%3Afree&filter=category%3Arug")] + [InlineData("scheme://host/", "scheme://host/?filter=price%3Alow&filter=shipping%3Afree&filter=category%3Arug")] public void UriWithQueryParameterOfTValue_ReplacesExistingQueryParameters(string baseUri, string expectedUri) { var navigationManager = new TestNavigationManager(baseUri); @@ -149,6 +150,7 @@ public void UriWithQueryParameterOfTValue_ReplacesExistingQueryParameters(string [Theory] [InlineData("scheme://host/?search=rugs&items=8&items=42", "scheme://host/?search=rugs&items=5&items=13")] + [InlineData("scheme://host/", "scheme://host/?items=5&items=13")] public void UriWithQueryParameterOfTValue_SkipsNullValues(string baseUri, string expectedUri) { var navigationManager = new TestNavigationManager(baseUri); From d2e2dfa91e2c1d6b931c731c852956316572b206 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 28 Jul 2021 16:32:41 -0700 Subject: [PATCH 08/14] Added more unit tests. --- .../src/NavigationManagerExtensions.cs | 32 +++++++-- .../Components/test/NavigationManagerTest.cs | 68 +++++++++++++++++++ 2 files changed, 94 insertions(+), 6 deletions(-) diff --git a/src/Components/Components/src/NavigationManagerExtensions.cs b/src/Components/Components/src/NavigationManagerExtensions.cs index 2371a3e77d26..37376fee187f 100644 --- a/src/Components/Components/src/NavigationManagerExtensions.cs +++ b/src/Components/Components/src/NavigationManagerExtensions.cs @@ -18,6 +18,8 @@ namespace Microsoft.AspNetCore.Components /// public static class NavigationManagerExtensions { + private const string EmptyQueryParameterExceptionMessage = "Cannot have empty query parameter names."; + private delegate string QueryParameterFormatter(object value); // We don't include mappings for Nullable types because we explicitly check for null values @@ -341,9 +343,9 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana throw new ArgumentNullException(nameof(navigationManager)); } - if (name is null) + if (string.IsNullOrEmpty(name)) { - throw new ArgumentNullException(nameof(name)); + throw new ArgumentException(EmptyQueryParameterExceptionMessage, nameof(name)); } var uri = navigationManager.Uri; @@ -371,9 +373,9 @@ public static string UriWithQueryParameter(this NavigationManager naviga throw new ArgumentNullException(nameof(navigationManager)); } - if (name is null) + if (string.IsNullOrEmpty(name)) { - throw new ArgumentNullException(nameof(name)); + throw new ArgumentException(EmptyQueryParameterExceptionMessage, nameof(name)); } var uri = navigationManager.Uri; @@ -399,7 +401,11 @@ public static string UriWithQueryParameter(this NavigationManager naviga continue; } - hasNextValue = AppendNextValue(pair.EncodedName.Span, valueEnumerator, formatter, ref newQueryStringBuilder); + hasNextValue = AppendNextValue( + pair.EncodedName.Span, + valueEnumerator, + formatter, + ref newQueryStringBuilder); } else { @@ -409,7 +415,11 @@ public static string UriWithQueryParameter(this NavigationManager naviga while (hasNextValue) { - hasNextValue = AppendNextValue(encodedName, valueEnumerator, formatter, ref newQueryStringBuilder); + hasNextValue = AppendNextValue( + encodedName, + valueEnumerator, + formatter, + ref newQueryStringBuilder); } return newQueryStringBuilder.UriWithQueryString; @@ -540,6 +550,11 @@ public static string UriWithQueryParameters( QueryParameterNameComparer.Instance); foreach (var (name, value) in parameters) { + if (string.IsNullOrEmpty(name)) + { + throw new InvalidOperationException(EmptyQueryParameterExceptionMessage); + } + var encodedName = Uri.EscapeDataString(name).AsMemory(); var encodedValue = GetEncodedParameterValue(value); @@ -611,6 +626,11 @@ private static string UriWithAppendedQueryParameters( // Build a new query from the existing URI, appending all parameters with non-null values. foreach (var (name, value) in parameters) { + if (string.IsNullOrEmpty(name)) + { + throw new InvalidOperationException(EmptyQueryParameterExceptionMessage); + } + var encodedName = Uri.EscapeDataString(name); var encodedValue = GetEncodedParameterValue(value); diff --git a/src/Components/Components/test/NavigationManagerTest.cs b/src/Components/Components/test/NavigationManagerTest.cs index e2a3805fd5ae..88d08fcd0708 100644 --- a/src/Components/Components/test/NavigationManagerTest.cs +++ b/src/Components/Components/test/NavigationManagerTest.cs @@ -122,6 +122,7 @@ public void UriWithQueryParameter_AppendsWhenParameterDoesNotExist(string baseUr [InlineData("scheme://host/?name=Sally%Smith&age=42&name=Emily", "scheme://host/?age=42")] [InlineData("scheme://host/?name=&age=42", "scheme://host/?age=42")] [InlineData("scheme://host/?name=", "scheme://host/")] + [InlineData("scheme://host/", "scheme://host/")] public void UriWithQueryParameter_RemovesWhenParameterValueIsNull(string baseUri, string expectedUri) { var navigationManager = new TestNavigationManager(baseUri); @@ -130,6 +131,18 @@ public void UriWithQueryParameter_RemovesWhenParameterValueIsNull(string baseUri Assert.Equal(expectedUri, actualUri); } + [Theory] + [InlineData("")] + [InlineData((string)null)] + public void UriWithQueryParameter_ThrowsWhenNameIsNullOrEmpty(string name) + { + var baseUri = "scheme://host/"; + var navigationManager = new TestNavigationManager(baseUri); + + var exception = Assert.Throws("name", () => navigationManager.UriWithQueryParameter(name, "test")); + Assert.StartsWith("Cannot have empty query parameter names.", exception.Message); + } + [Theory] [InlineData("scheme://host/?search=rugs&filter=price%3Ahigh", "scheme://host/?search=rugs&filter=price%3Alow&filter=shipping%3Afree&filter=category%3Arug")] [InlineData("scheme://host/?filter=price%3Ahigh&search=rugs&filter=shipping%3A2day", "scheme://host/?filter=price%3Alow&search=rugs&filter=shipping%3Afree&filter=category%3Arug")] @@ -164,8 +177,33 @@ public void UriWithQueryParameterOfTValue_SkipsNullValues(string baseUri, string Assert.Equal(expectedUri, actualUri); } + [Fact] + public void UriWithQueryParameterOfTValue_ThrowsWhenParameterValueTypeIsUnsupported() + { + var baseUri = "scheme://host/"; + var navigationManager = new TestNavigationManager(baseUri); + var unsupportedParameterValues = new[] { new { Value = 3 } }; + + var exception = Assert.Throws(() => navigationManager.UriWithQueryParameter("value", unsupportedParameterValues)); + Assert.StartsWith("Cannot format query parameters with values of type", exception.Message); + } + + [Theory] + [InlineData("")] + [InlineData((string)null)] + public void UriWithQueryParameterOfTValue_ThrowsWhenNameIsNullOrEmpty(string name) + { + var baseUri = "scheme://host/"; + var navigationManager = new TestNavigationManager(baseUri); + var values = new string[] { "test" }; + + var exception = Assert.Throws("name", () => navigationManager.UriWithQueryParameter(name, values)); + Assert.StartsWith("Cannot have empty query parameter names.", exception.Message); + } + [Theory] [InlineData("scheme://host/?name=Bob%20Joe&age=42", "scheme://host/?age=25&eye-color=green")] + [InlineData("scheme://host/?name=Bob%20Joe&age=42&keepme=true", "scheme://host/?age=25&keepme=true&eye-color=green")] [InlineData("scheme://host/?age=42&eye-color=87", "scheme://host/?age=25&eye-color=green")] [InlineData("scheme://host/?", "scheme://host/?age=25&eye-color=green")] [InlineData("scheme://host/", "scheme://host/?age=25&eye-color=green")] @@ -182,6 +220,36 @@ public void UriWithQueryParameters_CanAddUpdateAndRemove(string baseUri, string Assert.Equal(expectedUri, actualUri); } + [Fact] + public void UriWithQueryParameters_ThrowsWhenParameterValueTypeIsUnsupported() + { + var baseUri = "scheme://host/"; + var navigationManager = new TestNavigationManager(baseUri); + var unsupportedParameterValues = new Dictionary + { + ["value"] = new { Value = 3 } + }; + + var exception = Assert.Throws(() => navigationManager.UriWithQueryParameters(unsupportedParameterValues)); + Assert.StartsWith("Cannot format query parameters with values of type", exception.Message); + } + + [Theory] + [InlineData("scheme://host/")] + [InlineData("scheme://host/?existing-param=test")] + public void UriWithQueryParameters_ThrowsWhenAnyParameterNameIsEmpty(string baseUri) + { + var navigationManager = new TestNavigationManager(baseUri); + var values = new Dictionary + { + ["name1"] = "value1", + [string.Empty] = "value2", + }; + + var exception = Assert.Throws(() => navigationManager.UriWithQueryParameters(values)); + Assert.StartsWith("Cannot have empty query parameter names.", exception.Message); + } + private class TestNavigationManager : NavigationManager { public TestNavigationManager() From 80a8242c359dcee6fb6c680e2e7fa2556fc077e4 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Thu, 29 Jul 2021 11:23:09 -0700 Subject: [PATCH 09/14] PR feedback. --- .../src/NavigationManagerExtensions.cs | 402 ++++++++++++++---- .../src/Routing/QueryParameterNameComparer.cs | 2 +- .../Components/test/NavigationManagerTest.cs | 16 +- 3 files changed, 315 insertions(+), 105 deletions(-) diff --git a/src/Components/Components/src/NavigationManagerExtensions.cs b/src/Components/Components/src/NavigationManagerExtensions.cs index 37376fee187f..2bbbc6979015 100644 --- a/src/Components/Components/src/NavigationManagerExtensions.cs +++ b/src/Components/Components/src/NavigationManagerExtensions.cs @@ -21,6 +21,7 @@ public static class NavigationManagerExtensions private const string EmptyQueryParameterExceptionMessage = "Cannot have empty query parameter names."; private delegate string QueryParameterFormatter(object value); + 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 @@ -28,7 +29,7 @@ public static class NavigationManagerExtensions // types before performing lookups in this dictionary. private static readonly Dictionary _queryParameterFormatters = new() { - [typeof(string)] = value => (string)value, + [typeof(string)] = value => Format((string)value)!, [typeof(bool)] = value => Format((bool)value), [typeof(DateTime)] = value => Format((DateTime)value), [typeof(decimal)] = value => Format((decimal)value), @@ -39,6 +40,9 @@ public static class NavigationManagerExtensions [typeof(long)] = value => Format((long)value), }; + private static string? Format(string? value) + => value; + private static string Format(bool value) => value.ToString(CultureInfo.InvariantCulture); @@ -95,9 +99,9 @@ private struct QueryStringBuilder public string UriWithQueryString => _builder.ToString(); - public QueryStringBuilder(ReadOnlySpan uriWithoutQueryString) + public QueryStringBuilder(ReadOnlySpan uriWithoutQueryString, int additionalCapacity = 0) { - _builder = new(); + _builder = new(uriWithoutQueryString.Length + additionalCapacity); _builder.Append(uriWithoutQueryString); _hasNewParameters = false; @@ -121,34 +125,27 @@ public void AppendParameter(ReadOnlySpan encodedName, ReadOnlySpan e } } - private class ParameterData + private readonly record struct ParameterData(string? EncodedName, string? EncodedValue) { - public string? EncodedValue { get; private set; } - public bool DidReplace { get; set; } - - public ParameterData(string? encodedValue) - { - EncodedValue = encodedValue; - DidReplace = false; - } + public bool DidReplace { get; init; } = false; } /// - /// Returns a equal to except with a single parameter + /// Returns a URI that is constructed by updating with a single parameter /// added or updated. /// /// The . - /// The name of the paramter to add or update. + /// The name of the parameter to add or update. /// The value of the parameter to add or update. public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, bool value) => UriWithQueryParameter(navigationManager, name, Format(value)); /// - /// Returns a equal to except with a single parameter + /// Returns a URI that is constructed by updating with a single parameter /// added, updated, or removed. /// /// The . - /// The name of the paramter to add or update. + /// The name of the parameter to add or update. /// The value of the parameter to add or update. /// /// If is null, the parameter will be removed if it exists in the URI. @@ -158,21 +155,21 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana => UriWithQueryParameter(navigationManager, name, Format(value)); /// - /// Returns a equal to except with a single parameter + /// Returns a URI that is constructed by updating with a single parameter /// added or updated. /// /// The . - /// The name of the paramter to add or update. + /// The name of the parameter to add or update. /// The value of the parameter to add or update. public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, DateTime value) => UriWithQueryParameter(navigationManager, name, Format(value)); /// - /// Returns a equal to except with a single parameter + /// Returns a URI that is constructed by updating with a single parameter /// added, updated, or removed. /// /// The . - /// The name of the paramter to add or update. + /// The name of the parameter to add or update. /// The value of the parameter to add or update. /// /// If is null, the parameter will be removed if it exists in the URI. @@ -182,21 +179,21 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana => UriWithQueryParameter(navigationManager, name, Format(value)); /// - /// Returns a equal to except with a single parameter + /// Returns a URI that is constructed by updating with a single parameter /// added or updated. /// /// The . - /// The name of the paramter to add or update. + /// The name of the parameter to add or update. /// The value of the parameter to add or update. public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, decimal value) => UriWithQueryParameter(navigationManager, name, Format(value)); /// - /// Returns a equal to except with a single parameter + /// Returns a URI that is constructed by updating with a single parameter /// added, updated, or removed. /// /// The . - /// The name of the paramter to add or update. + /// The name of the parameter to add or update. /// The value of the parameter to add or update. /// /// If is null, the parameter will be removed if it exists in the URI. @@ -206,21 +203,21 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana => UriWithQueryParameter(navigationManager, name, Format(value)); /// - /// Returns a equal to except with a single parameter + /// Returns a URI that is constructed by updating with a single parameter /// added or updated. /// /// The . - /// The name of the paramter to add or update. + /// The name of the parameter to add or update. /// The value of the parameter to add or update. public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, double value) => UriWithQueryParameter(navigationManager, name, Format(value)); /// - /// Returns a equal to except with a single parameter + /// Returns a URI that is constructed by updating with a single parameter /// added, updated, or removed. /// /// The . - /// The name of the paramter to add or update. + /// The name of the parameter to add or update. /// The value of the parameter to add or update. /// /// If is null, the parameter will be removed if it exists in the URI. @@ -230,21 +227,21 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana => UriWithQueryParameter(navigationManager, name, Format(value)); /// - /// Returns a equal to except with a single parameter + /// Returns a URI that is constructed by updating with a single parameter /// added or updated. /// /// The . - /// The name of the paramter to add or update. + /// The name of the parameter to add or update. /// The value of the parameter to add or update. public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, float value) => UriWithQueryParameter(navigationManager, name, Format(value)); /// - /// Returns a equal to except with a single parameter + /// Returns a URI that is constructed by updating with a single parameter /// added, updated, or removed. /// /// The . - /// The name of the paramter to add or update. + /// The name of the parameter to add or update. /// The value of the parameter to add or update. /// /// If is null, the parameter will be removed if it exists in the URI. @@ -254,21 +251,21 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana => UriWithQueryParameter(navigationManager, name, Format(value)); /// - /// Returns a equal to except with a single parameter + /// Returns a URI that is constructed by updating with a single parameter /// added or updated. /// /// The . - /// The name of the paramter to add or update. + /// The name of the parameter to add or update. /// The value of the parameter to add or update. public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, Guid value) => UriWithQueryParameter(navigationManager, name, Format(value)); /// - /// Returns a equal to except with a single parameter + /// Returns a URI that is constructed by updating with a single parameter /// added, updated, or removed. /// /// The . - /// The name of the paramter to add or update. + /// The name of the parameter to add or update. /// The value of the parameter to add or update. /// /// If is null, the parameter will be removed if it exists in the URI. @@ -278,21 +275,21 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana => UriWithQueryParameter(navigationManager, name, Format(value)); /// - /// Returns a equal to except with a single parameter + /// Returns a URI that is constructed by updating with a single parameter /// added or updated. /// /// The . - /// The name of the paramter to add or update. + /// The name of the parameter to add or update. /// The value of the parameter to add or update. public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, int value) => UriWithQueryParameter(navigationManager, name, Format(value)); /// - /// Returns a equal to except with a single parameter + /// Returns a URI that is constructed by updating with a single parameter /// added, updated, or removed. /// /// The . - /// The name of the paramter to add or update. + /// The name of the parameter to add or update. /// The value of the parameter to add or update. /// /// If is null, the parameter will be removed if it exists in the URI. @@ -302,21 +299,21 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana => UriWithQueryParameter(navigationManager, name, Format(value)); /// - /// Returns a equal to except with a single parameter + /// Returns a URI that is constructed by updating with a single parameter /// added or updated. /// /// The . - /// The name of the paramter to add or update. + /// The name of the parameter to add or update. /// The value of the parameter to add or update. public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, long value) => UriWithQueryParameter(navigationManager, name, Format(value)); /// - /// Returns a equal to except with a single parameter + /// Returns a URI that is constructed by updating with a single parameter /// added, updated, or removed. /// /// The . - /// The name of the paramter to add or update. + /// The name of the parameter to add or update. /// The value of the parameter to add or update. /// /// If is null, the parameter will be removed if it exists in the URI. @@ -326,11 +323,249 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana => UriWithQueryParameter(navigationManager, name, Format(value)); /// - /// Returns a equal to except with a single parameter + /// Returns a URI that is constructed by updating with a single parameter + /// updated with the provided . + /// + /// The . + /// The name of the parameter to add or update. + /// The parameter values to add or update. + /// + /// Any null entries in will be skipped. Existing querystring parameters not in + /// will be removed from the querystring in the returned URI. + /// + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) + => UriWithQueryParameter(navigationManager, name, values, Format); + + /// + /// Returns a URI that is constructed by updating with a single parameter + /// updated with the provided . + /// + /// The . + /// The name of the parameter to add or update. + /// The parameter values to add or update. + /// + /// Any null entries in will be skipped. Existing querystring parameters not in + /// will be removed from the querystring in the returned URI. + /// + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) + => UriWithQueryParameter(navigationManager, name, values, Format); + + /// + /// Returns a URI that is constructed by updating with a single parameter + /// updated with the provided . + /// + /// The . + /// The name of the parameter to add or update. + /// The parameter values to add or update. + /// + /// Any null entries in will be skipped. Existing querystring parameters not in + /// will be removed from the querystring in the returned URI. + /// + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) + => UriWithQueryParameter(navigationManager, name, values, Format); + + /// + /// Returns a URI that is constructed by updating with a single parameter + /// updated with the provided . + /// + /// The . + /// The name of the parameter to add or update. + /// The parameter values to add or update. + /// + /// Any null entries in will be skipped. Existing querystring parameters not in + /// will be removed from the querystring in the returned URI. + /// + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) + => UriWithQueryParameter(navigationManager, name, values, Format); + + /// + /// Returns a URI that is constructed by updating with a single parameter + /// updated with the provided . + /// + /// The . + /// The name of the parameter to add or update. + /// The parameter values to add or update. + /// + /// Any null entries in will be skipped. Existing querystring parameters not in + /// will be removed from the querystring in the returned URI. + /// + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) + => UriWithQueryParameter(navigationManager, name, values, Format); + + /// + /// Returns a URI that is constructed by updating with a single parameter + /// updated with the provided . + /// + /// The . + /// The name of the parameter to add or update. + /// The parameter values to add or update. + /// + /// Any null entries in will be skipped. Existing querystring parameters not in + /// will be removed from the querystring in the returned URI. + /// + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) + => UriWithQueryParameter(navigationManager, name, values, Format); + + /// + /// Returns a URI that is constructed by updating with a single parameter + /// updated with the provided . + /// + /// The . + /// The name of the parameter to add or update. + /// The parameter values to add or update. + /// + /// Any null entries in will be skipped. Existing querystring parameters not in + /// will be removed from the querystring in the returned URI. + /// + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) + => UriWithQueryParameter(navigationManager, name, values, Format); + + /// + /// Returns a URI that is constructed by updating with a single parameter + /// updated with the provided . + /// + /// The . + /// The name of the parameter to add or update. + /// The parameter values to add or update. + /// + /// Any null entries in will be skipped. Existing querystring parameters not in + /// will be removed from the querystring in the returned URI. + /// + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) + => UriWithQueryParameter(navigationManager, name, values, Format); + + /// + /// Returns a URI that is constructed by updating with a single parameter + /// updated with the provided . + /// + /// The . + /// The name of the parameter to add or update. + /// The parameter values to add or update. + /// + /// Any null entries in will be skipped. Existing querystring parameters not in + /// will be removed from the querystring in the returned URI. + /// + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) + => UriWithQueryParameter(navigationManager, name, values, Format); + + /// + /// Returns a URI that is constructed by updating with a single parameter + /// updated with the provided . + /// + /// The . + /// The name of the parameter to add or update. + /// The parameter values to add or update. + /// + /// Any null entries in will be skipped. Existing querystring parameters not in + /// will be removed from the querystring in the returned URI. + /// + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) + => UriWithQueryParameter(navigationManager, name, values, Format); + + /// + /// Returns a URI that is constructed by updating with a single parameter + /// updated with the provided . + /// + /// The . + /// The name of the parameter to add or update. + /// The parameter values to add or update. + /// + /// Any null entries in will be skipped. Existing querystring parameters not in + /// will be removed from the querystring in the returned URI. + /// + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) + => UriWithQueryParameter(navigationManager, name, values, Format); + + /// + /// Returns a URI that is constructed by updating with a single parameter + /// updated with the provided . + /// + /// The . + /// The name of the parameter to add or update. + /// The parameter values to add or update. + /// + /// Any null entries in will be skipped. Existing querystring parameters not in + /// will be removed from the querystring in the returned URI. + /// + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) + => UriWithQueryParameter(navigationManager, name, values, Format); + + /// + /// Returns a URI that is constructed by updating with a single parameter + /// updated with the provided . + /// + /// The . + /// The name of the parameter to add or update. + /// The parameter values to add or update. + /// + /// Any null entries in will be skipped. Existing querystring parameters not in + /// will be removed from the querystring in the returned URI. + /// + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) + => UriWithQueryParameter(navigationManager, name, values, Format); + + /// + /// Returns a URI that is constructed by updating with a single parameter + /// updated with the provided . + /// + /// The . + /// The name of the parameter to add or update. + /// The parameter values to add or update. + /// + /// Any null entries in will be skipped. Existing querystring parameters not in + /// will be removed from the querystring in the returned URI. + /// + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) + => UriWithQueryParameter(navigationManager, name, values, Format); + + /// + /// Returns a URI that is constructed by updating with a single parameter + /// updated with the provided . + /// + /// The . + /// The name of the parameter to add or update. + /// The parameter values to add or update. + /// + /// Any null entries in will be skipped. Existing querystring parameters not in + /// will be removed from the querystring in the returned URI. + /// + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) + => UriWithQueryParameter(navigationManager, name, values, Format); + + /// + /// Returns a URI that is constructed by updating with a single parameter + /// updated with the provided . + /// + /// The . + /// The name of the parameter to add or update. + /// The parameter values to add or update. + /// + /// Any null entries in will be skipped. Existing querystring parameters not in + /// will be removed from the querystring in the returned URI. + /// + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) + => UriWithQueryParameter(navigationManager, name, values, Format); + + /// + /// Returns a URI that is constructed by updating with a single parameter + /// updated with the provided . + /// + /// The . + /// The name of the parameter to add or update. + /// The parameter values to add or update. + /// + /// Any null entries in will be skipped. Existing querystring parameters not in + /// will be removed from the querystring in the returned URI. + /// + public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) + => UriWithQueryParameter(navigationManager, name, values, Format); + + /// + /// Returns a URI that is constructed by updating with a single parameter /// added, updated, or removed. /// /// The . - /// The name of the paramter to add or update. + /// The name of the parameter to add or update. /// The value of the parameter to add or update. /// /// If is null, the parameter will be removed if it exists in the URI. @@ -355,18 +590,11 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana : UriWithQueryParameterCore(uri, name, value); } - /// - /// Returns a equal to except with a single parameter - /// updated with the provided . - /// - /// The . - /// The name of the paramter to add or update. - /// The value of the parameter to add or update. - /// - /// Any null entries in will be skipped. Existing querystring parameters not in - /// will be removed from the querystring in the returned URI. - /// - public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) + private static string UriWithQueryParameter( + this NavigationManager navigationManager, + string name, + IEnumerable values, + QueryParameterFormatter formatter) { if (navigationManager is null) { @@ -385,14 +613,13 @@ public static string UriWithQueryParameter(this NavigationManager naviga return UriWithAppendedQueryParameters(uri, name, values); } - var formatter = GetFormatterFromParameterValueType(typeof(TValue)); var encodedName = Uri.EscapeDataString(name).AsSpan(); var valueEnumerator = values.GetEnumerator(); var hasNextValue = valueEnumerator.MoveNext(); foreach (var pair in existingQueryStringEnumerable) { - if (pair.EncodedName.Span.SequenceEqual(encodedName)) + if (pair.EncodedName.Span.Equals(encodedName, StringComparison.OrdinalIgnoreCase)) { if (!hasNextValue) { @@ -401,11 +628,8 @@ public static string UriWithQueryParameter(this NavigationManager naviga continue; } - hasNextValue = AppendNextValue( - pair.EncodedName.Span, - valueEnumerator, - formatter, - ref newQueryStringBuilder); + AppendNextValue(encodedName, valueEnumerator.Current, formatter, ref newQueryStringBuilder); + hasNextValue = valueEnumerator.MoveNext(); } else { @@ -415,36 +639,28 @@ public static string UriWithQueryParameter(this NavigationManager naviga while (hasNextValue) { - hasNextValue = AppendNextValue( - encodedName, - valueEnumerator, - formatter, - ref newQueryStringBuilder); + AppendNextValue(encodedName, valueEnumerator.Current, formatter, ref newQueryStringBuilder); + hasNextValue = valueEnumerator.MoveNext(); } return newQueryStringBuilder.UriWithQueryString; - static bool AppendNextValue( + static void AppendNextValue( ReadOnlySpan name, - IEnumerator valueEnumerator, - QueryParameterFormatter formatter, + TValue? value, + QueryParameterFormatter formatter, ref QueryStringBuilder queryStringBuilder) { - var currentValue = valueEnumerator.Current; - var hasNext = valueEnumerator.MoveNext(); - - if (currentValue is null) + if (value is null) { // If we encounter a null value, we exclude it from the querystring, so we skip whatever // the old value was. - return hasNext; + return; } - var formattedValue = formatter(currentValue); - var encodedValue = Uri.EscapeDataString(formattedValue); + var formattedValue = formatter(value); + var encodedValue = Uri.EscapeDataString(formattedValue!); queryStringBuilder.AppendParameter(name, encodedValue); - - return hasNext; } } @@ -462,10 +678,10 @@ private static string UriWithQueryParameterCore(string uri, string name, string var didReplace = false; foreach (var pair in existingQueryStringEnumerable) { - if (pair.EncodedName.Span.SequenceEqual(encodedName)) + if (pair.EncodedName.Span.Equals(encodedName, StringComparison.OrdinalIgnoreCase)) { didReplace = true; - newQueryStringBuilder.AppendParameter(pair.EncodedName.Span, encodedValue); + newQueryStringBuilder.AppendParameter(encodedName, encodedValue); } else { @@ -495,7 +711,7 @@ private static string UriWithoutQueryParameter(string uri, string name) // Rebuild the query omitting parameters with a matching name. foreach (var pair in existingQueryStringEnumerable) { - if (!pair.EncodedName.Span.SequenceEqual(encodedName)) + if (!pair.EncodedName.Span.Equals(encodedName, StringComparison.OrdinalIgnoreCase)) { newQueryStringBuilder.AppendParameter(pair.EncodedName.Span, pair.EncodedValue.Span); } @@ -505,7 +721,7 @@ private static string UriWithoutQueryParameter(string uri, string name) } /// - /// Returns a equal to except with multiple parameters + /// Returns a URI constructed from with multiple parameters /// added, updated, or removed. /// /// The . @@ -516,7 +732,7 @@ public static string UriWithQueryParameters( => UriWithQueryParameters(navigationManager, navigationManager.Uri, parameters); /// - /// Returns a equal to except with multiple parameters + /// Returns a URI constructed from except with multiple parameters /// added, updated, or removed. /// /// The . @@ -555,10 +771,10 @@ public static string UriWithQueryParameters( throw new InvalidOperationException(EmptyQueryParameterExceptionMessage); } - var encodedName = Uri.EscapeDataString(name).AsMemory(); + var encodedName = Uri.EscapeDataString(name); var encodedValue = GetEncodedParameterValue(value); - parameterDataByEncodedName.Add(encodedName, new ParameterData(encodedValue)); + parameterDataByEncodedName.Add(encodedName.AsMemory(), new ParameterData(encodedName, encodedValue)); } // Rebuild the query, updating or removing parameters. @@ -566,12 +782,12 @@ public static string UriWithQueryParameters( { if (parameterDataByEncodedName.TryGetValue(pair.EncodedName, out var parameterData)) { - parameterData.DidReplace = true; - if (parameterData.EncodedValue is not null) { - newQueryStringBuilder.AppendParameter(pair.EncodedName.Span, parameterData.EncodedValue); + newQueryStringBuilder.AppendParameter(parameterData.EncodedName, parameterData.EncodedValue); } + + parameterDataByEncodedName[pair.EncodedName] = parameterData with { DidReplace = true }; } else { @@ -686,7 +902,7 @@ private static bool TryRebuildExistingQueryFromUri( existingQueryStringEnumerable = new(query); var uriWithoutQueryString = uri.AsSpan(0, queryStartIndex); - newQueryStringBuilder = new(uriWithoutQueryString); + newQueryStringBuilder = new(uriWithoutQueryString, query.Length); return true; } diff --git a/src/Components/Components/src/Routing/QueryParameterNameComparer.cs b/src/Components/Components/src/Routing/QueryParameterNameComparer.cs index 65e5771a26f4..ccb6b5bb0076 100644 --- a/src/Components/Components/src/Routing/QueryParameterNameComparer.cs +++ b/src/Components/Components/src/Routing/QueryParameterNameComparer.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Components.Routing { - internal class QueryParameterNameComparer : IComparer>, IEqualityComparer> + internal sealed class QueryParameterNameComparer : IComparer>, IEqualityComparer> { public static readonly QueryParameterNameComparer Instance = new(); diff --git a/src/Components/Components/test/NavigationManagerTest.cs b/src/Components/Components/test/NavigationManagerTest.cs index 88d08fcd0708..1d9854d2748a 100644 --- a/src/Components/Components/test/NavigationManagerTest.cs +++ b/src/Components/Components/test/NavigationManagerTest.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Globalization; using Xunit; namespace Microsoft.AspNetCore.Components @@ -94,6 +95,7 @@ public void ToBaseRelativePath_ThrowsForInvalidBaseRelativePaths(string baseUri, [Theory] [InlineData("scheme://host/?name=Bob%20Joe&age=42", "scheme://host/?name=John%20Doe&age=42")] + [InlineData("scheme://host/?NaMe=Bob%20Joe&AgE=42", "scheme://host/?name=John%20Doe&AgE=42")] [InlineData("scheme://host/?name=Sally%Smith&age=42&name=Emily", "scheme://host/?name=John%20Doe&age=42&name=John%20Doe")] [InlineData("scheme://host/?name=&age=42", "scheme://host/?name=John%20Doe&age=42")] [InlineData("scheme://host/?name=", "scheme://host/?name=John%20Doe")] @@ -120,6 +122,7 @@ public void UriWithQueryParameter_AppendsWhenParameterDoesNotExist(string baseUr [Theory] [InlineData("scheme://host/?name=Bob%20Joe&age=42", "scheme://host/?age=42")] [InlineData("scheme://host/?name=Sally%Smith&age=42&name=Emily", "scheme://host/?age=42")] + [InlineData("scheme://host/?name=Sally%Smith&age=42&NaMe=Emily", "scheme://host/?age=42")] [InlineData("scheme://host/?name=&age=42", "scheme://host/?age=42")] [InlineData("scheme://host/?name=", "scheme://host/")] [InlineData("scheme://host/", "scheme://host/")] @@ -147,6 +150,7 @@ public void UriWithQueryParameter_ThrowsWhenNameIsNullOrEmpty(string name) [InlineData("scheme://host/?search=rugs&filter=price%3Ahigh", "scheme://host/?search=rugs&filter=price%3Alow&filter=shipping%3Afree&filter=category%3Arug")] [InlineData("scheme://host/?filter=price%3Ahigh&search=rugs&filter=shipping%3A2day", "scheme://host/?filter=price%3Alow&search=rugs&filter=shipping%3Afree&filter=category%3Arug")] [InlineData("scheme://host/?filter=price&filter=shipping%3A2day&filter=category%3Arug&filter=availability%3Atoday", "scheme://host/?filter=price%3Alow&filter=shipping%3Afree&filter=category%3Arug")] + [InlineData("scheme://host/?filter=price&FiLtEr=shipping%3A2day&filter=category%3Arug&FiLtEr=availability%3Atoday", "scheme://host/?filter=price%3Alow&filter=shipping%3Afree&filter=category%3Arug")] [InlineData("scheme://host/", "scheme://host/?filter=price%3Alow&filter=shipping%3Afree&filter=category%3Arug")] public void UriWithQueryParameterOfTValue_ReplacesExistingQueryParameters(string baseUri, string expectedUri) { @@ -177,17 +181,6 @@ public void UriWithQueryParameterOfTValue_SkipsNullValues(string baseUri, string Assert.Equal(expectedUri, actualUri); } - [Fact] - public void UriWithQueryParameterOfTValue_ThrowsWhenParameterValueTypeIsUnsupported() - { - var baseUri = "scheme://host/"; - var navigationManager = new TestNavigationManager(baseUri); - var unsupportedParameterValues = new[] { new { Value = 3 } }; - - var exception = Assert.Throws(() => navigationManager.UriWithQueryParameter("value", unsupportedParameterValues)); - Assert.StartsWith("Cannot format query parameters with values of type", exception.Message); - } - [Theory] [InlineData("")] [InlineData((string)null)] @@ -203,6 +196,7 @@ public void UriWithQueryParameterOfTValue_ThrowsWhenNameIsNullOrEmpty(string nam [Theory] [InlineData("scheme://host/?name=Bob%20Joe&age=42", "scheme://host/?age=25&eye-color=green")] + [InlineData("scheme://host/?NaMe=Bob%20Joe&AgE=42", "scheme://host/?age=25&eye-color=green")] [InlineData("scheme://host/?name=Bob%20Joe&age=42&keepme=true", "scheme://host/?age=25&keepme=true&eye-color=green")] [InlineData("scheme://host/?age=42&eye-color=87", "scheme://host/?age=25&eye-color=green")] [InlineData("scheme://host/?", "scheme://host/?age=25&eye-color=green")] From 11c7f4c04c0e337b1345f8241ad1a165c2bfe881 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Thu, 29 Jul 2021 17:55:47 -0700 Subject: [PATCH 10/14] Added support for IEnumerable values in UriWithQueryParameters --- .../src/NavigationManagerExtensions.cs | 264 ++++++++++-------- .../Components/test/NavigationManagerTest.cs | 70 +++-- 2 files changed, 189 insertions(+), 145 deletions(-) diff --git a/src/Components/Components/src/NavigationManagerExtensions.cs b/src/Components/Components/src/NavigationManagerExtensions.cs index 2bbbc6979015..10ed8876ddab 100644 --- a/src/Components/Components/src/NavigationManagerExtensions.cs +++ b/src/Components/Components/src/NavigationManagerExtensions.cs @@ -1,13 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; +using System.Collections; using System.Globalization; -using System.Linq; using System.Text; -using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Routing; using Microsoft.AspNetCore.Internal; @@ -18,16 +14,15 @@ namespace Microsoft.AspNetCore.Components /// public static class NavigationManagerExtensions { - private const string EmptyQueryParameterExceptionMessage = "Cannot have empty query parameter names."; + private const string EmptyQueryParameterNameExceptionMessage = "Cannot have empty query parameter names."; - private delegate string QueryParameterFormatter(object value); - private delegate string? QueryParameterFormatter(TValue? value); + 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() + private static readonly Dictionary> _queryParameterFormatters = new() { [typeof(string)] = value => Format((string)value)!, [typeof(bool)] = value => Format((bool)value), @@ -91,6 +86,7 @@ private static string Format(long value) private static string? Format(long? value) => value?.ToString(CultureInfo.InvariantCulture); + // Used for constructing a new query string from a URI. private struct QueryStringBuilder { private readonly StringBuilder _builder; @@ -125,9 +121,122 @@ public void AppendParameter(ReadOnlySpan encodedName, ReadOnlySpan e } } - private readonly record struct ParameterData(string? EncodedName, string? EncodedValue) + // A utility for feeding a collection of parameter values into a QueryStringBuilder. + private struct QueryParameterSource { - public bool DidReplace { get; init; } = false; + private IEnumerator? _enumerator; + private 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 AppendNextParameter(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 QueryParameterSource _source; + private string? _encodedValue; + + public string EncodedName => _source.EncodedName; + + public QueryParameterSource(string name, object? value) + { + if (value is null) + { + _source = new(name); + _encodedValue = default; + return; + } + + var valueType = value.GetType(); + + if (valueType != typeof(string) && typeof(IEnumerable).IsAssignableFrom(valueType)) + { + // The provided value was of enumerable type, so we populate the underlying source. + var elementType = valueType.GetElementType()!; + var formatter = GetFormatterFromParameterValueType(elementType); + + // This cast is inevitable; the values have to be boxed anyway to be formatted. + var values = ((IEnumerable)value).Cast(); + + _source = new(name, values, formatter); + _encodedValue = default; + } + else + { + // The provided value was not of enumerable type, so we leave the underlying source + // empty and instead cache the encoded value to be appended later. + var formatter = GetFormatterFromParameterValueType(valueType); + var formattedValue = formatter(value); + _source = new(name); + _encodedValue = Uri.EscapeDataString(formattedValue!); + } + } + + public bool AppendNextParameter(ref QueryStringBuilder builder) + { + if (_source.AppendNextParameter(ref builder)) + { + // The underlying source of values had elements, so there is no more work to do here. + return true; + } + + // Either we've run out of elements to append or the given value was not of enumerable + // type in the first place. + + // If the value was not of enumerable type and has not been appended, append it + // and set it to null so we don't provide the value more than once. + if (_encodedValue is not null) + { + builder.AppendParameter(_source.EncodedName, _encodedValue); + _encodedValue = null; + return true; + } + + return false; + } } /// @@ -580,7 +689,7 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana if (string.IsNullOrEmpty(name)) { - throw new ArgumentException(EmptyQueryParameterExceptionMessage, nameof(name)); + throw new InvalidOperationException(EmptyQueryParameterNameExceptionMessage); } var uri = navigationManager.Uri; @@ -601,35 +710,19 @@ private static string UriWithQueryParameter( throw new ArgumentNullException(nameof(navigationManager)); } - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentException(EmptyQueryParameterExceptionMessage, nameof(name)); - } - var uri = navigationManager.Uri; + var source = new QueryParameterSource(name, values, formatter); if (!TryRebuildExistingQueryFromUri(uri, out var existingQueryStringEnumerable, out var newQueryStringBuilder)) { - return UriWithAppendedQueryParameters(uri, name, values); + return UriWithAppendedQueryParameters(uri, ref source); } - var encodedName = Uri.EscapeDataString(name).AsSpan(); - var valueEnumerator = values.GetEnumerator(); - var hasNextValue = valueEnumerator.MoveNext(); - foreach (var pair in existingQueryStringEnumerable) { - if (pair.EncodedName.Span.Equals(encodedName, StringComparison.OrdinalIgnoreCase)) + if (pair.EncodedName.Span.Equals(source.EncodedName, StringComparison.OrdinalIgnoreCase)) { - if (!hasNextValue) - { - // We've added all provided values, so we'll skip any parameters with the provided name from - // now on. - continue; - } - - AppendNextValue(encodedName, valueEnumerator.Current, formatter, ref newQueryStringBuilder); - hasNextValue = valueEnumerator.MoveNext(); + source.AppendNextParameter(ref newQueryStringBuilder); } else { @@ -637,31 +730,9 @@ private static string UriWithQueryParameter( } } - while (hasNextValue) - { - AppendNextValue(encodedName, valueEnumerator.Current, formatter, ref newQueryStringBuilder); - hasNextValue = valueEnumerator.MoveNext(); - } + while (source.AppendNextParameter(ref newQueryStringBuilder)) ; return newQueryStringBuilder.UriWithQueryString; - - static void AppendNextValue( - ReadOnlySpan name, - TValue? value, - QueryParameterFormatter formatter, - ref QueryStringBuilder queryStringBuilder) - { - if (value is null) - { - // If we encounter a null value, we exclude it from the querystring, so we skip whatever - // the old value was. - return; - } - - var formattedValue = formatter(value); - var encodedValue = Uri.EscapeDataString(formattedValue!); - queryStringBuilder.AppendParameter(name, encodedValue); - } } private static string UriWithQueryParameterCore(string uri, string name, string value) @@ -760,34 +831,18 @@ public static string UriWithQueryParameters( return UriWithAppendedQueryParameters(uri, parameters); } - // Build a dictionary mapping encoded parameter names to an object containing their encoded values - // and whether they've replaced an existing parameter. - var parameterDataByEncodedName = new Dictionary, ParameterData>( - QueryParameterNameComparer.Instance); - foreach (var (name, value) in parameters) - { - if (string.IsNullOrEmpty(name)) - { - throw new InvalidOperationException(EmptyQueryParameterExceptionMessage); - } - - var encodedName = Uri.EscapeDataString(name); - var encodedValue = GetEncodedParameterValue(value); - - parameterDataByEncodedName.Add(encodedName.AsMemory(), new ParameterData(encodedName, encodedValue)); - } + var parameterSourceCollection = CreateParameterSourceDictionary(parameters); // Rebuild the query, updating or removing parameters. foreach (var pair in existingQueryStringEnumerable) { - if (parameterDataByEncodedName.TryGetValue(pair.EncodedName, out var parameterData)) + if (parameterSourceCollection.TryGetValue(pair.EncodedName, out var source)) { - if (parameterData.EncodedValue is not null) + if (source.AppendNextParameter(ref newQueryStringBuilder)) { - newQueryStringBuilder.AppendParameter(parameterData.EncodedName, parameterData.EncodedValue); + // We need to add the parameter source back into the dictionary since we're working on a copy. + parameterSourceCollection[pair.EncodedName] = source; } - - parameterDataByEncodedName[pair.EncodedName] = parameterData with { DidReplace = true }; } else { @@ -796,12 +851,9 @@ public static string UriWithQueryParameters( } // Append any parameters with non-null values that did not replace existing parameters. - foreach (var (encodedName, data) in parameterDataByEncodedName) + foreach (var source in parameterSourceCollection.Values) { - if (!data.DidReplace && data.EncodedValue is not null) - { - newQueryStringBuilder.AppendParameter(encodedName.Span, data.EncodedValue); - } + while (source.AppendNextParameter(ref newQueryStringBuilder)) ; } return newQueryStringBuilder.UriWithQueryString; @@ -809,26 +861,11 @@ public static string UriWithQueryParameters( private static string UriWithAppendedQueryParameters( string uriWithoutQueryString, - string name, - IEnumerable values) + ref QueryParameterSource queryParameterSource) { - var formatter = GetFormatterFromParameterValueType(typeof(TValue)); - var encodedName = Uri.EscapeDataString(name); var builder = new QueryStringBuilder(uriWithoutQueryString); - // Build a new query from the existing URI, appending all values. - foreach (var value in values) - { - if (value is null) - { - continue; - } - - var formattedValue = formatter(value); - var encodedValue = Uri.EscapeDataString(formattedValue); - - builder.AppendParameter(encodedName, encodedValue); - } + while (queryParameterSource.AppendNextParameter(ref builder)) ; return builder.UriWithQueryString; } @@ -839,39 +876,30 @@ private static string UriWithAppendedQueryParameters( { var builder = new QueryStringBuilder(uriWithoutQueryString); - // Build a new query from the existing URI, appending all parameters with non-null values. foreach (var (name, value) in parameters) { - if (string.IsNullOrEmpty(name)) - { - throw new InvalidOperationException(EmptyQueryParameterExceptionMessage); - } - - var encodedName = Uri.EscapeDataString(name); - var encodedValue = GetEncodedParameterValue(value); - - if (encodedValue is not null) - { - builder.AppendParameter(encodedName, encodedValue); - } + var source = new QueryParameterSource(name, value); + while (source.AppendNextParameter(ref builder)) ; } return builder.UriWithQueryString; } - private static string? GetEncodedParameterValue(object? value) + private static Dictionary, QueryParameterSource> CreateParameterSourceDictionary( + IReadOnlyDictionary parameters) { - if (value is null) + var parameterSources = new Dictionary, QueryParameterSource>(QueryParameterNameComparer.Instance); + + foreach (var (name, value) in parameters) { - return null; + var parameterSource = new QueryParameterSource(name, value); + parameterSources.Add(parameterSource.EncodedName.AsMemory(), parameterSource); } - var formatter = GetFormatterFromParameterValueType(value.GetType()); - var formattedValue = formatter(value); - return formattedValue is null ? null : Uri.EscapeDataString(formattedValue); + return parameterSources; } - private static QueryParameterFormatter GetFormatterFromParameterValueType(Type parameterValueType) + private static QueryParameterFormatter GetFormatterFromParameterValueType(Type parameterValueType) { var underlyingParameterValueType = Nullable.GetUnderlyingType(parameterValueType) ?? parameterValueType; diff --git a/src/Components/Components/test/NavigationManagerTest.cs b/src/Components/Components/test/NavigationManagerTest.cs index 1d9854d2748a..f6e39a97e452 100644 --- a/src/Components/Components/test/NavigationManagerTest.cs +++ b/src/Components/Components/test/NavigationManagerTest.cs @@ -94,15 +94,15 @@ public void ToBaseRelativePath_ThrowsForInvalidBaseRelativePaths(string baseUri, } [Theory] - [InlineData("scheme://host/?name=Bob%20Joe&age=42", "scheme://host/?name=John%20Doe&age=42")] - [InlineData("scheme://host/?NaMe=Bob%20Joe&AgE=42", "scheme://host/?name=John%20Doe&AgE=42")] - [InlineData("scheme://host/?name=Sally%Smith&age=42&name=Emily", "scheme://host/?name=John%20Doe&age=42&name=John%20Doe")] - [InlineData("scheme://host/?name=&age=42", "scheme://host/?name=John%20Doe&age=42")] - [InlineData("scheme://host/?name=", "scheme://host/?name=John%20Doe")] + [InlineData("scheme://host/?full%20name=Bob%20Joe&age=42", "scheme://host/?full%20name=John%20Doe&age=42")] + [InlineData("scheme://host/?fUlL%20nAmE=Bob%20Joe&AgE=42", "scheme://host/?full%20name=John%20Doe&AgE=42")] + [InlineData("scheme://host/?full%20name=Sally%20Smith&age=42&full%20name=Emily", "scheme://host/?full%20name=John%20Doe&age=42&full%20name=John%20Doe")] + [InlineData("scheme://host/?full%20name=&age=42", "scheme://host/?full%20name=John%20Doe&age=42")] + [InlineData("scheme://host/?full%20name=", "scheme://host/?full%20name=John%20Doe")] public void UriWithQueryParameter_ReplacesWhenParameterExists(string baseUri, string expectedUri) { var navigationManager = new TestNavigationManager(baseUri); - var actualUri = navigationManager.UriWithQueryParameter("name", "John Doe"); + var actualUri = navigationManager.UriWithQueryParameter("full name", "John Doe"); Assert.Equal(expectedUri, actualUri); } @@ -120,16 +120,16 @@ public void UriWithQueryParameter_AppendsWhenParameterDoesNotExist(string baseUr } [Theory] - [InlineData("scheme://host/?name=Bob%20Joe&age=42", "scheme://host/?age=42")] - [InlineData("scheme://host/?name=Sally%Smith&age=42&name=Emily", "scheme://host/?age=42")] - [InlineData("scheme://host/?name=Sally%Smith&age=42&NaMe=Emily", "scheme://host/?age=42")] - [InlineData("scheme://host/?name=&age=42", "scheme://host/?age=42")] - [InlineData("scheme://host/?name=", "scheme://host/")] + [InlineData("scheme://host/?full%20name=Bob%20Joe&age=42", "scheme://host/?age=42")] + [InlineData("scheme://host/?full%20name=Sally%Smith&age=42&full%20name=Emily%20Karlsen", "scheme://host/?age=42")] + [InlineData("scheme://host/?full%20name=Sally%Smith&age=42&FuLl%20NaMe=Emily%20Karlsen", "scheme://host/?age=42")] + [InlineData("scheme://host/?full%20name=&age=42", "scheme://host/?age=42")] + [InlineData("scheme://host/?full%20name=", "scheme://host/")] [InlineData("scheme://host/", "scheme://host/")] public void UriWithQueryParameter_RemovesWhenParameterValueIsNull(string baseUri, string expectedUri) { var navigationManager = new TestNavigationManager(baseUri); - var actualUri = navigationManager.UriWithQueryParameter("name", (string)null); + var actualUri = navigationManager.UriWithQueryParameter("full name", (string)null); Assert.Equal(expectedUri, actualUri); } @@ -142,20 +142,20 @@ public void UriWithQueryParameter_ThrowsWhenNameIsNullOrEmpty(string name) var baseUri = "scheme://host/"; var navigationManager = new TestNavigationManager(baseUri); - var exception = Assert.Throws("name", () => navigationManager.UriWithQueryParameter(name, "test")); + var exception = Assert.Throws(() => navigationManager.UriWithQueryParameter(name, "test")); Assert.StartsWith("Cannot have empty query parameter names.", exception.Message); } [Theory] - [InlineData("scheme://host/?search=rugs&filter=price%3Ahigh", "scheme://host/?search=rugs&filter=price%3Alow&filter=shipping%3Afree&filter=category%3Arug")] - [InlineData("scheme://host/?filter=price%3Ahigh&search=rugs&filter=shipping%3A2day", "scheme://host/?filter=price%3Alow&search=rugs&filter=shipping%3Afree&filter=category%3Arug")] - [InlineData("scheme://host/?filter=price&filter=shipping%3A2day&filter=category%3Arug&filter=availability%3Atoday", "scheme://host/?filter=price%3Alow&filter=shipping%3Afree&filter=category%3Arug")] - [InlineData("scheme://host/?filter=price&FiLtEr=shipping%3A2day&filter=category%3Arug&FiLtEr=availability%3Atoday", "scheme://host/?filter=price%3Alow&filter=shipping%3Afree&filter=category%3Arug")] - [InlineData("scheme://host/", "scheme://host/?filter=price%3Alow&filter=shipping%3Afree&filter=category%3Arug")] + [InlineData("scheme://host/?search=rugs&item%20filter=price%3Ahigh", "scheme://host/?search=rugs&item%20filter=price%3Alow&item%20filter=shipping%3Afree&item%20filter=category%3Arug")] + [InlineData("scheme://host/?item%20filter=price%3Ahigh&search=rugs&item%20filter=shipping%3A2day", "scheme://host/?item%20filter=price%3Alow&search=rugs&item%20filter=shipping%3Afree&item%20filter=category%3Arug")] + [InlineData("scheme://host/?item%20filter=price&item%20filter=shipping%3A2day&item%20filter=category%3Arug&item%20filter=availability%3Atoday", "scheme://host/?item%20filter=price%3Alow&item%20filter=shipping%3Afree&item%20filter=category%3Arug")] + [InlineData("scheme://host/?item%20filter=price&iTeM%20fIlTeR=shipping%3A2day&item%20filter=category%3Arug&ItEm%20FiLtEr=availability%3Atoday", "scheme://host/?item%20filter=price%3Alow&item%20filter=shipping%3Afree&item%20filter=category%3Arug")] + [InlineData("scheme://host/", "scheme://host/?item%20filter=price%3Alow&item%20filter=shipping%3Afree&item%20filter=category%3Arug")] public void UriWithQueryParameterOfTValue_ReplacesExistingQueryParameters(string baseUri, string expectedUri) { var navigationManager = new TestNavigationManager(baseUri); - var actualUri = navigationManager.UriWithQueryParameter("filter", new string[] + var actualUri = navigationManager.UriWithQueryParameter("item filter", new string[] { "price:low", "shipping:free", @@ -190,17 +190,17 @@ public void UriWithQueryParameterOfTValue_ThrowsWhenNameIsNullOrEmpty(string nam var navigationManager = new TestNavigationManager(baseUri); var values = new string[] { "test" }; - var exception = Assert.Throws("name", () => navigationManager.UriWithQueryParameter(name, values)); + var exception = Assert.Throws(() => navigationManager.UriWithQueryParameter(name, values)); Assert.StartsWith("Cannot have empty query parameter names.", exception.Message); } [Theory] - [InlineData("scheme://host/?name=Bob%20Joe&age=42", "scheme://host/?age=25&eye-color=green")] - [InlineData("scheme://host/?NaMe=Bob%20Joe&AgE=42", "scheme://host/?age=25&eye-color=green")] - [InlineData("scheme://host/?name=Bob%20Joe&age=42&keepme=true", "scheme://host/?age=25&keepme=true&eye-color=green")] - [InlineData("scheme://host/?age=42&eye-color=87", "scheme://host/?age=25&eye-color=green")] - [InlineData("scheme://host/?", "scheme://host/?age=25&eye-color=green")] - [InlineData("scheme://host/", "scheme://host/?age=25&eye-color=green")] + [InlineData("scheme://host/?name=Bob%20Joe&age=42", "scheme://host/?age=25&eye%20color=green")] + [InlineData("scheme://host/?NaMe=Bob%20Joe&AgE=42", "scheme://host/?age=25&eye%20color=green")] + [InlineData("scheme://host/?name=Bob%20Joe&age=42&keepme=true", "scheme://host/?age=25&keepme=true&eye%20color=green")] + [InlineData("scheme://host/?age=42&eye%20color=87", "scheme://host/?age=25&eye%20color=green")] + [InlineData("scheme://host/?", "scheme://host/?age=25&eye%20color=green")] + [InlineData("scheme://host/", "scheme://host/?age=25&eye%20color=green")] public void UriWithQueryParameters_CanAddUpdateAndRemove(string baseUri, string expectedUri) { var navigationManager = new TestNavigationManager(baseUri); @@ -208,7 +208,23 @@ public void UriWithQueryParameters_CanAddUpdateAndRemove(string baseUri, string { ["name"] = null, // Remove ["age"] = (int?)25, // Add/update - ["eye-color"] = "green",// Add/update + ["eye color"] = "green",// Add/update + }); + + Assert.Equal(expectedUri, actualUri); + } + + [Theory] + [InlineData("scheme://host/?full%20name=Bob%20Joe&ping=8&ping=300", "scheme://host/?full%20name=John%20Doe&ping=35&ping=16&ping=87&ping=240")] + [InlineData("scheme://host/?ping=8&full%20name=Bob%20Joe&ping=300", "scheme://host/?ping=35&full%20name=John%20Doe&ping=16&ping=87&ping=240")] + [InlineData("scheme://host/?ping=8&ping=300&ping=50&ping=68&ping=42", "scheme://host/?ping=35&ping=16&ping=87&ping=240&full%20name=John%20Doe")] + public void UriWithQueryParameters_SupportsEnumerableValues(string baseUri, string expectedUri) + { + var navigationManager = new TestNavigationManager(baseUri); + var actualUri = navigationManager.UriWithQueryParameters(new Dictionary + { + ["full name"] = "John Doe", // Single value + ["ping"] = new int?[] { 35, 16, null, 87, 240 } }); Assert.Equal(expectedUri, actualUri); From f01f4c935f69c36b52ac42512d6c4de50e60ce04 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Thu, 29 Jul 2021 18:07:49 -0700 Subject: [PATCH 11/14] Update NavigationManagerExtensions.cs --- src/Components/Components/src/NavigationManagerExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Components/src/NavigationManagerExtensions.cs b/src/Components/Components/src/NavigationManagerExtensions.cs index 10ed8876ddab..46a32fbdf833 100644 --- a/src/Components/Components/src/NavigationManagerExtensions.cs +++ b/src/Components/Components/src/NavigationManagerExtensions.cs @@ -86,7 +86,7 @@ private static string Format(long value) private static string? Format(long? value) => value?.ToString(CultureInfo.InvariantCulture); - // Used for constructing a new query string from a URI. + // Used for constructing a URI with a new querystring from an existing URI. private struct QueryStringBuilder { private readonly StringBuilder _builder; From 3c0996f410ee0919a46e791996abf8c7058eed57 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Fri, 30 Jul 2021 09:15:37 -0700 Subject: [PATCH 12/14] Updated PublicAPI.Unshipped.txt Made QueryParameterSource readonly. --- .../src/NavigationManagerExtensions.cs | 8 ++-- .../Components/src/PublicAPI.Unshipped.txt | 37 +++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/Components/Components/src/NavigationManagerExtensions.cs b/src/Components/Components/src/NavigationManagerExtensions.cs index 46a32fbdf833..e7d0bb0bb6a9 100644 --- a/src/Components/Components/src/NavigationManagerExtensions.cs +++ b/src/Components/Components/src/NavigationManagerExtensions.cs @@ -122,10 +122,10 @@ public void AppendParameter(ReadOnlySpan encodedName, ReadOnlySpan e } // A utility for feeding a collection of parameter values into a QueryStringBuilder. - private struct QueryParameterSource + private readonly struct QueryParameterSource { - private IEnumerator? _enumerator; - private QueryParameterFormatter? _formatter; + private readonly IEnumerator? _enumerator; + private readonly QueryParameterFormatter? _formatter; public string EncodedName { get; } @@ -176,7 +176,7 @@ public bool AppendNextParameter(ref QueryStringBuilder builder) // a QueryStringBuilder. private struct QueryParameterSource { - private QueryParameterSource _source; + private readonly QueryParameterSource _source; private string? _encodedValue; public string EncodedName => _source.EncodedName; diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 9af925b903db..243a78ee7302 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -34,6 +34,7 @@ Microsoft.AspNetCore.Components.Lifetime.IComponentApplicationStateStore.Persist Microsoft.AspNetCore.Components.NavigationManager.NavigateTo(string! uri, Microsoft.AspNetCore.Components.NavigationOptions options) -> void Microsoft.AspNetCore.Components.NavigationManager.NavigateTo(string! uri, bool forceLoad = false, bool replace = false) -> void Microsoft.AspNetCore.Components.NavigationManager.NavigateTo(string! uri, bool forceLoad) -> void +Microsoft.AspNetCore.Components.NavigationManagerExtensions Microsoft.AspNetCore.Components.NavigationOptions Microsoft.AspNetCore.Components.NavigationOptions.ForceLoad.get -> bool Microsoft.AspNetCore.Components.NavigationOptions.ForceLoad.init -> void @@ -88,6 +89,42 @@ static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.Crea static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, System.Action! setter, System.TimeOnly existingValue, string! format, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, System.Action! setter, System.TimeOnly? existingValue, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback static Microsoft.AspNetCore.Components.EventCallbackFactoryBinderExtensions.CreateBinder(this Microsoft.AspNetCore.Components.EventCallbackFactory! factory, object! receiver, System.Action! setter, System.TimeOnly? existingValue, string! format, System.Globalization.CultureInfo? culture = null) -> Microsoft.AspNetCore.Components.EventCallback +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameter(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! name, System.Collections.Generic.IEnumerable! values) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameter(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! name, System.Collections.Generic.IEnumerable! values) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameter(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! name, System.Collections.Generic.IEnumerable! values) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameter(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! name, System.Collections.Generic.IEnumerable! values) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameter(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! name, System.Collections.Generic.IEnumerable! values) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameter(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! name, System.Collections.Generic.IEnumerable! values) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameter(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! name, System.Collections.Generic.IEnumerable! values) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameter(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! name, System.Collections.Generic.IEnumerable! values) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameter(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! name, System.Collections.Generic.IEnumerable! values) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameter(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! name, System.Collections.Generic.IEnumerable! values) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameter(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! name, System.Collections.Generic.IEnumerable! values) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameter(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! name, System.Collections.Generic.IEnumerable! values) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameter(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! name, System.Collections.Generic.IEnumerable! values) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameter(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! name, System.Collections.Generic.IEnumerable! values) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameter(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! name, System.Collections.Generic.IEnumerable! values) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameter(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! name, System.Collections.Generic.IEnumerable! values) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameter(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! name, System.Collections.Generic.IEnumerable! values) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameter(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! name, System.DateTime value) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameter(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! name, System.DateTime? value) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameter(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! name, System.Guid value) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameter(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! name, System.Guid? value) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameter(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! name, bool value) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameter(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! name, bool? value) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameter(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! name, decimal value) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameter(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! name, decimal? value) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameter(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! name, double value) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameter(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! name, double? value) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameter(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! name, float value) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameter(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! name, float? value) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameter(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! name, int value) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameter(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! name, int? value) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameter(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! name, long value) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameter(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! name, long? value) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameter(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! name, string? value) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameters(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, System.Collections.Generic.IReadOnlyDictionary! parameters) -> string! +static Microsoft.AspNetCore.Components.NavigationManagerExtensions.UriWithQueryParameters(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! uri, System.Collections.Generic.IReadOnlyDictionary! parameters) -> string! static Microsoft.AspNetCore.Components.ParameterView.FromDictionary(System.Collections.Generic.IDictionary! parameters) -> Microsoft.AspNetCore.Components.ParameterView virtual Microsoft.AspNetCore.Components.NavigationManager.NavigateToCore(string! uri, Microsoft.AspNetCore.Components.NavigationOptions options) -> void virtual Microsoft.AspNetCore.Components.NavigationManager.NavigateToCore(string! uri, bool forceLoad) -> void From eb3bb5e9c90c033ee2cd6db78f207a5d961ce7f5 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Mon, 2 Aug 2021 14:25:54 -0700 Subject: [PATCH 13/14] PR feedback. --- .../src/NavigationManagerExtensions.cs | 161 +++++++++--------- 1 file changed, 81 insertions(+), 80 deletions(-) diff --git a/src/Components/Components/src/NavigationManagerExtensions.cs b/src/Components/Components/src/NavigationManagerExtensions.cs index e7d0bb0bb6a9..7030e5530c3b 100644 --- a/src/Components/Components/src/NavigationManagerExtensions.cs +++ b/src/Components/Components/src/NavigationManagerExtensions.cs @@ -122,6 +122,7 @@ public void AppendParameter(ReadOnlySpan encodedName, ReadOnlySpan e } // 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; @@ -150,7 +151,7 @@ public QueryParameterSource(string name, IEnumerable values, QueryParam _formatter = formatter; } - public bool AppendNextParameter(ref QueryStringBuilder builder) + public bool TryAppendNextParameter(ref QueryStringBuilder builder) { if (_enumerator is null || !_enumerator.MoveNext()) { @@ -215,9 +216,9 @@ public QueryParameterSource(string name, object? value) } } - public bool AppendNextParameter(ref QueryStringBuilder builder) + public bool TryAppendNextParameter(ref QueryStringBuilder builder) { - if (_source.AppendNextParameter(ref builder)) + if (_source.TryAppendNextParameter(ref builder)) { // The underlying source of values had elements, so there is no more work to do here. return true; @@ -443,7 +444,7 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana /// will be removed from the querystring in the returned URI. /// public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) - => UriWithQueryParameter(navigationManager, name, values, Format); + => GetUriWithUpdatedQueryParameter(navigationManager, name, values, Format); /// /// Returns a URI that is constructed by updating with a single parameter @@ -457,7 +458,7 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana /// will be removed from the querystring in the returned URI. /// public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) - => UriWithQueryParameter(navigationManager, name, values, Format); + => GetUriWithUpdatedQueryParameter(navigationManager, name, values, Format); /// /// Returns a URI that is constructed by updating with a single parameter @@ -471,7 +472,7 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana /// will be removed from the querystring in the returned URI. /// public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) - => UriWithQueryParameter(navigationManager, name, values, Format); + => GetUriWithUpdatedQueryParameter(navigationManager, name, values, Format); /// /// Returns a URI that is constructed by updating with a single parameter @@ -485,7 +486,7 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana /// will be removed from the querystring in the returned URI. /// public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) - => UriWithQueryParameter(navigationManager, name, values, Format); + => GetUriWithUpdatedQueryParameter(navigationManager, name, values, Format); /// /// Returns a URI that is constructed by updating with a single parameter @@ -499,7 +500,7 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana /// will be removed from the querystring in the returned URI. /// public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) - => UriWithQueryParameter(navigationManager, name, values, Format); + => GetUriWithUpdatedQueryParameter(navigationManager, name, values, Format); /// /// Returns a URI that is constructed by updating with a single parameter @@ -513,7 +514,7 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana /// will be removed from the querystring in the returned URI. /// public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) - => UriWithQueryParameter(navigationManager, name, values, Format); + => GetUriWithUpdatedQueryParameter(navigationManager, name, values, Format); /// /// Returns a URI that is constructed by updating with a single parameter @@ -527,7 +528,7 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana /// will be removed from the querystring in the returned URI. /// public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) - => UriWithQueryParameter(navigationManager, name, values, Format); + => GetUriWithUpdatedQueryParameter(navigationManager, name, values, Format); /// /// Returns a URI that is constructed by updating with a single parameter @@ -541,7 +542,7 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana /// will be removed from the querystring in the returned URI. /// public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) - => UriWithQueryParameter(navigationManager, name, values, Format); + => GetUriWithUpdatedQueryParameter(navigationManager, name, values, Format); /// /// Returns a URI that is constructed by updating with a single parameter @@ -555,7 +556,7 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana /// will be removed from the querystring in the returned URI. /// public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) - => UriWithQueryParameter(navigationManager, name, values, Format); + => GetUriWithUpdatedQueryParameter(navigationManager, name, values, Format); /// /// Returns a URI that is constructed by updating with a single parameter @@ -569,7 +570,7 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana /// will be removed from the querystring in the returned URI. /// public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) - => UriWithQueryParameter(navigationManager, name, values, Format); + => GetUriWithUpdatedQueryParameter(navigationManager, name, values, Format); /// /// Returns a URI that is constructed by updating with a single parameter @@ -583,7 +584,7 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana /// will be removed from the querystring in the returned URI. /// public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) - => UriWithQueryParameter(navigationManager, name, values, Format); + => GetUriWithUpdatedQueryParameter(navigationManager, name, values, Format); /// /// Returns a URI that is constructed by updating with a single parameter @@ -597,7 +598,7 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana /// will be removed from the querystring in the returned URI. /// public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) - => UriWithQueryParameter(navigationManager, name, values, Format); + => GetUriWithUpdatedQueryParameter(navigationManager, name, values, Format); /// /// Returns a URI that is constructed by updating with a single parameter @@ -611,7 +612,7 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana /// will be removed from the querystring in the returned URI. /// public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) - => UriWithQueryParameter(navigationManager, name, values, Format); + => GetUriWithUpdatedQueryParameter(navigationManager, name, values, Format); /// /// Returns a URI that is constructed by updating with a single parameter @@ -625,7 +626,7 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana /// will be removed from the querystring in the returned URI. /// public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) - => UriWithQueryParameter(navigationManager, name, values, Format); + => GetUriWithUpdatedQueryParameter(navigationManager, name, values, Format); /// /// Returns a URI that is constructed by updating with a single parameter @@ -639,7 +640,7 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana /// will be removed from the querystring in the returned URI. /// public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) - => UriWithQueryParameter(navigationManager, name, values, Format); + => GetUriWithUpdatedQueryParameter(navigationManager, name, values, Format); /// /// Returns a URI that is constructed by updating with a single parameter @@ -653,7 +654,7 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana /// will be removed from the querystring in the returned URI. /// public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) - => UriWithQueryParameter(navigationManager, name, values, Format); + => GetUriWithUpdatedQueryParameter(navigationManager, name, values, Format); /// /// Returns a URI that is constructed by updating with a single parameter @@ -667,7 +668,7 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana /// will be removed from the querystring in the returned URI. /// public static string UriWithQueryParameter(this NavigationManager navigationManager, string name, IEnumerable values) - => UriWithQueryParameter(navigationManager, name, values, Format); + => GetUriWithUpdatedQueryParameter(navigationManager, name, values, Format); /// /// Returns a URI that is constructed by updating with a single parameter @@ -695,34 +696,62 @@ public static string UriWithQueryParameter(this NavigationManager navigationMana var uri = navigationManager.Uri; return value is null - ? UriWithoutQueryParameter(uri, name) - : UriWithQueryParameterCore(uri, name, value); + ? GetUriWithRemovedQueryParameter(uri, name) + : GetUriWithUpdatedQueryParameter(uri, name, value); } - private static string UriWithQueryParameter( + /// + /// Returns a URI constructed from with multiple parameters + /// added, updated, or removed. + /// + /// The . + /// The values to add, update, or remove. + public static string UriWithQueryParameters( this NavigationManager navigationManager, - string name, - IEnumerable values, - QueryParameterFormatter formatter) + IReadOnlyDictionary parameters) + => UriWithQueryParameters(navigationManager, navigationManager.Uri, parameters); + + /// + /// Returns a URI constructed from except with multiple parameters + /// added, updated, or removed. + /// + /// The . + /// The URI with the query to modify. + /// The values to add, update, or remove. + public static string UriWithQueryParameters( + this NavigationManager navigationManager, + string uri, + IReadOnlyDictionary parameters) { if (navigationManager is null) { throw new ArgumentNullException(nameof(navigationManager)); } - var uri = navigationManager.Uri; - var source = new QueryParameterSource(name, values, formatter); + if (uri is null) + { + throw new ArgumentNullException(nameof(uri)); + } if (!TryRebuildExistingQueryFromUri(uri, out var existingQueryStringEnumerable, out var newQueryStringBuilder)) { - return UriWithAppendedQueryParameters(uri, ref source); + // There was no existing query, so there is no need to allocate a new dictionary to cache + // encoded parameter values and track which parameters have been added. + return GetUriWithAppendedQueryParameters(uri, parameters); } + var parameterSources = CreateParameterSourceDictionary(parameters); + + // Rebuild the query, updating or removing parameters. foreach (var pair in existingQueryStringEnumerable) { - if (pair.EncodedName.Span.Equals(source.EncodedName, StringComparison.OrdinalIgnoreCase)) + if (parameterSources.TryGetValue(pair.EncodedName, out var source)) { - source.AppendNextParameter(ref newQueryStringBuilder); + if (source.TryAppendNextParameter(ref newQueryStringBuilder)) + { + // We have just mutated the struct value so we need to overwrite the copy in the dictionary. + parameterSources[pair.EncodedName] = source; + } } else { @@ -730,12 +759,16 @@ private static string UriWithQueryParameter( } } - while (source.AppendNextParameter(ref newQueryStringBuilder)) ; + // Append any parameters with non-null values that did not replace existing parameters. + foreach (var source in parameterSources.Values) + { + while (source.TryAppendNextParameter(ref newQueryStringBuilder)) ; + } return newQueryStringBuilder.UriWithQueryString; } - private static string UriWithQueryParameterCore(string uri, string name, string value) + private static string GetUriWithUpdatedQueryParameter(string uri, string name, string value) { var encodedName = Uri.EscapeDataString(name); var encodedValue = Uri.EscapeDataString(value); @@ -769,7 +802,7 @@ private static string UriWithQueryParameterCore(string uri, string name, string return newQueryStringBuilder.UriWithQueryString; } - private static string UriWithoutQueryParameter(string uri, string name) + private static string GetUriWithRemovedQueryParameter(string uri, string name) { if (!TryRebuildExistingQueryFromUri(uri, out var existingQueryStringEnumerable, out var newQueryStringBuilder)) { @@ -791,58 +824,30 @@ private static string UriWithoutQueryParameter(string uri, string name) return newQueryStringBuilder.UriWithQueryString; } - /// - /// Returns a URI constructed from with multiple parameters - /// added, updated, or removed. - /// - /// The . - /// The values to add, update, or remove. - public static string UriWithQueryParameters( - this NavigationManager navigationManager, - IReadOnlyDictionary parameters) - => UriWithQueryParameters(navigationManager, navigationManager.Uri, parameters); - - /// - /// Returns a URI constructed from except with multiple parameters - /// added, updated, or removed. - /// - /// The . - /// The URI with the query to modify. - /// The values to add, update, or remove. - public static string UriWithQueryParameters( + private static string GetUriWithUpdatedQueryParameter( this NavigationManager navigationManager, - string uri, - IReadOnlyDictionary parameters) + string name, + IEnumerable values, + QueryParameterFormatter formatter) { if (navigationManager is null) { throw new ArgumentNullException(nameof(navigationManager)); } - if (uri is null) - { - throw new ArgumentNullException(nameof(uri)); - } + var uri = navigationManager.Uri; + var source = new QueryParameterSource(name, values, formatter); if (!TryRebuildExistingQueryFromUri(uri, out var existingQueryStringEnumerable, out var newQueryStringBuilder)) { - // There was no existing query, so there is no need to allocate a new dictionary to cache - // encoded parameter values and track which parameters have been added. - return UriWithAppendedQueryParameters(uri, parameters); + return GetUriWithAppendedQueryParameter(uri, ref source); } - var parameterSourceCollection = CreateParameterSourceDictionary(parameters); - - // Rebuild the query, updating or removing parameters. foreach (var pair in existingQueryStringEnumerable) { - if (parameterSourceCollection.TryGetValue(pair.EncodedName, out var source)) + if (pair.EncodedName.Span.Equals(source.EncodedName, StringComparison.OrdinalIgnoreCase)) { - if (source.AppendNextParameter(ref newQueryStringBuilder)) - { - // We need to add the parameter source back into the dictionary since we're working on a copy. - parameterSourceCollection[pair.EncodedName] = source; - } + source.TryAppendNextParameter(ref newQueryStringBuilder); } else { @@ -850,27 +855,23 @@ public static string UriWithQueryParameters( } } - // Append any parameters with non-null values that did not replace existing parameters. - foreach (var source in parameterSourceCollection.Values) - { - while (source.AppendNextParameter(ref newQueryStringBuilder)) ; - } + while (source.TryAppendNextParameter(ref newQueryStringBuilder)) ; return newQueryStringBuilder.UriWithQueryString; } - private static string UriWithAppendedQueryParameters( + private static string GetUriWithAppendedQueryParameter( string uriWithoutQueryString, ref QueryParameterSource queryParameterSource) { var builder = new QueryStringBuilder(uriWithoutQueryString); - while (queryParameterSource.AppendNextParameter(ref builder)) ; + while (queryParameterSource.TryAppendNextParameter(ref builder)) ; return builder.UriWithQueryString; } - private static string UriWithAppendedQueryParameters( + private static string GetUriWithAppendedQueryParameters( string uriWithoutQueryString, IReadOnlyDictionary parameters) { @@ -879,7 +880,7 @@ private static string UriWithAppendedQueryParameters( foreach (var (name, value) in parameters) { var source = new QueryParameterSource(name, value); - while (source.AppendNextParameter(ref builder)) ; + while (source.TryAppendNextParameter(ref builder)) ; } return builder.UriWithQueryString; From 0f261635ef242f441173dab33b5e1f24dcfe8a92 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Mon, 2 Aug 2021 14:30:28 -0700 Subject: [PATCH 14/14] Update NavigationManagerExtensions.cs --- src/Components/Components/src/NavigationManagerExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Components/Components/src/NavigationManagerExtensions.cs b/src/Components/Components/src/NavigationManagerExtensions.cs index 7030e5530c3b..c8a04d28878c 100644 --- a/src/Components/Components/src/NavigationManagerExtensions.cs +++ b/src/Components/Components/src/NavigationManagerExtensions.cs @@ -847,6 +847,7 @@ private static string GetUriWithUpdatedQueryParameter( { if (pair.EncodedName.Span.Equals(source.EncodedName, StringComparison.OrdinalIgnoreCase)) { + // This will no-op if all parameter values have been appended. source.TryAppendNextParameter(ref newQueryStringBuilder); } else