diff --git a/src/RestSharp/Extensions/ObjectExtensions.cs b/src/RestSharp/Extensions/ObjectExtensions.cs new file mode 100644 index 000000000..5a596d8ba --- /dev/null +++ b/src/RestSharp/Extensions/ObjectExtensions.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation and Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Globalization; + +namespace RestSharp.Extensions; + +static class ObjectExtensions { + /// + /// Converts a value to its string representation using the specified culture for IFormattable types. + /// + /// The type of value to convert + /// The value to convert + /// The culture to use for formatting. If null, uses the current culture. + /// String representation using the specified culture, or null if value is null + internal static string? ToStringWithCulture(this T value, CultureInfo? culture) => value switch { + null => null, + IFormattable f => f.ToString(null, culture), + _ => value.ToString() + }; +} diff --git a/src/RestSharp/Options/RestClientOptions.cs b/src/RestSharp/Options/RestClientOptions.cs index d30e34f92..3017fd985 100644 --- a/src/RestSharp/Options/RestClientOptions.cs +++ b/src/RestSharp/Options/RestClientOptions.cs @@ -13,6 +13,7 @@ // limitations under the License. // +using System.Globalization; using System.Net.Http.Headers; using System.Net.Security; using System.Reflection; @@ -230,4 +231,10 @@ public RestClientOptions(string baseUrl) : this(new Uri(Ensure.NotEmptyString(ba /// Custom function to encode a string for use in a URL query. /// public Func EncodeQuery { get; set; } = (s, encoding) => s.UrlEncode(encoding)!; + + /// + /// Culture to use for formatting IFormattable parameter values. Default is null which uses the current culture. + /// Set to to ensure consistent formatting across different locales. + /// + public CultureInfo? CultureForParameters { get; set; } } diff --git a/src/RestSharp/Request/RestRequestExtensions.Culture.cs b/src/RestSharp/Request/RestRequestExtensions.Culture.cs new file mode 100644 index 000000000..335e88aaf --- /dev/null +++ b/src/RestSharp/Request/RestRequestExtensions.Culture.cs @@ -0,0 +1,71 @@ +// Copyright (c) .NET Foundation and Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using RestSharp.Extensions; + +namespace RestSharp; + +public static partial class RestRequestExtensions { + /// + /// 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 using the culture specified in . + /// + /// The RestClient instance containing the culture settings + /// The request to add the parameter to + /// Name of the parameter + /// Value of the parameter + /// Encode the value or not, default true + /// This request + public static RestRequest AddParameter(this RestRequest request, IRestClient client, string name, T value, bool encode = true) where T : struct + => request.AddParameter(name, value.ToStringWithCulture(client.Options.CultureForParameters), encode); + + /// + /// 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 culture specified in . + /// + /// The RestClient instance containing the culture settings + /// The request to add the parameter to + /// Name of the parameter + /// Value of the parameter + /// Encode the value or not, default true + /// This request + public static RestRequest AddOrUpdateParameter(this RestRequest request, IRestClient client, string name, T value, bool encode = true) where T : struct + => request.AddOrUpdateParameter(name, value.ToStringWithCulture(client.Options.CultureForParameters), encode); + + /// + /// Adds a query string parameter to the request. + /// The value will be converted to string using the culture specified in . + /// + /// The RestClient instance containing the culture settings + /// The request to add the parameter to + /// Parameter name + /// Parameter value + /// Encode the value or not, default true + /// + public static RestRequest AddQueryParameter(this RestRequest request, IRestClient client, string name, T value, bool encode = true) where T : struct + => request.AddQueryParameter(name, value.ToStringWithCulture(client.Options.CultureForParameters), encode); + + /// + /// Adds a URL segment parameter to the request. + /// The value will be converted to string using the culture specified in . + /// + /// The RestClient instance containing the culture settings + /// The request to add the parameter to + /// 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 + /// + public static RestRequest AddUrlSegment(this RestRequest request, IRestClient client, string name, T value, bool encode = true) where T : struct + => request.AddUrlSegment(name, value.ToStringWithCulture(client.Options.CultureForParameters), encode); +} diff --git a/src/RestSharp/RestClient.Extensions.Params.cs b/src/RestSharp/RestClient.Extensions.Params.cs index cd3bb635c..33531d908 100644 --- a/src/RestSharp/RestClient.Extensions.Params.cs +++ b/src/RestSharp/RestClient.Extensions.Params.cs @@ -13,6 +13,8 @@ // limitations under the License. // +using RestSharp.Extensions; + namespace RestSharp; public static partial class RestClientExtensions { @@ -38,6 +40,17 @@ public IRestClient AddDefaultParameter(Parameter parameter) { public IRestClient AddDefaultParameter(string name, string value) => client.AddDefaultParameter(new GetOrPostParameter(name, value)); + /// + /// Adds a default HTTP parameter (QueryString for GET, DELETE, OPTIONS and HEAD; Encoded form for POST and PUT) + /// Used on every request made by this client instance. The value will be formatted using the culture + /// specified in . + /// + /// Name of the parameter + /// Value of the parameter + /// This request + public IRestClient AddDefaultParameter(string name, T value) where T : struct + => client.AddDefaultParameter(new GetOrPostParameter(name, value.ToStringWithCulture(client.Options.CultureForParameters))); + /// /// Adds a default parameter to the client options. There are four types of parameters: /// - GetOrPost: Either a QueryString value or encoded form value based on method @@ -82,6 +95,16 @@ public IRestClient AddDefaultHeaders(Dictionary headers) { public IRestClient AddDefaultUrlSegment(string name, string value) => client.AddDefaultParameter(new UrlSegmentParameter(name, value)); + /// + /// Adds a default URL segment parameter to the RestClient. Used on every request made by this client instance. + /// The value will be formatted using the culture specified in . + /// + /// Name of the segment to add + /// Value of the segment to add + /// + public IRestClient AddDefaultUrlSegment(string name, T value) where T : struct + => client.AddDefaultParameter(new UrlSegmentParameter(name, value.ToStringWithCulture(client.Options.CultureForParameters))); + /// /// Adds a default URL query parameter to the RestClient. Used on every request made by this client instance. /// @@ -90,5 +113,23 @@ public IRestClient AddDefaultUrlSegment(string name, string value) /// public IRestClient AddDefaultQueryParameter(string name, string value) => client.AddDefaultParameter(new QueryParameter(name, value)); + + /// + /// Adds a default URL query parameter to the RestClient. Used on every request made by this client instance. + /// The value will be formatted using the culture specified in . + /// + /// Name of the query parameter to add + /// Value of the query parameter to add + /// + public IRestClient AddDefaultQueryParameter(string name, T value) where T : struct + => client.AddDefaultParameter(new QueryParameter(name, value.ToStringWithCulture(client.Options.CultureForParameters))); + + /// + /// Formats the value using the culture specified in . + /// + /// Value to format + /// String representation of the value using the client's culture setting + public string? FormatValue(T value) where T : struct + => value.ToStringWithCulture(client.Options.CultureForParameters); } } diff --git a/test/RestSharp.Tests/Parameters/InvariantCultureParameterTests.cs b/test/RestSharp.Tests/Parameters/InvariantCultureParameterTests.cs new file mode 100644 index 000000000..43d5ccf50 --- /dev/null +++ b/test/RestSharp.Tests/Parameters/InvariantCultureParameterTests.cs @@ -0,0 +1,265 @@ +using System.Globalization; + +namespace RestSharp.Tests.Parameters; + +public class InvariantCultureParameterTests { + [Fact] + public void AddParameter_Double_UsesInvariantCulture_WhenConfigured() { + // Save original culture + var originalCulture = Thread.CurrentThread.CurrentCulture; + try { + // Set a culture that uses comma as decimal separator + Thread.CurrentThread.CurrentCulture = new CultureInfo("da-DK"); + + var options = new RestClientOptions { CultureForParameters = CultureInfo.InvariantCulture }; + using var client = new RestClient(options); + var request = new RestRequest(); + request.AddParameter(client, "value", 1.234); + + var parameter = request.Parameters.First(); + parameter.Value.Should().Be("1.234"); + } + finally { + Thread.CurrentThread.CurrentCulture = originalCulture; + } + } + + [Fact] + public void AddParameter_Double_UsesCurrentCulture_ByDefault() { + // Save original culture + var originalCulture = Thread.CurrentThread.CurrentCulture; + try { + // Set a culture that uses comma as decimal separator + Thread.CurrentThread.CurrentCulture = new CultureInfo("da-DK"); + + var request = new RestRequest(); + request.AddParameter("value", 1.234); + + var parameter = request.Parameters.First(); + // Default behavior uses current culture (comma as decimal separator) + parameter.Value.Should().Be("1,234"); + } + finally { + Thread.CurrentThread.CurrentCulture = originalCulture; + } + } + + [Fact] + public void AddOrUpdateParameter_Double_UsesInvariantCulture_WhenConfigured() { + var originalCulture = Thread.CurrentThread.CurrentCulture; + try { + Thread.CurrentThread.CurrentCulture = new CultureInfo("da-DK"); + + var options = new RestClientOptions { CultureForParameters = CultureInfo.InvariantCulture }; + using var client = new RestClient(options); + var request = new RestRequest(); + request.AddOrUpdateParameter(client, "value", 1.234); + + var parameter = request.Parameters.First(); + parameter.Value.Should().Be("1.234"); + } + finally { + Thread.CurrentThread.CurrentCulture = originalCulture; + } + } + + [Fact] + public void AddQueryParameter_Double_UsesInvariantCulture_WhenConfigured() { + var originalCulture = Thread.CurrentThread.CurrentCulture; + try { + Thread.CurrentThread.CurrentCulture = new CultureInfo("da-DK"); + + var options = new RestClientOptions { CultureForParameters = CultureInfo.InvariantCulture }; + using var client = new RestClient(options); + var request = new RestRequest(); + request.AddQueryParameter(client, "value", 1.234); + + var parameter = request.Parameters.First(); + parameter.Value.Should().Be("1.234"); + } + finally { + Thread.CurrentThread.CurrentCulture = originalCulture; + } + } + + [Fact] + public void AddUrlSegment_Double_UsesInvariantCulture_WhenConfigured() { + var originalCulture = Thread.CurrentThread.CurrentCulture; + try { + Thread.CurrentThread.CurrentCulture = new CultureInfo("da-DK"); + + var options = new RestClientOptions { CultureForParameters = CultureInfo.InvariantCulture }; + using var client = new RestClient(options); + var request = new RestRequest("{value}"); + request.AddUrlSegment(client, "value", 1.234); + + var parameter = request.Parameters.First(); + parameter.Value.Should().Be("1.234"); + } + finally { + Thread.CurrentThread.CurrentCulture = originalCulture; + } + } + + [Fact] + public void AddDefaultParameter_Double_UsesInvariantCulture_WhenConfigured() { + var originalCulture = Thread.CurrentThread.CurrentCulture; + try { + Thread.CurrentThread.CurrentCulture = new CultureInfo("da-DK"); + + var options = new RestClientOptions { CultureForParameters = CultureInfo.InvariantCulture }; + using var client = new RestClient(options); + client.AddDefaultParameter("value", 1.234); + + var parameter = client.DefaultParameters.First(p => p.Name == "value"); + parameter.Value.Should().Be("1.234"); + } + finally { + Thread.CurrentThread.CurrentCulture = originalCulture; + } + } + + [Fact] + public void AddDefaultQueryParameter_Double_UsesInvariantCulture_WhenConfigured() { + var originalCulture = Thread.CurrentThread.CurrentCulture; + try { + Thread.CurrentThread.CurrentCulture = new CultureInfo("da-DK"); + + var options = new RestClientOptions { CultureForParameters = CultureInfo.InvariantCulture }; + using var client = new RestClient(options); + client.AddDefaultQueryParameter("value", 1.234); + + var parameter = client.DefaultParameters.First(p => p.Name == "value"); + parameter.Value.Should().Be("1.234"); + } + finally { + Thread.CurrentThread.CurrentCulture = originalCulture; + } + } + + [Fact] + public void AddDefaultUrlSegment_Double_UsesInvariantCulture_WhenConfigured() { + var originalCulture = Thread.CurrentThread.CurrentCulture; + try { + Thread.CurrentThread.CurrentCulture = new CultureInfo("da-DK"); + + var options = new RestClientOptions { CultureForParameters = CultureInfo.InvariantCulture }; + using var client = new RestClient(options); + client.AddDefaultUrlSegment("value", 1.234); + + var parameter = client.DefaultParameters.First(p => p.Name == "value"); + parameter.Value.Should().Be("1.234"); + } + finally { + Thread.CurrentThread.CurrentCulture = originalCulture; + } + } + + [Fact] + public void FormatValue_Double_UsesInvariantCulture_WhenConfigured() { + var originalCulture = Thread.CurrentThread.CurrentCulture; + try { + Thread.CurrentThread.CurrentCulture = new CultureInfo("da-DK"); + + var options = new RestClientOptions { CultureForParameters = CultureInfo.InvariantCulture }; + using var client = new RestClient(options); + + var formattedValue = client.FormatValue(1.234); + + formattedValue.Should().Be("1.234"); + } + finally { + Thread.CurrentThread.CurrentCulture = originalCulture; + } + } + + [Fact] + public void AddParameter_Decimal_UsesInvariantCulture_WhenConfigured() { + var originalCulture = Thread.CurrentThread.CurrentCulture; + try { + Thread.CurrentThread.CurrentCulture = new CultureInfo("de-DE"); + + var options = new RestClientOptions { CultureForParameters = CultureInfo.InvariantCulture }; + using var client = new RestClient(options); + var request = new RestRequest(); + request.AddParameter(client, "value", 123.456m); + + var parameter = request.Parameters.First(); + parameter.Value.Should().Be("123.456"); + } + finally { + Thread.CurrentThread.CurrentCulture = originalCulture; + } + } + + [Fact] + public void AddParameter_Float_UsesInvariantCulture_WhenConfigured() { + var originalCulture = Thread.CurrentThread.CurrentCulture; + try { + Thread.CurrentThread.CurrentCulture = new CultureInfo("fr-FR"); + + var options = new RestClientOptions { CultureForParameters = CultureInfo.InvariantCulture }; + using var client = new RestClient(options); + var request = new RestRequest(); + request.AddParameter(client, "value", 2.5f); + + var parameter = request.Parameters.First(); + parameter.Value.Should().Be("2.5"); + } + finally { + Thread.CurrentThread.CurrentCulture = originalCulture; + } + } + + [Fact] + public void AddParameter_DateTime_UsesInvariantCulture_WhenConfigured() { + var originalCulture = Thread.CurrentThread.CurrentCulture; + try { + Thread.CurrentThread.CurrentCulture = new CultureInfo("da-DK"); + + var dateTime = new DateTime(2024, 12, 25, 10, 30, 0, DateTimeKind.Unspecified); + var options = new RestClientOptions { CultureForParameters = CultureInfo.InvariantCulture }; + using var client = new RestClient(options); + var request = new RestRequest(); + request.AddParameter(client, "date", dateTime); + + var parameter = request.Parameters.First(); + // DateTime.ToString with InvariantCulture uses MM/dd/yyyy format + parameter.Value.Should().Be("12/25/2024 10:30:00"); + } + finally { + Thread.CurrentThread.CurrentCulture = originalCulture; + } + } + + [Fact] + public void AddParameter_Integer_SameValueWithOrWithoutInvariantCulture() { + var originalCulture = Thread.CurrentThread.CurrentCulture; + try { + Thread.CurrentThread.CurrentCulture = new CultureInfo("da-DK"); + + var options = new RestClientOptions { CultureForParameters = CultureInfo.InvariantCulture }; + using var client = new RestClient(options); + var requestWithInvariant = new RestRequest(); + requestWithInvariant.AddParameter(client, "value", 12345); + + var requestWithoutInvariant = new RestRequest(); + requestWithoutInvariant.AddParameter("value", 12345); + + var parameterWithInvariant = requestWithInvariant.Parameters.First(); + var parameterWithoutInvariant = requestWithoutInvariant.Parameters.First(); + + parameterWithInvariant.Value.Should().Be("12345"); + parameterWithoutInvariant.Value.Should().Be("12345"); + } + finally { + Thread.CurrentThread.CurrentCulture = originalCulture; + } + } + + [Fact] + public void CultureForParameters_DefaultValue_IsNull() { + var options = new RestClientOptions(); + options.CultureForParameters.Should().BeNull(); + } +}