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/src/RestSharp/RestClient.Async.cs b/src/RestSharp/RestClient.Async.cs index 3e6a83d6f..8beee730b 100644 --- a/src/RestSharp/RestClient.Async.cs +++ b/src/RestSharp/RestClient.Async.cs @@ -61,11 +61,13 @@ public async Task ExecuteAsync(RestRequest request, CancellationTo } static RestResponse GetErrorResponse(RestRequest request, Exception exception, CancellationToken timeoutToken) { + var timedOut = exception is OperationCanceledException && TimedOut(); + var response = new RestResponse(request) { ResponseStatus = exception is OperationCanceledException - ? TimedOut() ? ResponseStatus.TimedOut : ResponseStatus.Aborted + ? timedOut ? ResponseStatus.TimedOut : ResponseStatus.Aborted : ResponseStatus.Error, - ErrorMessage = exception.GetBaseException().Message, + ErrorMessage = timedOut ? "The request timed out." : exception.GetBaseException().Message, ErrorException = exception }; 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]