Skip to content

Use InvariantCulture for parameter value formatting#2354

Merged
alexeyzimarev merged 1 commit intodevfrom
fix/invariant-culture-parameter-formatting
Feb 26, 2026
Merged

Use InvariantCulture for parameter value formatting#2354
alexeyzimarev merged 1 commit intodevfrom
fix/invariant-culture-parameter-formatting

Conversation

@alexeyzimarev
Copy link
Member

Summary

  • All generic <T> where T : struct overloads (AddParameter<T>, AddOrUpdateParameter<T>, AddQueryParameter<T>, AddUrlSegment<T>, AddHeader<T>, AddOrUpdateHeader<T>) now format values using CultureInfo.InvariantCulture by default
  • Parameter.CreateParameter and ObjectParser.ParseValue also use invariant culture
  • An optional CultureInfo? culture parameter is added to all generic overloads for callers who need locale-specific formatting (e.g. .AddParameter("value", 1.234, culture: new CultureInfo("da-DK")))
  • Adds internal ToStringValue(CultureInfo?) helper in StringExtensions

Fixes #2270

Test plan

  • Project builds successfully
  • All existing unit tests pass (ObjectParserTests updated to match invariant culture output)
  • New tests verify invariant culture is used by default for double, decimal, float across da-DK, fr-FR, de-DE locales
  • New test verifies culture override parameter works correctly

🤖 Generated with Claude Code

… values to strings

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>
@qodo-free-for-open-source-projects
Copy link
Contributor

Review Summary by Qodo

Use InvariantCulture for parameter value formatting across all generic overloads

🐞 Bug fix ✨ Enhancement

Grey Divider

Walkthroughs

Description
• Use CultureInfo.InvariantCulture by default for all generic parameter formatting methods
• Add optional CultureInfo parameter to allow locale-specific formatting when needed
• Implement ToStringValue() helper extension for consistent invariant culture conversion
• Update ObjectParser and Parameter.CreateParameter to use invariant culture formatting
Diagram
flowchart LR
  A["Parameter Methods<br/>AddParameter, AddOrUpdateParameter<br/>AddQueryParameter, AddUrlSegment<br/>AddHeader, AddOrUpdateHeader"] -->|"Use ToStringValue()"| B["ToStringValue Extension<br/>IFormattable.ToString<br/>CultureInfo.InvariantCulture"]
  B -->|"Default Culture"| C["InvariantCulture<br/>Decimal: 1.234"]
  B -->|"Optional Override"| D["Custom Culture<br/>Decimal: 1,234"]
  E["ObjectParser.ParseValue<br/>Parameter.CreateParameter"] -->|"Use ToStringValue()"| B
Loading

Grey Divider

File Changes

1. src/RestSharp/Extensions/StringExtensions.cs ✨ Enhancement +3/-0

Add ToStringValue helper for invariant culture conversion

src/RestSharp/Extensions/StringExtensions.cs


2. src/RestSharp/Parameters/ObjectParser.cs 🐞 Bug fix +4/-1

Use invariant culture in ParseValue method

src/RestSharp/Parameters/ObjectParser.cs


3. src/RestSharp/Parameters/Parameter.cs 🐞 Bug fix +5/-4

Apply invariant culture to CreateParameter method

src/RestSharp/Parameters/Parameter.cs


View more (6)
4. src/RestSharp/Request/RestRequestExtensions.cs ✨ Enhancement +12/-6

Add culture parameter to AddParameter and AddOrUpdateParameter

src/RestSharp/Request/RestRequestExtensions.cs


5. src/RestSharp/Request/RestRequestExtensions.Headers.cs ✨ Enhancement +11/-4

Add culture parameter to header generic overloads

src/RestSharp/Request/RestRequestExtensions.Headers.cs


6. src/RestSharp/Request/RestRequestExtensions.Query.cs ✨ Enhancement +7/-2

Add culture parameter to AddQueryParameter generic overload

src/RestSharp/Request/RestRequestExtensions.Query.cs


7. src/RestSharp/Request/RestRequestExtensions.Url.cs ✨ Enhancement +7/-2

Add culture parameter to AddUrlSegment generic overload

src/RestSharp/Request/RestRequestExtensions.Url.cs


8. test/RestSharp.Tests/InvariantCultureTests.cs 🧪 Tests +98/-0

Add comprehensive tests for invariant culture formatting

test/RestSharp.Tests/InvariantCultureTests.cs


9. test/RestSharp.Tests/ObjectParserTests.cs 🧪 Tests +6/-5

Update tests to expect invariant culture formatted values

