Skip to content

Commit acdf9af

Browse files
Use CultureInfo.InvariantCulture by default when converting parameter values to strings (#2354)
Previously, AddParameter<T>, AddQueryParameter<T>, AddUrlSegment<T>, AddHeader<T>, AddOrUpdateParameter<T>, AddOrUpdateHeader<T>, 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 <noreply@anthropic.com>
1 parent 53b797b commit acdf9af

File tree

9 files changed

+153
-24
lines changed

9 files changed

+153
-24
lines changed

src/RestSharp/Extensions/StringExtensions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,9 @@ internal IEnumerable<string> GetNameVariants(CultureInfo culture) {
149149
}
150150
}
151151

152+
internal static string? ToStringValue(this object? value, CultureInfo? culture = null)
153+
=> value is IFormattable f ? f.ToString(null, culture ?? CultureInfo.InvariantCulture) : value?.ToString();
154+
152155
internal static bool IsEmpty([NotNullWhen(false)] this string? value) => string.IsNullOrWhiteSpace(value);
153156

154157
internal static bool IsNotEmpty([NotNullWhen(true)] this string? value) => !string.IsNullOrWhiteSpace(value);

src/RestSharp/Parameters/ObjectParser.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515

1616
using System.Reflection;
1717

18+
using System.Globalization;
19+
using RestSharp.Extensions;
20+
1821
namespace RestSharp;
1922

2023
static class ObjectParser {
@@ -72,7 +75,7 @@ IEnumerable<ParsedParameter> GetArray(PropertyInfo propertyInfo, object? value)
7275
bool IsAllowedProperty(string propertyName)
7376
=> includedProperties.Length == 0 || includedProperties.Length > 0 && includedProperties.Contains(propertyName);
7477

75-
string? ParseValue(string? format, object? value) => format == null ? value?.ToString() : string.Format($"{{0:{format}}}", value);
78+
string? ParseValue(string? format, object? value) => format == null ? value.ToStringValue() : string.Format(CultureInfo.InvariantCulture, $"{{0:{format}}}", value);
7679
}
7780
}
7881

src/RestSharp/Parameters/Parameter.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// limitations under the License.
1414

1515
using System.Diagnostics;
16+
using RestSharp.Extensions;
1617

1718
namespace RestSharp;
1819

