From 668dbfdbdcc3f4235e73148bc573fa400457a232 Mon Sep 17 00:00:00 2001 From: Morten Jansrud Date: Thu, 30 Apr 2026 07:46:57 +0200 Subject: [PATCH 1/4] [csharp][restsharp] add throwOnAnyError option to surface client errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit By default, RestSharp swallows deserialization and transport exceptions into RestResponse.ErrorException, and the generated ToApiResponse in this template only carries ErrorText — the actual exception is dropped. Combined with a generated GetXxxAsync that returns Data directly, callers silently receive null on any deserialization failure (e.g. a required property missing in the upstream JSON). The error never reaches application logs or APM. Add an opt-in `throwOnAnyError` switch (default false, restsharp library only) that sets `ThrowOnAnyError = true` on RestClientOptions, making RestSharp rethrow the original ApiException(500, ...) that the generated deserializer already throws. The exception then propagates to the caller and into normal application error handling. Default kept off to preserve backwards compatibility — opt in when you want bugs to surface instead of silently producing null/[]. --- docs/generators/csharp.md | 1 + .../codegen/languages/CSharpClientCodegen.java | 11 +++++++++++ .../src/main/resources/csharp/ApiClient.mustache | 3 ++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/generators/csharp.md b/docs/generators/csharp.md index b490fa6fe1d3..28b79a05965c 100644 --- a/docs/generators/csharp.md +++ b/docs/generators/csharp.md @@ -51,6 +51,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |useDateTimeForDate|Use DateTime to model date properties even if DateOnly supported. (.net 6.0+ only)| |false| |useDateTimeOffset|Use DateTimeOffset to model date-time properties| |false| |useIntForTimeout|Use int for Timeout (fall back to v7.9.0 templates). This option (for restsharp only) will be deprecated so please migrated to TimeSpan instead.| |false| +|throwOnAnyError|Configure RestSharp to rethrow deserialization and transport errors instead of swallowing them into RestResponse.ErrorException (which the default ToApiResponse<T> discards as null Data). Recommended for production use to surface bugs that would otherwise be invisible. (restsharp only)| |false| |useOneOfDiscriminatorLookup|Use the discriminator's mapping in oneOf to speed up the model lookup. IMPORTANT: Validation (e.g. one and only one match in oneOf's schemas) will be skipped.| |false| |useSourceGeneration|Use source generation where available (only `generichost` library supports this option).| |false| |useVirtualForHooks|Generate code that exposes public virtual hooks on ApiClient to customize low-level HTTP requests (only `restsharp`. `httpclient` libraries support this option).| |false| diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CSharpClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CSharpClientCodegen.java index 466167194012..72c1fdf0921c 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CSharpClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CSharpClientCodegen.java @@ -122,6 +122,7 @@ public class CSharpClientCodegen extends AbstractCSharpCodegen { protected boolean supportsFileParameters = Boolean.TRUE; protected boolean supportsDateOnly = Boolean.FALSE; protected boolean useIntForTimeout = Boolean.FALSE; + protected boolean throwOnAnyError = Boolean.FALSE; @Setter protected boolean validatable = Boolean.TRUE; @Setter protected boolean equatable = Boolean.FALSE; @@ -132,6 +133,7 @@ public class CSharpClientCodegen extends AbstractCSharpCodegen { private static final String OPERATION_PARAMETER_SORTING_KEY = "operationParameterSorting"; private static final String MODEL_PROPERTY_SORTING_KEY = "modelPropertySorting"; private static final String USE_INT_FOR_TIMEOUT = "useIntForTimeout"; + private static final String THROW_ON_ANY_ERROR = "throwOnAnyError"; enum SortingMethod { DEFAULT, @@ -249,6 +251,10 @@ public CSharpClientCodegen() { "Use int for Timeout (fall back to v7.9.0 templates). This option (for restsharp only) will be deprecated so please migrated to TimeSpan instead.", String.valueOf(this.useIntForTimeout)); + addSwitch(CSharpClientCodegen.THROW_ON_ANY_ERROR, + "Configure RestSharp to rethrow deserialization and transport errors instead of swallowing them into RestResponse.ErrorException (which the default ToApiResponse discards as null Data). Recommended for production use to surface bugs that would otherwise be invisible. (restsharp only)", + this.throwOnAnyError); + CliOption framework = new CliOption( CodegenConstants.DOTNET_FRAMEWORK, CodegenConstants.DOTNET_FRAMEWORK_DESC @@ -871,6 +877,7 @@ public void processOpts() { syncBooleanProperty(additionalProperties, "useSourceGeneration", this::setUseSourceGeneration, this.useSourceGeneration); syncBooleanProperty(additionalProperties, "supportsDateOnly", this::setSupportsDateOnly, this.supportsDateOnly); syncBooleanProperty(additionalProperties, "useIntForTimeout", this::setUseIntForTimeout, this.useIntForTimeout); + syncBooleanProperty(additionalProperties, "throwOnAnyError", this::setThrowOnAnyError, this.throwOnAnyError); final String testPackageName = testPackageName(); String packageFolder = sourceFolder + File.separator + packageName; @@ -1244,6 +1251,10 @@ public void setUseIntForTimeout(Boolean useIntForTimeout) { this.useIntForTimeout = useIntForTimeout; } + public void setThrowOnAnyError(Boolean throwOnAnyError) { + this.throwOnAnyError = throwOnAnyError; + } + public void setSupportsRetry(Boolean supportsRetry) { this.supportsRetry = supportsRetry; } diff --git a/modules/openapi-generator/src/main/resources/csharp/ApiClient.mustache b/modules/openapi-generator/src/main/resources/csharp/ApiClient.mustache index 3e774675356f..e00e356d14ea 100644 --- a/modules/openapi-generator/src/main/resources/csharp/ApiClient.mustache +++ b/modules/openapi-generator/src/main/resources/csharp/ApiClient.mustache @@ -467,7 +467,8 @@ namespace {{packageName}}.Client Proxy = configuration.Proxy, UserAgent = configuration.UserAgent, UseDefaultCredentials = configuration.UseDefaultCredentials, - RemoteCertificateValidationCallback = configuration.RemoteCertificateValidationCallback + RemoteCertificateValidationCallback = configuration.RemoteCertificateValidationCallback{{#throwOnAnyError}}, + ThrowOnAnyError = true{{/throwOnAnyError}} }; setOptions(clientOptions); From fe704b5c8c0abd0783ee54bd61ccc7d52a557f2f Mon Sep 17 00:00:00 2001 From: Morten Jansrud Date: Thu, 30 Apr 2026 09:01:24 +0200 Subject: [PATCH 2/4] docs: regenerate csharp.md to alphabetical order --- docs/generators/csharp.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/generators/csharp.md b/docs/generators/csharp.md index 28b79a05965c..eb6fcdf5df13 100644 --- a/docs/generators/csharp.md +++ b/docs/generators/csharp.md @@ -47,11 +47,11 @@ These options may be applied as additional-properties (cli) or configOptions (pl |returnICollection|Return ICollection<T> instead of the concrete type.| |false| |sourceFolder|source folder for generated code| |src| |targetFramework|The target .NET framework version. To target multiple frameworks, use `;` as the separator, e.g. `netstandard2.1;netcoreapp3.1`|
**netstandard1.3**
.NET Standard 1.3
**netstandard1.4**
.NET Standard 1.4
**netstandard1.5**
.NET Standard 1.5
**netstandard1.6**
.NET Standard 1.6
**netstandard2.0**
.NET Standard 2.0
**netstandard2.1**
.NET Standard 2.1
**net47**
.NET Framework 4.7
**net48**
.NET Framework 4.8
**net8.0**
.NET 8.0 (End of Support 10 November 2026)
**net9.0**
.NET 9.0 (End of Support 10 November 2026)
**net10.0**
.NET 10.0 (End of Support 14 November 2028)
|net10.0| +|throwOnAnyError|Configure RestSharp to rethrow deserialization and transport errors instead of swallowing them into RestResponse.ErrorException (which the default ToApiResponse<T> discards as null Data). Recommended for production use to surface bugs that would otherwise be invisible. (restsharp only)| |false| |useCollection|Deserialize array types to Collection<T> instead of List<T>.| |false| |useDateTimeForDate|Use DateTime to model date properties even if DateOnly supported. (.net 6.0+ only)| |false| |useDateTimeOffset|Use DateTimeOffset to model date-time properties| |false| |useIntForTimeout|Use int for Timeout (fall back to v7.9.0 templates). This option (for restsharp only) will be deprecated so please migrated to TimeSpan instead.| |false| -|throwOnAnyError|Configure RestSharp to rethrow deserialization and transport errors instead of swallowing them into RestResponse.ErrorException (which the default ToApiResponse<T> discards as null Data). Recommended for production use to surface bugs that would otherwise be invisible. (restsharp only)| |false| |useOneOfDiscriminatorLookup|Use the discriminator's mapping in oneOf to speed up the model lookup. IMPORTANT: Validation (e.g. one and only one match in oneOf's schemas) will be skipped.| |false| |useSourceGeneration|Use source generation where available (only `generichost` library supports this option).| |false| |useVirtualForHooks|Generate code that exposes public virtual hooks on ApiClient to customize low-level HTTP requests (only `restsharp`. `httpclient` libraries support this option).| |false| From c19a3f76bd0ceeaee9557e825db8118b82d7a534 Mon Sep 17 00:00:00 2001 From: Morten Jansrud Date: Thu, 30 Apr 2026 09:15:31 +0200 Subject: [PATCH 3/4] [csharp][restsharp] honour throwOnAnyError when retry policy is configured Polly's ExecuteAndCapture catches the rethrown ApiException, so without this change the option is silently neutralized whenever RetryConfiguration.RetryPolicy != null: the exception ends up in RestResponse.ErrorException, which ToApiResponse discards. When throwOnAnyError is enabled, rethrow PolicyResult.FinalException from DeserializeRestResponseFromPolicyAsync so the contract is consistent across both the no-retry and retry paths. Default-off branch is byte-identical to the previous output. --- .../src/main/resources/csharp/ApiClient.mustache | 7 ++++++- .../src/main/resources/csharp/ApiClient.v790.mustache | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/csharp/ApiClient.mustache b/modules/openapi-generator/src/main/resources/csharp/ApiClient.mustache index e00e356d14ea..72a8860f53bb 100644 --- a/modules/openapi-generator/src/main/resources/csharp/ApiClient.mustache +++ b/modules/openapi-generator/src/main/resources/csharp/ApiClient.mustache @@ -561,16 +561,21 @@ namespace {{packageName}}.Client private async Task> DeserializeRestResponseFromPolicyAsync(RestClient client, RestRequest request, PolicyResult policyResult, CancellationToken cancellationToken = default) { - if (policyResult.Outcome == OutcomeType.Successful) + if (policyResult.Outcome == OutcomeType.Successful) { return await client.Deserialize(policyResult.Result, cancellationToken).ConfigureAwait(false); } else { + {{#throwOnAnyError}} + throw policyResult.FinalException; + {{/throwOnAnyError}} + {{^throwOnAnyError}} return new RestResponse(request) { ErrorException = policyResult.FinalException }; + {{/throwOnAnyError}} } } diff --git a/modules/openapi-generator/src/main/resources/csharp/ApiClient.v790.mustache b/modules/openapi-generator/src/main/resources/csharp/ApiClient.v790.mustache index 682b762511b8..e2774473e14a 100644 --- a/modules/openapi-generator/src/main/resources/csharp/ApiClient.v790.mustache +++ b/modules/openapi-generator/src/main/resources/csharp/ApiClient.v790.mustache @@ -552,16 +552,21 @@ namespace {{packageName}}.Client private async Task> DeserializeRestResponseFromPolicyAsync(RestClient client, RestRequest request, PolicyResult policyResult, CancellationToken cancellationToken = default) { - if (policyResult.Outcome == OutcomeType.Successful) + if (policyResult.Outcome == OutcomeType.Successful) { return await client.Deserialize(policyResult.Result, cancellationToken); } else { + {{#throwOnAnyError}} + throw policyResult.FinalException; + {{/throwOnAnyError}} + {{^throwOnAnyError}} return new RestResponse(request) { ErrorException = policyResult.FinalException }; + {{/throwOnAnyError}} } } From 65b15cc99999c16ffe78b067faa495fbc669627a Mon Sep 17 00:00:00 2001 From: Morten Jansrud Date: Thu, 30 Apr 2026 10:05:30 +0200 Subject: [PATCH 4/4] [csharp][restsharp] guard throwOnAnyError against null FinalException Polly's PolicyResult.FinalException is null when Outcome is Failure but the failure type is ResultHandledByThisPolicy (e.g. a retry policy configured with .HandleResult(...) that gives up after N retries). Throwing it directly would NRE; fall back to InvalidOperationException so the option still surfaces an error. Also restores the trailing whitespace on the if line so existing restsharp samples don't all need regenerating. --- .../src/main/resources/csharp/ApiClient.mustache | 4 ++-- .../src/main/resources/csharp/ApiClient.v790.mustache | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/csharp/ApiClient.mustache b/modules/openapi-generator/src/main/resources/csharp/ApiClient.mustache index 72a8860f53bb..d1946b37a38f 100644 --- a/modules/openapi-generator/src/main/resources/csharp/ApiClient.mustache +++ b/modules/openapi-generator/src/main/resources/csharp/ApiClient.mustache @@ -561,14 +561,14 @@ namespace {{packageName}}.Client private async Task> DeserializeRestResponseFromPolicyAsync(RestClient client, RestRequest request, PolicyResult policyResult, CancellationToken cancellationToken = default) { - if (policyResult.Outcome == OutcomeType.Successful) + if (policyResult.Outcome == OutcomeType.Successful) { return await client.Deserialize(policyResult.Result, cancellationToken).ConfigureAwait(false); } else { {{#throwOnAnyError}} - throw policyResult.FinalException; + throw policyResult.FinalException ?? new InvalidOperationException("The retry policy failed without an exception."); {{/throwOnAnyError}} {{^throwOnAnyError}} return new RestResponse(request) diff --git a/modules/openapi-generator/src/main/resources/csharp/ApiClient.v790.mustache b/modules/openapi-generator/src/main/resources/csharp/ApiClient.v790.mustache index e2774473e14a..6eb294e1aec0 100644 --- a/modules/openapi-generator/src/main/resources/csharp/ApiClient.v790.mustache +++ b/modules/openapi-generator/src/main/resources/csharp/ApiClient.v790.mustache @@ -552,14 +552,14 @@ namespace {{packageName}}.Client private async Task> DeserializeRestResponseFromPolicyAsync(RestClient client, RestRequest request, PolicyResult policyResult, CancellationToken cancellationToken = default) { - if (policyResult.Outcome == OutcomeType.Successful) + if (policyResult.Outcome == OutcomeType.Successful) { return await client.Deserialize(policyResult.Result, cancellationToken); } else { {{#throwOnAnyError}} - throw policyResult.FinalException; + throw policyResult.FinalException ?? new InvalidOperationException("The retry policy failed without an exception."); {{/throwOnAnyError}} {{^throwOnAnyError}} return new RestResponse(request)