From 9a51642e1f118e01827dd2fc4ec2cbdb218a7f87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sat, 12 Aug 2023 13:48:07 +0200 Subject: [PATCH 1/2] Route parameter serialization made consistent for custom primitive types --- .../Framework/Routing/DotvvmRoute.cs | 9 ++---- src/Framework/Framework/Routing/UrlHelper.cs | 31 ++++++++++++++++--- src/Tests/Routing/DotvvmRouteTests.cs | 30 +++++++++++++++++- 3 files changed, 57 insertions(+), 13 deletions(-) diff --git a/src/Framework/Framework/Routing/DotvvmRoute.cs b/src/Framework/Framework/Routing/DotvvmRoute.cs index 7bdc85778d..488e725c77 100644 --- a/src/Framework/Framework/Routing/DotvvmRoute.cs +++ b/src/Framework/Framework/Routing/DotvvmRoute.cs @@ -8,6 +8,7 @@ using DotVVM.Framework.Configuration; using System.Diagnostics.CodeAnalysis; using System.Globalization; +using DotVVM.Framework.Utils; namespace DotVVM.Framework.Routing { @@ -127,13 +128,7 @@ protected override string BuildUrlCore(Dictionary values) var convertedValues = values.ToDictionary( v => v.Key, - v => { - var strVal = v.Value is IConvertible convertible ? - convertible.ToString(CultureInfo.InvariantCulture) : - v.Value?.ToString(); - - return strVal == null ? null : Uri.EscapeDataString(strVal); - }, + v => UrlHelper.ParameterToString(v.Value), StringComparer.OrdinalIgnoreCase ); try diff --git a/src/Framework/Framework/Routing/UrlHelper.cs b/src/Framework/Framework/Routing/UrlHelper.cs index f4a93a4982..6783c0494a 100644 --- a/src/Framework/Framework/Routing/UrlHelper.cs +++ b/src/Framework/Framework/Routing/UrlHelper.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Reflection; using System.Text; @@ -33,13 +34,13 @@ public static string BuildUrlSuffix(string? urlSuffix, object? query) case IEnumerable> keyValueCollection: foreach (var item in keyValueCollection.Where(i => i.Value != null)) { - AppendQueryParam(ref resultSuffix, item.Key, item.Value.ToString().NotNull()); + AppendQueryParam(ref resultSuffix, item.Key, ParameterToString(item.Value)); } break; default: foreach (var prop in query.GetType().GetProperties().Where(p => p.GetValue(query) != null)) { - AppendQueryParam(ref resultSuffix, prop.Name, prop.GetValue(query)!.ToString().NotNull()); + AppendQueryParam(ref resultSuffix, prop.Name, ParameterToString(prop.GetValue(query)!)); } break; } @@ -47,14 +48,14 @@ public static string BuildUrlSuffix(string? urlSuffix, object? query) return resultSuffix + (!suffixContainsHash ? "" : urlSuffix.Substring(hashIndex)); } - private static string AppendQueryParam(ref string urlSuffix, string name, string value) + private static string AppendQueryParam(ref string urlSuffix, string name, string? value) { urlSuffix += (urlSuffix.LastIndexOf('?') < 0 ? "?" : "&"); - var hasValue = value.Trim() != string.Empty; + var hasValue = !string.IsNullOrWhiteSpace(value); return (!hasValue) ? urlSuffix += Uri.EscapeDataString(name) : - urlSuffix += $"{Uri.EscapeDataString(name)}={Uri.EscapeDataString(value)}"; + urlSuffix += $"{Uri.EscapeDataString(name)}={value}"; } /// @@ -127,5 +128,25 @@ private static bool ContainsOnlyValidUrlChars(string url) } return true; } + + public static string? ParameterToString(object? value) + { + if (value is null) + { + return null; + } + else if (ReflectionUtils.TryGetCustomPrimitiveTypeRegistration(value.GetType()) is { } registration) + { + return Uri.EscapeDataString(registration.ToStringMethod(value)); + } + else if (value is IConvertible convertible) + { + return Uri.EscapeDataString(convertible.ToString(CultureInfo.InvariantCulture)); + } + else + { + return Uri.EscapeDataString(value.ToString()); + } + } } } diff --git a/src/Tests/Routing/DotvvmRouteTests.cs b/src/Tests/Routing/DotvvmRouteTests.cs index f3f17da929..c11403f94e 100644 --- a/src/Tests/Routing/DotvvmRouteTests.cs +++ b/src/Tests/Routing/DotvvmRouteTests.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using System.Globalization; +using DotVVM.Framework.Tests.Binding; namespace DotVVM.Framework.Tests.Routing { @@ -458,6 +459,18 @@ public void DotvvmRoute_BuildUrl_UrlEncode() }); } + [TestMethod] + + public void DotvvmRoute_BuildUrl_CustomPrimitiveType() + { + CultureUtils.RunWithCulture("cs-CZ", () => { + var route = new DotvvmRoute("Test/{Id}", null, null, null, configuration); + + var result = route.BuildUrl(new { Id = new DecimalNumber(123.4m) }) + UrlHelper.BuildUrlSuffix(null, new { Id = new DecimalNumber(555.5m) }); + Assert.AreEqual("~/Test/123%2C4?Id=555%2C5", result); + }); + } + [TestMethod] public void DotvvmRoute_BuildUrl_Invalid_UnclosedParameter() { @@ -469,7 +482,6 @@ public void DotvvmRoute_BuildUrl_Invalid_UnclosedParameter() }); } - [TestMethod] public void DotvvmRoute_BuildUrl_Invalid_UnclosedParameterConstraint() @@ -669,6 +681,22 @@ public void DotvvmRoute_UrlWithoutTypes() } } + record struct DecimalNumber(decimal Value) : IFormattable + { + public static bool TryParse(string value, out decimal result) + { + if (decimal.TryParse(value, out var r)) + { + result = r; + return true; + } + result = default; + return false; + } + public override string ToString() => Value.ToString(); + public string ToString(string format, IFormatProvider formatProvider) => Value.ToString(null, formatProvider); + } + public class TestPresenterWithoutInterface { From f8d4a5e93a90393c16d6fa2eaed26c1301f290b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sat, 12 Aug 2023 14:29:11 +0200 Subject: [PATCH 2/2] Fixed build warning --- src/Framework/Framework/Routing/UrlHelper.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Framework/Framework/Routing/UrlHelper.cs b/src/Framework/Framework/Routing/UrlHelper.cs index 6783c0494a..1ec51999e0 100644 --- a/src/Framework/Framework/Routing/UrlHelper.cs +++ b/src/Framework/Framework/Routing/UrlHelper.cs @@ -145,7 +145,8 @@ private static bool ContainsOnlyValidUrlChars(string url) } else { - return Uri.EscapeDataString(value.ToString()); + var strVal = value.ToString(); + return strVal == null ? null : Uri.EscapeDataString(strVal); } } }