@@ -76,10 +77,10 @@ protected Parameter(string? name, object? value, ParameterType type, bool encode
7677
public static Parameter CreateParameter(string? name, object? value, ParameterType type, bool encode = true)
7778
// ReSharper disable once SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault
7879
=> type switch {
79-
ParameterType.GetOrPost => new GetOrPostParameter(Ensure.NotEmptyString(name, nameof(name)), value?.ToString(), encode),
80-
ParameterType.UrlSegment => new UrlSegmentParameter(Ensure.NotEmptyString(name, nameof(name)), value?.ToString()!, encode),
81-
ParameterType.HttpHeader => new HeaderParameter(name!, value?.ToString()!),
82-
ParameterType.QueryString => new QueryParameter(Ensure.NotEmptyString(name, nameof(name)), value?.ToString(), encode),
80+
ParameterType.GetOrPost => new GetOrPostParameter(Ensure.NotEmptyString(name, nameof(name)), value.ToStringValue(), encode),
81+
ParameterType.UrlSegment => new UrlSegmentParameter(Ensure.NotEmptyString(name, nameof(name)), value.ToStringValue()!, encode),
82+
ParameterType.HttpHeader => new HeaderParameter(name!, value.ToStringValue()!),
83+
ParameterType.QueryString => new QueryParameter(Ensure.NotEmptyString(name, nameof(name)), value.ToStringValue(), encode),
8384
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
8485
};
8586

src/RestSharp/Request/RestRequestExtensions.Headers.cs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
using System.Globalization;
16+
using RestSharp.Extensions;
17+
1518
namespace RestSharp;
1619

1720
public static partial class RestRequestExtensions {
@@ -42,12 +45,14 @@ public RestRequest AddHeader(string name, string value)
4245

4346
/// <summary>
4447
/// Adds a header to the request. RestSharp will try to separate request and content headers when calling the resource.
48+
/// The value will be converted to string using the specified culture, or <see cref="CultureInfo.InvariantCulture"/> by default.
4549
/// </summary>
4650
/// <param name="name">Header name</param>
4751
/// <param name="value">Header value</param>
52+
/// <param name="culture">Culture to use for formatting the value, defaults to <see cref="CultureInfo.InvariantCulture"/></param>
4853
/// <returns></returns>
49-
public RestRequest AddHeader<T>(string name, T value) where T : struct
50-
=> request.AddHeader(name, Ensure.NotNull(value.ToString(), nameof(value)));
54+
public RestRequest AddHeader<T>(string name, T value, CultureInfo? culture = null) where T : struct
55+
=> request.AddHeader(name, Ensure.NotNull(value.ToStringValue(culture), nameof(value)));
5156

5257
/// <summary>
5358
/// 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)
6267
/// <summary>
6368
/// Adds or updates the request header. RestSharp will try to separate request and content headers when calling the resource.
6469
/// The existing header with the same name will be replaced.
70+
/// The value will be converted to string using the specified culture, or <see cref="CultureInfo.InvariantCulture"/> by default.
6571
/// </summary>
6672
/// <param name="name">Header name</param>
6773
/// <param name="value">Header value</param>
74+
/// <param name="culture">Culture to use for formatting the value, defaults to <see cref="CultureInfo.InvariantCulture"/></param>
6875
/// <returns></returns>
69-
public RestRequest AddOrUpdateHeader<T>(string name, T value) where T : struct
70-
=> request.AddOrUpdateHeader(name, Ensure.NotNull(value.ToString(), nameof(value)));
76+
public RestRequest AddOrUpdateHeader<T>(string name, T value, CultureInfo? culture = null) where T : struct
77+
=> request.AddOrUpdateHeader(name, Ensure.NotNull(value.ToStringValue(culture), nameof(value)));
7178

7279
/// <summary>
7380
/// Adds multiple headers to the request, using the key-value pairs provided.

src/RestSharp/Request/RestRequestExtensions.Query.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
using System.Globalization;
16+
using RestSharp.Extensions;
17+
1518
namespace RestSharp;
1619

1720
public static partial class RestRequestExtensions {
@@ -31,12 +34,14 @@ public RestRequest AddQueryParameter(string name, string? value, bool encode = t
3134
/// <summary>
3235
/// Adds a query string parameter to the request. The request resource should not contain any placeholders for this parameter.
3336
/// The parameter will be added to the request URL as a query string using name=value format.
37+
/// The value will be converted to string using the specified culture, or <see cref="CultureInfo.InvariantCulture"/> by default.
3438
/// </summary>
3539
/// <param name="name">Parameter name</param>
3640
/// <param name="value">Parameter value</param>
3741
/// <param name="encode">Encode the value or not, default true</param>
42+
/// <param name="culture">Culture to use for formatting the value, defaults to <see cref="CultureInfo.InvariantCulture"/></param>
3843
/// <returns></returns>
39-
public RestRequest AddQueryParameter<T>(string name, T value, bool encode = true) where T : struct
40-
=> request.AddQueryParameter(name, value.ToString(), encode);
44+
public RestRequest AddQueryParameter<T>(string name, T value, bool encode = true, CultureInfo? culture = null) where T : struct
45+
=> request.AddQueryParameter(name, value.ToStringValue(culture), encode);
4146
}
4247
}

src/RestSharp/Request/RestRequestExtensions.Url.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
using System.Globalization;
16+
using RestSharp.Extensions;
17+
1518
namespace RestSharp;
1619

1720
public static partial class RestRequestExtensions {
@@ -31,12 +34,14 @@ public RestRequest AddUrlSegment(string name, string? value, bool encode = true)
3134
/// <summary>
3235
/// Adds a URL segment parameter to the request. The resource URL must have a placeholder for the parameter for it to work.
3336
/// For example, if you add a URL segment parameter with the name "id", the resource URL should contain {id} in its path.
37+
/// The value will be converted to string using the specified culture, or <see cref="CultureInfo.InvariantCulture"/> by default.
3438
/// </summary>
3539
/// <param name="name">Name of the parameter; must be matching a placeholder in the resource URL as {name}</param>
3640
/// <param name="value">Value of the parameter</param>
3741
/// <param name="encode">Encode the value or not, default true</param>
42+
/// <param name="culture">Culture to use for formatting the value, defaults to <see cref="CultureInfo.InvariantCulture"/></param>
3843
/// <returns></returns>
39-
public RestRequest AddUrlSegment<T>(string name, T value, bool encode = true) where T : struct
40-
=> request.AddUrlSegment(name, value.ToString(), encode);
44+
public RestRequest AddUrlSegment<T>(string name, T value, bool encode = true, CultureInfo? culture = null) where T : struct
45+
=> request.AddUrlSegment(name, value.ToStringValue(culture), encode);
4146
}
4247
}

src/RestSharp/Request/RestRequestExtensions.cs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
using System.Globalization;
16+
using RestSharp.Extensions;
17+
1518
namespace RestSharp;
1619

1720
[PublicAPI]
@@ -45,14 +48,15 @@ public RestRequest AddParameter(string? name, object value, ParameterType type,
4548

4649
/// <summary>
4750
/// Adds a HTTP parameter to the request (QueryString for GET, DELETE, OPTIONS and HEAD; Encoded form for POST and PUT).
48-
/// The value will be converted to string.
51+
/// The value will be converted to string using the specified culture, or <see cref="CultureInfo.InvariantCulture"/> by default.
4952
/// </summary>
5053
/// <param name="name">Name of the parameter</param>
5154
/// <param name="value">Value of the parameter</param>
5255
/// <param name="encode">Encode the value or not, default true</param>
56+
/// <param name="culture">Culture to use for formatting the value, defaults to <see cref="CultureInfo.InvariantCulture"/></param>
5357
/// <returns>This request</returns>
54-
public RestRequest AddParameter<T>(string name, T value, bool encode = true) where T : struct
55-
=> request.AddParameter(name, value.ToString(), encode);
58+
public RestRequest AddParameter<T>(string name, T value, bool encode = true, CultureInfo? culture = null) where T : struct
59+
=> request.AddParameter(name, value.ToStringValue(culture), encode);
5660

5761
/// <summary>
5862
/// 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
6569
=> request.AddOrUpdateParameter(new GetOrPostParameter(name, value, encode));
6670

6771
/// <summary>
68-
/// Adds or updates a HTTP parameter to the request (QueryString for GET, DELETE, OPTIONS and HEAD; Encoded form for POST and PUT)
72+
/// Adds or updates a HTTP parameter to the request (QueryString for GET, DELETE, OPTIONS and HEAD; Encoded form for POST and PUT).
73+
/// The value will be converted to string using the specified culture, or <see cref="CultureInfo.InvariantCulture"/> by default.
6974
/// </summary>
7075
/// <param name="name">Name of the parameter</param>
7176
/// <param name="value">Value of the parameter</param>
7277
/// <param name="encode">Encode the value or not, default true</param>
78+
/// <param name="culture">Culture to use for formatting the value, defaults to <see cref="CultureInfo.InvariantCulture"/></param>
7379
/// <returns>This request</returns>
74-
public RestRequest AddOrUpdateParameter<T>(string name, T value, bool encode = true) where T : struct
75-
=> request.AddOrUpdateParameter(name, value.ToString(), encode);
80+
public RestRequest AddOrUpdateParameter<T>(string name, T value, bool encode = true, CultureInfo? culture = null) where T : struct
81+
=> request.AddOrUpdateParameter(name, value.ToStringValue(culture), encode);
7682

7783
RestRequest AddParameters(IEnumerable<Parameter> parameters) {
7884
request.Parameters.AddParameters(parameters);
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
using System.Globalization;
2+
3+
namespace RestSharp.Tests;
4+
5+
public class InvariantCultureTests {
6+
[Fact]
7+
public void AddParameter_uses_invariant_culture_for_double() {
8+
var originalCulture = CultureInfo.CurrentCulture;
9+
10+
try {
11+
CultureInfo.CurrentCulture = new CultureInfo("da-DK");
12+
var request = new RestRequest().AddParameter("value", 1.234);
13+
14+
var parameter = request.Parameters.FirstOrDefault(p => p.Name == "value");
15+
parameter.Should().NotBeNull();
16+
parameter!.Value.Should().Be("1.234");
17+
}
18+
finally {
19+
CultureInfo.CurrentCulture = originalCulture;
20+
}
21+
}
22+
23+
[Fact]
24+
public void AddParameter_can_use_specific_culture() {
25+
var request = new RestRequest().AddParameter("value", 1.234, culture: new CultureInfo("da-DK"));
26+
27+
var parameter = request.Parameters.FirstOrDefault(p => p.Name == "value");
28+
parameter.Should().NotBeNull();
29+
parameter!.Value.Should().Be("1,234");
30+
}
31+
32+
[Fact]
33+
public void AddOrUpdateParameter_uses_invariant_culture_for_double() {
34+
var originalCulture = CultureInfo.CurrentCulture;
35+
36+
try {
37+
CultureInfo.CurrentCulture = new CultureInfo("da-DK");
38+
var request = new RestRequest().AddOrUpdateParameter("value", 1.234);
39+
40+
var parameter = request.Parameters.FirstOrDefault(p => p.Name == "value");
41+
parameter.Should().NotBeNull();
42+
parameter!.Value.Should().Be("1.234");
43+
}
44+
finally {
45+
CultureInfo.CurrentCulture = originalCulture;
46+
}
47+
}
48+
49+
[Fact]
50+
public void AddQueryParameter_uses_invariant_culture_for_decimal() {
51+
var originalCulture = CultureInfo.CurrentCulture;
52+
53+
try {
54+
CultureInfo.CurrentCulture = new CultureInfo("fr-FR");
55+
var request = new RestRequest().AddQueryParameter("price", 99.95m);
56+
57+
var parameter = request.Parameters.FirstOrDefault(p => p.Name == "price");
58+
parameter.Should().NotBeNull();
59+
parameter!.Value.Should().Be("99.95");
60+
}
61+
finally {
62+
CultureInfo.CurrentCulture = originalCulture;
63+
}
64+
}
65+
66+
[Fact]
67+
public void AddUrlSegment_uses_invariant_culture_for_float() {
68+
var originalCulture = CultureInfo.CurrentCulture;
69+
70+
try {
71+
CultureInfo.CurrentCulture = new CultureInfo("de-DE");
72+
var request = new RestRequest("{id}").AddUrlSegment("id", 3.14f);
73+
74+
var parameter = request.Parameters.FirstOrDefault(p => p.Name == "id");
75+
parameter.Should().NotBeNull();
76+
parameter!.Value.Should().Be("3.14");
77+
}
78+
finally {
79+
CultureInfo.CurrentCulture = originalCulture;
80+
}
81+
}
82+
83+
[Fact]
84+
public void CreateParameter_uses_invariant_culture_for_object_value() {
85+
var originalCulture = CultureInfo.CurrentCulture;
86+
87+
try {
88+
CultureInfo.CurrentCulture = new CultureInfo("da-DK");
89+
object value = 1.234;
90+
var parameter = Parameter.CreateParameter("value", value, ParameterType.QueryString);
91+
92+
parameter.Value.Should().Be("1.234");
93+
}
94+
finally {
95+
CultureInfo.CurrentCulture = originalCulture;
96+
}
97+
}
98+
}

test/RestSharp.Tests/ObjectParserTests.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
// ReSharper disable PropertyCanBeMadeInitOnly.Local
2+
using System.Globalization;
3+
24
namespace RestSharp.Tests;
35

46
public class ObjectParserTests {
@@ -17,11 +19,10 @@ public void ShouldUseRequestProperty() {
1719

1820
var parsed = request.GetProperties().ToDictionary(x => x.Name, x => x.Value);
1921
parsed["some_data"].Should().Be(request.SomeData);
20-
parsed["SomeDate"].Should().Be(request.SomeDate.ToString("d"));
21-
parsed["Plain"].Should().Be(request.Plain.ToString());
22-
// ReSharper disable once SpecifyACultureInStringConversionExplicitly
23-
parsed["PlainArray"].Should().Be(string.Join(",", dates.Select(x => x.ToString())));
24-
parsed["dates"].Should().Be(string.Join(",", dates.Select(x => x.ToString("d"))));
22+
parsed["SomeDate"].Should().Be(request.SomeDate.ToString("d", CultureInfo.InvariantCulture));
23+
parsed["Plain"].Should().Be(request.Plain.ToString(CultureInfo.InvariantCulture));
24+
parsed["PlainArray"].Should().Be(string.Join(",", dates.Select(x => x.ToString(CultureInfo.InvariantCulture))));
25+
parsed["dates"].Should().Be(string.Join(",", dates.Select(x => x.ToString("d", CultureInfo.InvariantCulture))));
2526
}
2627

2728
[Fact]

0 commit comments

Comments
 (0)