test/RestSharp.Tests/ObjectParserTests.cs


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Contributor

qodo-free-for-open-source-projects bot commented Feb 26, 2026

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Nullable struct becomes null 🐞 Bug ✓ Correctness
Description
ToStringValue(object?) returns null for null input, and generic where T : struct overloads box
Nullable<T> with no value to null. This can change query-string output from name= to name and
can throw ArgumentNullException in AddHeader<T>/AddOrUpdateHeader<T> when called with nullable
structs that have no value.
Code

src/RestSharp/Extensions/StringExtensions.cs[R152-153]

+    internal static string? ToStringValue(this object? value, CultureInfo? culture = null)
+        => value is IFormattable f ? f.ToString(null, culture ?? CultureInfo.InvariantCulture) : value?.ToString();
Evidence
The new helper returns null when the input object is null. Generic overloads like AddHeader<T>
call it and then immediately enforce non-null via Ensure.NotNull, so a boxed-null nullable will
now throw. For query-string building, RestSharp emits only the parameter name when the value is
null, so nullable-with-no-value now serializes differently than an empty string would.

src/RestSharp/Extensions/StringExtensions.cs[152-153]
src/RestSharp/Request/RestRequestExtensions.Headers.cs[54-56]
src/RestSharp/Ensure.cs[17-25]
src/RestSharp/BuildUriExtensions.cs[75-88]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Generic parameter/header overloads (`where T : struct`) now call `ToStringValue(object?)`. When `T` is `Nullable&lt;U&gt;` and the value has no value, boxing results in `null`, so `ToStringValue` returns `null`.

This changes behavior:
- **Headers**: `AddHeader&lt;T&gt;`/`AddOrUpdateHeader&lt;T&gt;` call `Ensure.NotNull(...)` and will now throw for `T = int?` etc when null.
- **Query-string**: RestSharp renders `null` values as just the parameter name (no `=`), changing output compared to empty-string values.

## Issue Context
The new invariant-culture behavior is correct for non-null values, but nullable structs are an important edge case because they are still `struct` and thus accepted by these overloads.

## Fix Focus Areas
- src/RestSharp/Extensions/StringExtensions.cs[152-153]
- src/RestSharp/Request/RestRequestExtensions.Headers.cs[54-57]
- src/RestSharp/Request/RestRequestExtensions.Headers.cs[76-78]
- src/RestSharp/Request/RestRequestExtensions.cs[58-61]
- src/RestSharp/Request/RestRequestExtensions.cs[80-82]
- src/RestSharp/Request/RestRequestExtensions.Query.cs[44-46]
- src/RestSharp/Request/RestRequestExtensions.Url.cs[44-46]
- src/RestSharp/BuildUriExtensions.cs[75-88]
- test/RestSharp.Tests/InvariantCultureTests.cs[1-98]

## Implementation guidance
1. Add a struct-specific helper (or update the call sites) to preserve nullable semantics:
  - If `T` is `Nullable&lt;U&gt;` and `value` is default (no value), return `string.Empty` (matching `Nullable&lt;T&gt;.ToString()` behavior).
  - Otherwise format via invariant culture (`IFormattable.ToString(null, ...)`).
2. Update generic overloads to call the struct-specific helper to avoid boxing-to-null.
3. Add tests:
  - `AddHeader(&quot;x&quot;, (int?)null)` should not throw and should set header value to empty string (or whatever behavior you decide, but make it consistent/documented).
  - `AddQueryParameter(&quot;x&quot;, (int?)null)` + `BuildUriString` should verify whether output is `x=` vs `x` according to the intended behavior (and lock it in with a test).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@cloudflare-workers-and-pages
Copy link

Deploying restsharp with  Cloudflare Pages  Cloudflare Pages

Latest commit: 6e7947e
Status: ✅  Deploy successful!
Preview URL: https://7739fd96.restsharp.pages.dev
Branch Preview URL: https://fix-invariant-culture-parame.restsharp.pages.dev

View logs

@sonarqubecloud
Copy link

Comment on lines +152 to +153
internal static string? ToStringValue(this object? value, CultureInfo? culture = null)
=> value is IFormattable f ? f.ToString(null, culture ?? CultureInfo.InvariantCulture) : value?.ToString();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Nullable struct becomes null 🐞 Bug ✓ Correctness

