From 6e7947e561689421f68c28556c8a3ed0ca37c8be Mon Sep 17 00:00:00 2001 From: Alexey Zimarev Date: Thu, 26 Feb 2026 18:18:52 +0100 Subject: [PATCH] Use CultureInfo.InvariantCulture by default when converting parameter values to strings Previously, AddParameter, AddQueryParameter, AddUrlSegment, AddHeader, AddOrUpdateParameter, AddOrUpdateHeader, and Parameter.CreateParameter all used ToString() which respects the current culture. This caused issues on non-English locales where e.g. 1.234 would be formatted as "1,234" with a comma decimal separator. All generic overloads now default to InvariantCulture and accept an optional CultureInfo parameter for callers who need locale-specific formatting. Fixes #2270 Co-Authored-By: Claude Opus 4.6 --- src/RestSharp/Extensions/StringExtensions.cs | 3 + src/RestSharp/Parameters/ObjectParser.cs | 5 +- src/RestSharp/Parameters/Parameter.cs | 9 +- .../Request/RestRequestExtensions.Headers.cs | 15 ++- .../Request/RestRequestExtensions.Query.cs | 9 +- .../Request/RestRequestExtensions.Url.cs | 9 +- .../Request/RestRequestExtensions.cs | 18 ++-- test/RestSharp.Tests/InvariantCultureTests.cs | 98 +++++++++++++++++++ test/RestSharp.Tests/ObjectParserTests.cs | 11 ++- 9 files changed, 153 insertions(+), 24 deletions(-) create mode 100644 test/RestSharp.Tests/InvariantCultureTests.cs diff --git a/src/RestSharp/Extensions/StringExtensions.cs b/src/RestSharp/Extensions/StringExtensions.cs index 637177a67..800e331be 100644 --- a/src/RestSharp/Extensions/StringExtensions.cs +++ b/src/RestSharp/Extensions/StringExtensions.cs @@ -149,6 +149,9 @@ internal IEnumerable GetNameVariants(CultureInfo culture) { } } + internal static string? ToStringValue(this object? value, CultureInfo? culture = null) + => value is IFormattable f ? f.ToString(null, culture ?? CultureInfo.InvariantCulture) : value?.ToString(); + internal static bool IsEmpty([NotNullWhen(false)] this string? value) => string.IsNullOrWhiteSpace(value); internal static bool IsNotEmpty([NotNullWhen(true)] this string? value) => !string.IsNullOrWhiteSpace(value); diff --git a/src/RestSharp/Parameters/ObjectParser.cs b/src/RestSharp/Parameters/ObjectParser.cs index 3f3464d97..c9cebcc02 100644 --- a/src/RestSharp/Parameters/ObjectParser.cs +++ b/src/RestSharp/Parameters/ObjectParser.cs @@ -15,6 +15,9 @@ using System.Reflection; +using System.Globalization; +using RestSharp.Extensions; + namespace RestSharp; static class ObjectParser { @@ -72,7 +75,7 @@ IEnumerable GetArray(PropertyInfo propertyInfo, object? value) bool IsAllowedProperty(string propertyName) => includedProperties.Length == 0 || includedProperties.Length > 0 && includedProperties.Contains(propertyName); - string? ParseValue(string? format, object? value) => format == null ? value?.ToString() : string.Format($"{{0:{format}}}", value); + string? ParseValue(string? format, object? value) => format == null ? value.ToStringValue() : string.Format(CultureInfo.InvariantCulture, $"{{0:{format}}}", value); } } diff --git a/src/RestSharp/Parameters/Parameter.cs b/src/RestSharp/Parameters/Parameter.cs index b23c592dc..ebe4cf590 100644 --- a/src/RestSharp/Parameters/Parameter.cs +++ b/src/RestSharp/Parameters/Parameter.cs @@ -13,6 +13,7 @@ // limitations under the License. using System.Diagnostics; +using RestSharp.Extensions; namespace RestSharp; @@ -76,10 +77,10 @@ protected Parameter(string? name, object? value, ParameterType type, bool encode public static Parameter CreateParameter(string? name, object? value, ParameterType type, bool encode = true) // ReSharper disable once SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault => type switch { - ParameterType.GetOrPost => new GetOrPostParameter(Ensure.NotEmptyString(name, nameof(name)), value?.ToString(), encode), - ParameterType.UrlSegment => new UrlSegmentParameter(Ensure.NotEmptyString(name, nameof(name)), value?.ToString()!, encode), - ParameterType.HttpHeader => new HeaderParameter(name!, value?.ToString()!), - ParameterType.QueryString => new QueryParameter(Ensure.NotEmptyString(name, nameof(name)), value?.ToString(), encode), + ParameterType.GetOrPost => new GetOrPostParameter(Ensure.NotEmptyString(name, nameof(name)), value.ToStringValue(), encode), + ParameterType.UrlSegment => new UrlSegmentParameter(Ensure.NotEmptyString(name, nameof(name)), value.ToStringValue()!, encode), + ParameterType.HttpHeader => new HeaderParameter(name!, value.ToStringValue()!), + ParameterType.QueryString => new QueryParameter(Ensure.NotEmptyString(name, nameof(name)), value.ToStringValue(), encode), _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) }; diff --git a/src/RestSharp/Request/RestRequestExtensions.Headers.cs b/src/RestSharp/Request/RestRequestExtensions.Headers.cs index 2838db65f..f026948a1 100644 --- a/src/RestSharp/Request/RestRequestExtensions.Headers.cs +++ b/src/RestSharp/Request/RestRequestExtensions.Headers.cs @@ -12,6 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.Globalization; +using RestSharp.Extensions; + namespace RestSharp; public static partial class RestRequestExtensions { @@ -42,12 +45,14 @@ public RestRequest AddHeader(string name, string value) /// /// Adds a header to the request. RestSharp will try to separate request and content headers when calling the resource. + /// The value will be converted to string using the specified culture, or by default. /// /// Header name /// Header value + /// Culture to use for formatting the value, defaults to /// - public RestRequest AddHeader(string name, T value) where T : struct - => request.AddHeader(name, Ensure.NotNull(value.ToString(), nameof(value))); + public RestRequest AddHeader(string name, T value, CultureInfo? culture = null) where T : struct + => request.AddHeader(name, Ensure.NotNull(value.ToStringValue(culture), nameof(value))); /// /// Adds or updates the request header. RestSharp will try to separate request and content headers when calling the resource. @@ -62,12 +67,14 @@ public RestRequest AddOrUpdateHeader(string name, string value) /// /// Adds or updates the request header. RestSharp will try to separate request and content headers when calling the resource. /// The existing header with the same name will be replaced. + /// The value will be converted to string using the specified culture, or by default. /// /// Header name /// Header value + /// Culture to use for formatting the value, defaults to /// - public RestRequest AddOrUpdateHeader(string name, T value) where T : struct - => request.AddOrUpdateHeader(name, Ensure.NotNull(value.ToString(), nameof(value))); + public RestRequest AddOrUpdateHeader(string name, T value, CultureInfo? culture = null) where T : struct + => request.AddOrUpdateHeader(name, Ensure.NotNull(value.ToStringValue(culture), nameof(value))); /// /// Adds multiple headers to the request, using the key-value pairs provided. diff --git a/src/RestSharp/Request/RestRequestExtensions.Query.cs b/src/RestSharp/Request/RestRequestExtensions.Query.cs index 265dc360d..14d69e309 100644 --- a/src/RestSharp/Request/RestRequestExtensions.Query.cs +++ b/src/RestSharp/Request/RestRequestExtensions.Query.cs @@ -12,6 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.Globalization; +using RestSharp.Extensions; + namespace RestSharp; public static partial class RestRequestExtensions { @@ -31,12 +34,14 @@ public RestRequest AddQueryParameter(string name, string? value, bool encode = t /// /// Adds a query string parameter to the request. The request resource should not contain any placeholders for this parameter. /// The parameter will be added to the request URL as a query string using name=value format. + /// The value will be converted to string using the specified culture, or by default. /// /// Parameter name /// Parameter value /// Encode the value or not, default true + /// Culture to use for formatting the value, defaults to /// - public RestRequest AddQueryParameter(string name, T value, bool encode = true) where T : struct - => request.AddQueryParameter(name, value.ToString(), encode); + public RestRequest AddQueryParameter(string name, T value, bool encode = true, CultureInfo? culture = null) where T : struct + => request.AddQueryParameter(name, value.ToStringValue(culture), encode); } } \ No newline at end of file diff --git a/src/RestSharp/Request/RestRequestExtensions.Url.cs b/src/RestSharp/Request/RestRequestExtensions.Url.cs index 499c629df..66dbfe5ad 100644 --- a/src/RestSharp/Request/RestRequestExtensions.Url.cs +++ b/src/RestSharp/Request/RestRequestExtensions.Url.cs @@ -12,6 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.Globalization; +using RestSharp.Extensions; + namespace RestSharp; public static partial class RestRequestExtensions { @@ -31,12 +34,14 @@ public RestRequest AddUrlSegment(string name, string? value, bool encode = true) /// /// Adds a URL segment parameter to the request. The resource URL must have a placeholder for the parameter for it to work. /// For example, if you add a URL segment parameter with the name "id", the resource URL should contain {id} in its path. + /// The value will be converted to string using the specified culture, or by default. /// /// Name of the parameter; must be matching a placeholder in the resource URL as {name} /// Value of the parameter /// Encode the value or not, default true + /// Culture to use for formatting the value, defaults to /// - public RestRequest AddUrlSegment(string name, T value, bool encode = true) where T : struct - => request.AddUrlSegment(name, value.ToString(), encode); + public RestRequest AddUrlSegment(string name, T value, bool encode = true, CultureInfo? culture = null) where T : struct + => request.AddUrlSegment(name, value.ToStringValue(culture), encode); } } \ No newline at end of file diff --git a/src/RestSharp/Request/RestRequestExtensions.cs b/src/RestSharp/Request/RestRequestExtensions.cs index 687884625..55bdd550f 100644 --- a/src/RestSharp/Request/RestRequestExtensions.cs +++ b/src/RestSharp/Request/RestRequestExtensions.cs @@ -12,6 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.Globalization; +using RestSharp.Extensions; + namespace RestSharp; [PublicAPI] @@ -45,14 +48,15 @@ public RestRequest AddParameter(string? name, object value, ParameterType type, /// /// Adds a HTTP parameter to the request (QueryString for GET, DELETE, OPTIONS and HEAD; Encoded form for POST and PUT). - /// The value will be converted to string. + /// The value will be converted to string using the specified culture, or by default. /// /// Name of the parameter /// Value of the parameter /// Encode the value or not, default true + /// Culture to use for formatting the value, defaults to /// This request - public RestRequest AddParameter(string name, T value, bool encode = true) where T : struct - => request.AddParameter(name, value.ToString(), encode); + public RestRequest AddParameter(string name, T value, bool encode = true, CultureInfo? culture = null) where T : struct + => request.AddParameter(name, value.ToStringValue(culture), encode); /// /// Adds or updates a HTTP parameter to the request (QueryString for GET, DELETE, OPTIONS and HEAD; Encoded form for POST and PUT) @@ -65,14 +69,16 @@ public RestRequest AddOrUpdateParameter(string name, string? value, bool encode => request.AddOrUpdateParameter(new GetOrPostParameter(name, value, encode)); /// - /// Adds or updates a HTTP parameter to the request (QueryString for GET, DELETE, OPTIONS and HEAD; Encoded form for POST and PUT) + /// Adds or updates a HTTP parameter to the request (QueryString for GET, DELETE, OPTIONS and HEAD; Encoded form for POST and PUT). + /// The value will be converted to string using the specified culture, or by default. /// /// Name of the parameter /// Value of the parameter /// Encode the value or not, default true + /// Culture to use for formatting the value, defaults to /// This request - public RestRequest AddOrUpdateParameter(string name, T value, bool encode = true) where T : struct - => request.AddOrUpdateParameter(name, value.ToString(), encode); + public RestRequest AddOrUpdateParameter(string name, T value, bool encode = true, CultureInfo? culture = null) where T : struct + => request.AddOrUpdateParameter(name, value.ToStringValue(culture), encode); RestRequest AddParameters(IEnumerable parameters) { request.Parameters.AddParameters(parameters); diff --git a/test/RestSharp.Tests/InvariantCultureTests.cs b/test/RestSharp.Tests/InvariantCultureTests.cs new file mode 100644 index 000000000..6816adae2 --- /dev/null +++ b/test/RestSharp.Tests/InvariantCultureTests.cs @@ -0,0 +1,98 @@ +using System.Globalization; + +namespace RestSharp.Tests; + +public class InvariantCultureTests { + [Fact] + public void AddParameter_uses_invariant_culture_for_double() { + var originalCulture = CultureInfo.CurrentCulture; + + try { + CultureInfo.CurrentCulture = new CultureInfo("da-DK"); + var request = new RestRequest().AddParameter("value", 1.234); + + var parameter = request.Parameters.FirstOrDefault(p => p.Name == "value"); + parameter.Should().NotBeNull(); + parameter!.Value.Should().Be("1.234"); + } + finally { + CultureInfo.CurrentCulture = originalCulture; + } + } + + [Fact] + public void AddParameter_can_use_specific_culture() { + var request = new RestRequest().AddParameter("value", 1.234, culture: new CultureInfo("da-DK")); + + var parameter = request.Parameters.FirstOrDefault(p => p.Name == "value"); + parameter.Should().NotBeNull(); + parameter!.Value.Should().Be("1,234"); + } + + [Fact] + public void AddOrUpdateParameter_uses_invariant_culture_for_double() { + var originalCulture = CultureInfo.CurrentCulture; + + try { + CultureInfo.CurrentCulture = new CultureInfo("da-DK"); + var request = new RestRequest().AddOrUpdateParameter("value", 1.234); + + var parameter = request.Parameters.FirstOrDefault(p => p.Name == "value"); + parameter.Should().NotBeNull(); + parameter!.Value.Should().Be("1.234"); + } + finally { + CultureInfo.CurrentCulture = originalCulture; + } + } + + [Fact] + public void AddQueryParameter_uses_invariant_culture_for_decimal() { + var originalCulture = CultureInfo.CurrentCulture; + + try { + CultureInfo.CurrentCulture = new CultureInfo("fr-FR"); + var request = new RestRequest().AddQueryParameter("price", 99.95m); + + var parameter = request.Parameters.FirstOrDefault(p => p.Name == "price"); + parameter.Should().NotBeNull(); + parameter!.Value.Should().Be("99.95"); + } + finally { + CultureInfo.CurrentCulture = originalCulture; + } + } + + [Fact] + public void AddUrlSegment_uses_invariant_culture_for_float() { + var originalCulture = CultureInfo.CurrentCulture; + + try { + CultureInfo.CurrentCulture = new CultureInfo("de-DE"); + var request = new RestRequest("{id}").AddUrlSegment("id", 3.14f); + + var parameter = request.Parameters.FirstOrDefault(p => p.Name == "id"); + parameter.Should().NotBeNull(); + parameter!.Value.Should().Be("3.14"); + } + finally { + CultureInfo.CurrentCulture = originalCulture; + } + } + + [Fact] + public void CreateParameter_uses_invariant_culture_for_object_value() { + var originalCulture = CultureInfo.CurrentCulture; + + try { + CultureInfo.CurrentCulture = new CultureInfo("da-DK"); + object value = 1.234; + var parameter = Parameter.CreateParameter("value", value, ParameterType.QueryString); + + parameter.Value.Should().Be("1.234"); + } + finally { + CultureInfo.CurrentCulture = originalCulture; + } + } +} diff --git a/test/RestSharp.Tests/ObjectParserTests.cs b/test/RestSharp.Tests/ObjectParserTests.cs index 6bcbef081..769493a86 100644 --- a/test/RestSharp.Tests/ObjectParserTests.cs +++ b/test/RestSharp.Tests/ObjectParserTests.cs @@ -1,4 +1,6 @@ // ReSharper disable PropertyCanBeMadeInitOnly.Local +using System.Globalization; + namespace RestSharp.Tests; public class ObjectParserTests { @@ -17,11 +19,10 @@ public void ShouldUseRequestProperty() { var parsed = request.GetProperties().ToDictionary(x => x.Name, x => x.Value); parsed["some_data"].Should().Be(request.SomeData); - parsed["SomeDate"].Should().Be(request.SomeDate.ToString("d")); - parsed["Plain"].Should().Be(request.Plain.ToString()); - // ReSharper disable once SpecifyACultureInStringConversionExplicitly - parsed["PlainArray"].Should().Be(string.Join(",", dates.Select(x => x.ToString()))); - parsed["dates"].Should().Be(string.Join(",", dates.Select(x => x.ToString("d")))); + parsed["SomeDate"].Should().Be(request.SomeDate.ToString("d", CultureInfo.InvariantCulture)); + parsed["Plain"].Should().Be(request.Plain.ToString(CultureInfo.InvariantCulture)); + parsed["PlainArray"].Should().Be(string.Join(",", dates.Select(x => x.ToString(CultureInfo.InvariantCulture)))); + parsed["dates"].Should().Be(string.Join(",", dates.Select(x => x.ToString("d", CultureInfo.InvariantCulture)))); } [Fact]