ToStringValue(object?) returns null for null input, and generic where T : struct overloads box
Nullable<T> with no value to null. This can change query-string output from name= to name and
can throw ArgumentNullException in AddHeader<T>/AddOrUpdateHeader<T> when called with nullable
structs that have no value.
Agent Prompt
## Issue description
Generic parameter/header overloads (`where T : struct`) now call `ToStringValue(object?)`. When `T` is `Nullable<U>` and the value has no value, boxing results in `null`, so `ToStringValue` returns `null`.

This changes behavior:
- **Headers**: `AddHeader<T>`/`AddOrUpdateHeader<T>` call `Ensure.NotNull(...)` and will now throw for `T = int?` etc when null.
- **Query-string**: RestSharp renders `null` values as just the parameter name (no `=`), changing output compared to empty-string values.

## Issue Context
The new invariant-culture behavior is correct for non-null values, but nullable structs are an important edge case because they are still `struct` and thus accepted by these overloads.

## Fix Focus Areas
- src/RestSharp/Extensions/StringExtensions.cs[152-153]
- src/RestSharp/Request/RestRequestExtensions.Headers.cs[54-57]
- src/RestSharp/Request/RestRequestExtensions.Headers.cs[76-78]
- src/RestSharp/Request/RestRequestExtensions.cs[58-61]
- src/RestSharp/Request/RestRequestExtensions.cs[80-82]
- src/RestSharp/Request/RestRequestExtensions.Query.cs[44-46]
- src/RestSharp/Request/RestRequestExtensions.Url.cs[44-46]
- src/RestSharp/BuildUriExtensions.cs[75-88]
- test/RestSharp.Tests/InvariantCultureTests.cs[1-98]

## Implementation guidance
1. Add a struct-specific helper (or update the call sites) to preserve nullable semantics:
   - If `T` is `Nullable<U>` and `value` is default (no value), return `string.Empty` (matching `Nullable<T>.ToString()` behavior).
   - Otherwise format via invariant culture (`IFormattable.ToString(null, ...)`).
2. Update generic overloads to call the struct-specific helper to avoid boxing-to-null.
3. Add tests:
   - `AddHeader("x", (int?)null)` should not throw and should set header value to empty string (or whatever behavior you decide, but make it consistent/documented).
   - `AddQueryParameter("x", (int?)null)` + `BuildUriString` should verify whether output is `x=` vs `x` according to the intended behavior (and lock it in with a test).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a false positive. The where T : struct constraint specifically excludes Nullable<T> — the C# compiler rejects it:

error CS0453: The type 'int?' must be a non-nullable value type in order to use it
as parameter 'T' in the generic type or method 'Test<T>(T)'

So AddParameter<int?>("name", null) won't compile. There's no way to pass a null value through these generic overloads.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question:

This is a false positive. The where T : struct constraint specifically excludes Nullable<T> — the C# compiler rejects it: error CS0453: The type 'int?' must be a non-nullable value type in order to use it as parameter 'T' in the generic type or method 'Test<T>(T)' So AddParameter<int?>(name, null) won't compile. There's no way to pass a null value through these generic overloads.

Answer:

Add a C#-specific guardrail to the improve prompt so suggestions don’t assume where T : struct accepts Nullable<T> (and to require compile-checked reasoning before proposing changes).

In your .pr_agent.toml (or as an inline flag on /improve), set:

[pr_code_suggestions]
extra_instructions="""\
For C#:
- Do NOT assume `where T : struct` permits `Nullable<T>` (`int?`, etc.). The compiler rejects `T = Nullable<U>` under this constraint (CS0453).
- Before flagging a bug involving generic constraints, explicitly verify (in the explanation) whether the code can compile and the call site is legal.
- If the scenario requires `T = int?` through a `where T : struct` API, treat it as invalid/unreachable unless there is evidence of a different overload or non-generic path.
"""

If you only want this behavior for one PR run, use:

/improve --pr_code_suggestions.extra_instructions="For C#: do not claim `where T : struct` accepts `Nullable<T>` (CS0453); verify compilation/legal call sites before suggesting fixes."

Relevant Sources:

@github-actions
Copy link

Test Results

   42 files     42 suites   17m 55s ⏱️
  514 tests   513 ✅ 0 💤 1 ❌
3 593 runs  3 592 ✅ 0 💤 1 ❌

For more details on these failures, see this check.

Results for commit 6e7947e.

@alexeyzimarev alexeyzimarev merged commit acdf9af into dev Feb 26, 2026
11 of 12 checks passed
@alexeyzimarev alexeyzimarev deleted the fix/invariant-culture-parameter-formatting branch February 26, 2026 18:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RestRequestExtensions.AddParameter and AddOrUpdateParameter should use CultureInfo.InvariantCulture for IFormattable types

1 participant