From 266c3444feb415d4ff8ccd8e3047eb94a8b7706e Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Tue, 31 Mar 2026 22:55:36 +0000 Subject: [PATCH 01/18] feat(csharp-sdk): support optional username/password in basic auth when configured in IR Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../src/root-client/RootClientGenerator.ts | 20 +- generators/csharp/sdk/versions.yml | 12 + ...e_errors_UnauthorizedRequestErrorBody.json | 13 + .../basic-auth-optional/.editorconfig | 35 + .../basic-auth-optional/.fern/metadata.json | 8 + .../.github/workflows/ci.yml | 52 + .../csharp-sdk/basic-auth-optional/.gitignore | 484 +++++++ seed/csharp-sdk/basic-auth-optional/README.md | 172 +++ .../SeedBasicAuthOptional.slnx | 4 + .../basic-auth-optional/reference.md | 97 ++ .../basic-auth-optional/snippet.json | 29 + .../src/SeedApi.DynamicSnippets/Example0.cs | 19 + .../src/SeedApi.DynamicSnippets/Example1.cs | 19 + .../src/SeedApi.DynamicSnippets/Example2.cs | 19 + .../src/SeedApi.DynamicSnippets/Example3.cs | 24 + .../src/SeedApi.DynamicSnippets/Example4.cs | 24 + .../src/SeedApi.DynamicSnippets/Example5.cs | 24 + .../src/SeedApi.DynamicSnippets/Example6.cs | 24 + .../SeedApi.DynamicSnippets.csproj | 13 + .../Core/HeadersBuilderTests.cs | 326 +++++ .../Core/Json/AdditionalPropertiesTests.cs | 365 ++++++ .../Core/Json/DateOnlyJsonTests.cs | 100 ++ .../Core/Json/DateTimeJsonTests.cs | 134 ++ .../Core/Json/JsonAccessAttributeTests.cs | 160 +++ .../Core/QueryStringBuilderTests.cs | 560 ++++++++ .../Core/QueryStringConverterTests.cs | 158 +++ .../Core/RawClientTests/MultipartFormTests.cs | 1121 +++++++++++++++++ .../RawClientTests/QueryParameterTests.cs | 108 ++ .../Core/RawClientTests/RetriesTests.cs | 406 ++++++ .../Core/WithRawResponseTests.cs | 269 ++++ .../SeedBasicAuthOptional.Test.Custom.props | 6 + .../SeedBasicAuthOptional.Test.csproj | 39 + .../SeedBasicAuthOptional.Test/TestClient.cs | 6 + .../Unit/MockServer/BaseMockServerTest.cs | 39 + .../BasicAuth/GetWithBasicAuthTest.cs | 50 + .../BasicAuth/PostWithBasicAuthTest.cs | 78 ++ .../Utils/AdditionalPropertiesComparer.cs | 219 ++++ .../Utils/JsonAssert.cs | 29 + .../Utils/JsonElementComparer.cs | 236 ++++ .../Utils/NUnitExtensions.cs | 32 + .../Utils/OneOfComparer.cs | 86 ++ .../Utils/OptionalComparer.cs | 104 ++ .../Utils/ReadOnlyMemoryComparer.cs | 87 ++ .../BasicAuth/BasicAuthClient.cs | 207 +++ .../BasicAuth/IBasicAuthClient.cs | 21 + .../SeedBasicAuthOptional/Core/ApiResponse.cs | 13 + .../SeedBasicAuthOptional/Core/BaseRequest.cs | 67 + .../Core/CollectionItemSerializer.cs | 91 ++ .../SeedBasicAuthOptional/Core/Constants.cs | 7 + .../Core/DateOnlyConverter.cs | 747 +++++++++++ .../Core/DateTimeSerializer.cs | 40 + .../Core/EmptyRequest.cs | 11 + .../Core/EncodingCache.cs | 11 + .../SeedBasicAuthOptional/Core/Extensions.cs | 55 + .../Core/FormUrlEncoder.cs | 33 + .../SeedBasicAuthOptional/Core/HeaderValue.cs | 52 + .../src/SeedBasicAuthOptional/Core/Headers.cs | 28 + .../Core/HeadersBuilder.cs | 197 +++ .../Core/HttpContentExtensions.cs | 20 + .../Core/HttpMethodExtensions.cs | 8 + .../Core/IIsRetryableContent.cs | 6 + .../Core/IRequestOptions.cs | 83 ++ .../Core/JsonAccessAttribute.cs | 15 + .../Core/JsonConfiguration.cs | 275 ++++ .../SeedBasicAuthOptional/Core/JsonRequest.cs | 36 + .../Core/MultipartFormRequest.cs | 294 +++++ .../Core/NullableAttribute.cs | 18 + .../Core/OneOfSerializer.cs | 145 +++ .../SeedBasicAuthOptional/Core/Optional.cs | 474 +++++++ .../Core/OptionalAttribute.cs | 17 + .../Core/Public/AdditionalProperties.cs | 353 ++++++ .../Core/Public/ClientOptions.cs | 84 ++ .../Core/Public/FileParameter.cs | 63 + .../Core/Public/RawResponse.cs | 24 + .../Core/Public/RequestOptions.cs | 86 ++ .../SeedBasicAuthOptionalApiException.cs | 22 + .../Public/SeedBasicAuthOptionalException.cs | 7 + .../Core/Public/Version.cs | 7 + .../Core/Public/WithRawResponse.cs | 18 + .../Core/Public/WithRawResponseTask.cs | 144 +++ .../Core/QueryStringBuilder.cs | 469 +++++++ .../Core/QueryStringConverter.cs | 259 ++++ .../SeedBasicAuthOptional/Core/RawClient.cs | 344 +++++ .../SeedBasicAuthOptional/Core/RawResponse.cs | 24 + .../Core/ResponseHeaders.cs | 108 ++ .../Core/StreamRequest.cs | 29 + .../SeedBasicAuthOptional/Core/StringEnum.cs | 6 + .../Core/StringEnumExtensions.cs | 6 + .../Core/ValueConvert.cs | 114 ++ .../Errors/Exceptions/BadRequest.cs | 7 + .../Errors/Exceptions/UnauthorizedRequest.cs | 14 + .../Types/UnauthorizedRequestErrorBody.cs | 28 + .../ISeedBasicAuthOptionalClient.cs | 6 + .../SeedBasicAuthOptional.Custom.props | 20 + .../SeedBasicAuthOptional.csproj | 63 + .../SeedBasicAuthOptionalClient.cs | 40 + .../basic-auth-optional/definition/api.yml | 12 + .../definition/basic-auth.yml | 39 + .../basic-auth-optional/definition/errors.yml | 11 + .../apis/basic-auth-optional/generators.yml | 22 + 100 files changed, 11204 insertions(+), 6 deletions(-) create mode 100644 packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/basic-auth-optional/type_errors_UnauthorizedRequestErrorBody.json create mode 100644 seed/csharp-sdk/basic-auth-optional/.editorconfig create mode 100644 seed/csharp-sdk/basic-auth-optional/.fern/metadata.json create mode 100644 seed/csharp-sdk/basic-auth-optional/.github/workflows/ci.yml create mode 100644 seed/csharp-sdk/basic-auth-optional/.gitignore create mode 100644 seed/csharp-sdk/basic-auth-optional/README.md create mode 100644 seed/csharp-sdk/basic-auth-optional/SeedBasicAuthOptional.slnx create mode 100644 seed/csharp-sdk/basic-auth-optional/reference.md create mode 100644 seed/csharp-sdk/basic-auth-optional/snippet.json create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example0.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example1.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example2.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example3.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example4.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example5.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example6.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/SeedApi.DynamicSnippets.csproj create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/HeadersBuilderTests.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/Json/AdditionalPropertiesTests.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/Json/DateOnlyJsonTests.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/Json/DateTimeJsonTests.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/Json/JsonAccessAttributeTests.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/QueryStringBuilderTests.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/QueryStringConverterTests.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/RawClientTests/MultipartFormTests.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/RawClientTests/QueryParameterTests.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/RawClientTests/RetriesTests.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/WithRawResponseTests.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/SeedBasicAuthOptional.Test.Custom.props create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/SeedBasicAuthOptional.Test.csproj create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/TestClient.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Unit/MockServer/BaseMockServerTest.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Unit/MockServer/BasicAuth/GetWithBasicAuthTest.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Unit/MockServer/BasicAuth/PostWithBasicAuthTest.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/AdditionalPropertiesComparer.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/JsonAssert.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/JsonElementComparer.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/NUnitExtensions.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/OneOfComparer.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/OptionalComparer.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/ReadOnlyMemoryComparer.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/BasicAuth/BasicAuthClient.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/BasicAuth/IBasicAuthClient.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/ApiResponse.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/BaseRequest.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/CollectionItemSerializer.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Constants.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/DateOnlyConverter.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/DateTimeSerializer.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/EmptyRequest.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/EncodingCache.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Extensions.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/FormUrlEncoder.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/HeaderValue.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Headers.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/HeadersBuilder.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/HttpContentExtensions.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/HttpMethodExtensions.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/IIsRetryableContent.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/IRequestOptions.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/JsonAccessAttribute.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/JsonConfiguration.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/JsonRequest.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/MultipartFormRequest.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/NullableAttribute.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/OneOfSerializer.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Optional.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/OptionalAttribute.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/AdditionalProperties.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/ClientOptions.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/FileParameter.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/RawResponse.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/RequestOptions.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/SeedBasicAuthOptionalApiException.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/SeedBasicAuthOptionalException.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/Version.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/WithRawResponse.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/WithRawResponseTask.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/QueryStringBuilder.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/QueryStringConverter.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/RawClient.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/RawResponse.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/ResponseHeaders.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/StreamRequest.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/StringEnum.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/StringEnumExtensions.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/ValueConvert.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Errors/Exceptions/BadRequest.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Errors/Exceptions/UnauthorizedRequest.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Errors/Types/UnauthorizedRequestErrorBody.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/ISeedBasicAuthOptionalClient.cs create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/SeedBasicAuthOptional.Custom.props create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/SeedBasicAuthOptional.csproj create mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/SeedBasicAuthOptionalClient.cs create mode 100644 test-definitions/fern/apis/basic-auth-optional/definition/api.yml create mode 100644 test-definitions/fern/apis/basic-auth-optional/definition/basic-auth.yml create mode 100644 test-definitions/fern/apis/basic-auth-optional/definition/errors.yml create mode 100644 test-definitions/fern/apis/basic-auth-optional/generators.yml diff --git a/generators/csharp/sdk/src/root-client/RootClientGenerator.ts b/generators/csharp/sdk/src/root-client/RootClientGenerator.ts index 12e7511588ff..6e9590ba7846 100644 --- a/generators/csharp/sdk/src/root-client/RootClientGenerator.ts +++ b/generators/csharp/sdk/src/root-client/RootClientGenerator.ts @@ -466,16 +466,24 @@ export class RootClientGenerator extends FileGenerator 1) { const controlFlowKeyword = i === 0 ? "if" : "else if"; - innerWriter.controlFlow( - controlFlowKeyword, - this.csharp.codeblock(`${usernameAccess} != null && ${passwordAccess} != null`) + innerWriter.controlFlow(controlFlowKeyword, this.csharp.codeblock(condition)); + } + if (eitherOmitted) { + innerWriter.writeTextStatement( + `clientOptionsWithAuth.Headers["Authorization"] = $"Basic {Convert.ToBase64String(global::System.Text.Encoding.UTF8.GetBytes($"{${usernameAccess} ?? ""}:{${passwordAccess} ?? ""}"))}"` + ); + } else { + innerWriter.writeTextStatement( + `clientOptionsWithAuth.Headers["Authorization"] = $"Basic {Convert.ToBase64String(global::System.Text.Encoding.UTF8.GetBytes($"{${usernameAccess}}:{${passwordAccess}}"))}"` ); } - innerWriter.writeTextStatement( - `clientOptionsWithAuth.Headers["Authorization"] = $"Basic {Convert.ToBase64String(global::System.Text.Encoding.UTF8.GetBytes($"{${usernameAccess}}:{${passwordAccess}}"))}"` - ); if (isAuthOptional || basicSchemes.length > 1) { innerWriter.endControlFlow(); } diff --git a/generators/csharp/sdk/versions.yml b/generators/csharp/sdk/versions.yml index 3da02cfc0193..f8a34f6076fd 100644 --- a/generators/csharp/sdk/versions.yml +++ b/generators/csharp/sdk/versions.yml @@ -1,4 +1,16 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 2.55.3 + changelogEntry: + - summary: | + Support optional username and password in basic auth. The SDK now accepts + username-only, password-only, or both credentials. Missing fields are treated + as empty strings (e.g., username-only encodes `username:`, password-only + encodes `:password`). When neither is provided, the Authorization header is + omitted entirely. + type: feat + createdAt: "2026-03-31" + irVersion: 65 + - version: 2.55.2 changelogEntry: - summary: | diff --git a/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/basic-auth-optional/type_errors_UnauthorizedRequestErrorBody.json b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/basic-auth-optional/type_errors_UnauthorizedRequestErrorBody.json new file mode 100644 index 000000000000..f50ccac10d76 --- /dev/null +++ b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/basic-auth-optional/type_errors_UnauthorizedRequestErrorBody.json @@ -0,0 +1,13 @@ +{ + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "additionalProperties": false, + "definitions": {} +} \ No newline at end of file diff --git a/seed/csharp-sdk/basic-auth-optional/.editorconfig b/seed/csharp-sdk/basic-auth-optional/.editorconfig new file mode 100644 index 000000000000..1e7a0adbac80 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/.editorconfig @@ -0,0 +1,35 @@ +root = true + +[*.cs] +resharper_arrange_object_creation_when_type_evident_highlighting = hint +resharper_auto_property_can_be_made_get_only_global_highlighting = hint +resharper_check_namespace_highlighting = hint +resharper_class_never_instantiated_global_highlighting = hint +resharper_class_never_instantiated_local_highlighting = hint +resharper_collection_never_updated_global_highlighting = hint +resharper_convert_type_check_pattern_to_null_check_highlighting = hint +resharper_inconsistent_naming_highlighting = hint +resharper_member_can_be_private_global_highlighting = hint +resharper_member_hides_static_from_outer_class_highlighting = hint +resharper_not_accessed_field_local_highlighting = hint +resharper_nullable_warning_suppression_is_used_highlighting = suggestion +resharper_partial_type_with_single_part_highlighting = hint +resharper_prefer_concrete_value_over_default_highlighting = none +resharper_private_field_can_be_converted_to_local_variable_highlighting = hint +resharper_property_can_be_made_init_only_global_highlighting = hint +resharper_property_can_be_made_init_only_local_highlighting = hint +resharper_redundant_name_qualifier_highlighting = none +resharper_redundant_using_directive_highlighting = hint +resharper_replace_slice_with_range_indexer_highlighting = none +resharper_unused_auto_property_accessor_global_highlighting = hint +resharper_unused_auto_property_accessor_local_highlighting = hint +resharper_unused_member_global_highlighting = hint +resharper_unused_type_global_highlighting = hint +resharper_use_string_interpolation_highlighting = hint +dotnet_diagnostic.CS1591.severity = suggestion + +[src/**/Types/*.cs] +resharper_check_namespace_highlighting = none + +[src/**/Core/Public/*.cs] +resharper_check_namespace_highlighting = none \ No newline at end of file diff --git a/seed/csharp-sdk/basic-auth-optional/.fern/metadata.json b/seed/csharp-sdk/basic-auth-optional/.fern/metadata.json new file mode 100644 index 000000000000..91d0855bee07 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/.fern/metadata.json @@ -0,0 +1,8 @@ +{ + "cliVersion": "DUMMY", + "generatorName": "fernapi/fern-csharp-sdk", + "generatorVersion": "latest", + "generatorConfig": {}, + "originGitCommit": "DUMMY", + "sdkVersion": "0.0.1" +} \ No newline at end of file diff --git a/seed/csharp-sdk/basic-auth-optional/.github/workflows/ci.yml b/seed/csharp-sdk/basic-auth-optional/.github/workflows/ci.yml new file mode 100644 index 000000000000..5a0b0300d85c --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/.github/workflows/ci.yml @@ -0,0 +1,52 @@ +name: ci + +on: [push] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +env: + DOTNET_NOLOGO: true + +jobs: + ci: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v6 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.x + + - name: Install tools + run: dotnet tool restore + + - name: Restore dependencies + run: dotnet restore src/SeedBasicAuthOptional/SeedBasicAuthOptional.csproj + + - name: Build + run: dotnet build src/SeedBasicAuthOptional/SeedBasicAuthOptional.csproj --no-restore -c Release + + - name: Restore test dependencies + run: dotnet restore src/SeedBasicAuthOptional.Test/SeedBasicAuthOptional.Test.csproj + + - name: Build tests + run: dotnet build src/SeedBasicAuthOptional.Test/SeedBasicAuthOptional.Test.csproj --no-restore -c Release + + - name: Test + run: dotnet test src/SeedBasicAuthOptional.Test/SeedBasicAuthOptional.Test.csproj --no-restore --no-build -c Release + + - name: Pack + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + run: dotnet pack src/SeedBasicAuthOptional/SeedBasicAuthOptional.csproj --no-build --no-restore -c Release + + - name: Publish to NuGet.org + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + env: + NUGET_API_KEY: ${{ secrets.NUGET_API_TOKEN }} + run: dotnet nuget push src/SeedBasicAuthOptional/bin/Release/*.nupkg --api-key $NUGET_API_KEY --source "nuget.org" + diff --git a/seed/csharp-sdk/basic-auth-optional/.gitignore b/seed/csharp-sdk/basic-auth-optional/.gitignore new file mode 100644 index 000000000000..11014f2b33d7 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/.gitignore @@ -0,0 +1,484 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +## This is based on `dotnet new gitignore` and customized by Fern + +# dotenv files +.env + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +# [Rr]elease/ (Ignored by Fern) +# [Rr]eleases/ (Ignored by Fern) +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +# [Ll]og/ (Ignored by Fern) +# [Ll]ogs/ (Ignored by Fern) + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +.idea + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Vim temporary swap files +*.swp diff --git a/seed/csharp-sdk/basic-auth-optional/README.md b/seed/csharp-sdk/basic-auth-optional/README.md new file mode 100644 index 000000000000..68e678269f42 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/README.md @@ -0,0 +1,172 @@ +# Seed C# Library + +[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=Seed%2FC%23) +[![nuget shield](https://img.shields.io/nuget/v/Fernbasic-auth-optional)](https://nuget.org/packages/Fernbasic-auth-optional) + +The Seed C# library provides convenient access to the Seed APIs from C#. + +## Table of Contents + +- [Requirements](#requirements) +- [Installation](#installation) +- [Reference](#reference) +- [Usage](#usage) +- [Exception Handling](#exception-handling) +- [Advanced](#advanced) + - [Retries](#retries) + - [Timeouts](#timeouts) + - [Raw Response](#raw-response) + - [Additional Headers](#additional-headers) + - [Additional Query Parameters](#additional-query-parameters) +- [Contributing](#contributing) + +## Requirements + +This SDK requires: + +## Installation + +```sh +dotnet add package Fernbasic-auth-optional +``` + +## Reference + +A full reference for this library is available [here](./reference.md). + +## Usage + +Instantiate and use the client with the following: + +```csharp +using SeedBasicAuthOptional; + +var client = new SeedBasicAuthOptionalClient("USERNAME", "PASSWORD"); +await client.BasicAuth.PostWithBasicAuthAsync( + new Dictionary() { { "key", "value" } } +); +``` + +## Exception Handling + +When the API returns a non-success status code (4xx or 5xx response), a subclass of the following error +will be thrown. + +```csharp +using SeedBasicAuthOptional; + +try { + var response = await client.BasicAuth.PostWithBasicAuthAsync(...); +} catch (SeedBasicAuthOptionalApiException e) { + System.Console.WriteLine(e.Body); + System.Console.WriteLine(e.StatusCode); +} +``` + +## Advanced + +### Retries + +The SDK is instrumented with automatic retries with exponential backoff. A request will be retried as long +as the request is deemed retryable and the number of retry attempts has not grown larger than the configured +retry limit (default: 2). + +A request is deemed retryable when any of the following HTTP status codes is returned: + +- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout) +- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests) +- [5XX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500) (Internal Server Errors) + +Use the `MaxRetries` request option to configure this behavior. + +```csharp +var response = await client.BasicAuth.PostWithBasicAuthAsync( + ..., + new RequestOptions { + MaxRetries: 0 // Override MaxRetries at the request level + } +); +``` + +### Timeouts + +The SDK defaults to a 30 second timeout. Use the `Timeout` option to configure this behavior. + +```csharp +var response = await client.BasicAuth.PostWithBasicAuthAsync( + ..., + new RequestOptions { + Timeout: TimeSpan.FromSeconds(3) // Override timeout to 3s + } +); +``` + +### Raw Response + +Access raw HTTP response data (status code, headers, URL) alongside parsed response data using the `.WithRawResponse()` method. + +```csharp +using SeedBasicAuthOptional; + +// Access raw response data (status code, headers, etc.) alongside the parsed response +var result = await client.BasicAuth.PostWithBasicAuthAsync(...).WithRawResponse(); + +// Access the parsed data +var data = result.Data; + +// Access raw response metadata +var statusCode = result.RawResponse.StatusCode; +var headers = result.RawResponse.Headers; +var url = result.RawResponse.Url; + +// Access specific headers (case-insensitive) +if (headers.TryGetValue("X-Request-Id", out var requestId)) +{ + System.Console.WriteLine($"Request ID: {requestId}"); +} + +// For the default behavior, simply await without .WithRawResponse() +var data = await client.BasicAuth.PostWithBasicAuthAsync(...); +``` + +### Additional Headers + +If you would like to send additional headers as part of the request, use the `AdditionalHeaders` request option. + +```csharp +var response = await client.BasicAuth.PostWithBasicAuthAsync( + ..., + new RequestOptions { + AdditionalHeaders = new Dictionary + { + { "X-Custom-Header", "custom-value" } + } + } +); +``` + +### Additional Query Parameters + +If you would like to send additional query parameters as part of the request, use the `AdditionalQueryParameters` request option. + +```csharp +var response = await client.BasicAuth.PostWithBasicAuthAsync( + ..., + new RequestOptions { + AdditionalQueryParameters = new Dictionary + { + { "custom_param", "custom-value" } + } + } +); +``` + +## Contributing + +While we value open-source contributions to this SDK, this library is generated programmatically. +Additions made directly to this library would have to be moved over to our generation code, +otherwise they would be overwritten upon the next generated release. Feel free to open a PR as +a proof of concept, but know that we will not be able to merge it as-is. We suggest opening +an issue first to discuss with us! + +On the other hand, contributions to the README are always very welcome! diff --git a/seed/csharp-sdk/basic-auth-optional/SeedBasicAuthOptional.slnx b/seed/csharp-sdk/basic-auth-optional/SeedBasicAuthOptional.slnx new file mode 100644 index 000000000000..9870a035ef0a --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/SeedBasicAuthOptional.slnx @@ -0,0 +1,4 @@ + + + + diff --git a/seed/csharp-sdk/basic-auth-optional/reference.md b/seed/csharp-sdk/basic-auth-optional/reference.md new file mode 100644 index 000000000000..3109a2c4b705 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/reference.md @@ -0,0 +1,97 @@ +# Reference +## BasicAuth +
client.BasicAuth.GetWithBasicAuthAsync() -> WithRawResponseTask<bool> +
+
+ +#### 📝 Description + +
+
+ +
+
+ +GET request with basic auth scheme +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.BasicAuth.GetWithBasicAuthAsync(); +``` +
+
+
+
+ + +
+
+
+ +
client.BasicAuth.PostWithBasicAuthAsync(object { ... }) -> WithRawResponseTask<bool> +
+
+ +#### 📝 Description + +
+
+ +
+
+ +POST request with basic auth scheme +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.BasicAuth.PostWithBasicAuthAsync( + new Dictionary() { { "key", "value" } } +); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `object` + +
+
+
+
+ + +
+
+
+ diff --git a/seed/csharp-sdk/basic-auth-optional/snippet.json b/seed/csharp-sdk/basic-auth-optional/snippet.json new file mode 100644 index 000000000000..3c004d73f893 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/snippet.json @@ -0,0 +1,29 @@ +{ + "types": {}, + "endpoints": [ + { + "example_identifier": null, + "id": { + "path": "/basic-auth", + "method": "GET", + "identifier_override": "endpoint_basic-auth.getWithBasicAuth" + }, + "snippet": { + "type": "csharp", + "client": "using SeedBasicAuthOptional;\n\nvar client = new SeedBasicAuthOptionalClient(\"USERNAME\", \"PASSWORD\");\nawait client.BasicAuth.GetWithBasicAuthAsync();\n" + } + }, + { + "example_identifier": null, + "id": { + "path": "/basic-auth", + "method": "POST", + "identifier_override": "endpoint_basic-auth.postWithBasicAuth" + }, + "snippet": { + "type": "csharp", + "client": "using SeedBasicAuthOptional;\n\nvar client = new SeedBasicAuthOptionalClient(\"USERNAME\", \"PASSWORD\");\nawait client.BasicAuth.PostWithBasicAuthAsync(\n new Dictionary() { { \"key\", \"value\" } }\n);\n" + } + } + ] +} \ No newline at end of file diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example0.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example0.cs new file mode 100644 index 000000000000..0a6d097846c7 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example0.cs @@ -0,0 +1,19 @@ +using SeedBasicAuthOptional; + +namespace Usage; + +public class Example0 +{ + public async Task Do() { + var client = new SeedBasicAuthOptionalClient( + username: "", + password: "", + clientOptions: new ClientOptions { + BaseUrl = "https://api.fern.com" + } + ); + + await client.BasicAuth.GetWithBasicAuthAsync(); + } + +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example1.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example1.cs new file mode 100644 index 000000000000..2b220847e903 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example1.cs @@ -0,0 +1,19 @@ +using SeedBasicAuthOptional; + +namespace Usage; + +public class Example1 +{ + public async Task Do() { + var client = new SeedBasicAuthOptionalClient( + username: "", + password: "", + clientOptions: new ClientOptions { + BaseUrl = "https://api.fern.com" + } + ); + + await client.BasicAuth.GetWithBasicAuthAsync(); + } + +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example2.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example2.cs new file mode 100644 index 000000000000..de30b520c866 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example2.cs @@ -0,0 +1,19 @@ +using SeedBasicAuthOptional; + +namespace Usage; + +public class Example2 +{ + public async Task Do() { + var client = new SeedBasicAuthOptionalClient( + username: "", + password: "", + clientOptions: new ClientOptions { + BaseUrl = "https://api.fern.com" + } + ); + + await client.BasicAuth.GetWithBasicAuthAsync(); + } + +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example3.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example3.cs new file mode 100644 index 000000000000..1994ca07d935 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example3.cs @@ -0,0 +1,24 @@ +using SeedBasicAuthOptional; + +namespace Usage; + +public class Example3 +{ + public async Task Do() { + var client = new SeedBasicAuthOptionalClient( + username: "", + password: "", + clientOptions: new ClientOptions { + BaseUrl = "https://api.fern.com" + } + ); + + await client.BasicAuth.PostWithBasicAuthAsync( + new Dictionary() + { + ["key"] = "value", + } + ); + } + +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example4.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example4.cs new file mode 100644 index 000000000000..2b5d846975d2 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example4.cs @@ -0,0 +1,24 @@ +using SeedBasicAuthOptional; + +namespace Usage; + +public class Example4 +{ + public async Task Do() { + var client = new SeedBasicAuthOptionalClient( + username: "", + password: "", + clientOptions: new ClientOptions { + BaseUrl = "https://api.fern.com" + } + ); + + await client.BasicAuth.PostWithBasicAuthAsync( + new Dictionary() + { + ["key"] = "value", + } + ); + } + +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example5.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example5.cs new file mode 100644 index 000000000000..fecaff239a15 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example5.cs @@ -0,0 +1,24 @@ +using SeedBasicAuthOptional; + +namespace Usage; + +public class Example5 +{ + public async Task Do() { + var client = new SeedBasicAuthOptionalClient( + username: "", + password: "", + clientOptions: new ClientOptions { + BaseUrl = "https://api.fern.com" + } + ); + + await client.BasicAuth.PostWithBasicAuthAsync( + new Dictionary() + { + ["key"] = "value", + } + ); + } + +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example6.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example6.cs new file mode 100644 index 000000000000..52319b55bf66 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example6.cs @@ -0,0 +1,24 @@ +using SeedBasicAuthOptional; + +namespace Usage; + +public class Example6 +{ + public async Task Do() { + var client = new SeedBasicAuthOptionalClient( + username: "", + password: "", + clientOptions: new ClientOptions { + BaseUrl = "https://api.fern.com" + } + ); + + await client.BasicAuth.PostWithBasicAuthAsync( + new Dictionary() + { + ["key"] = "value", + } + ); + } + +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/SeedApi.DynamicSnippets.csproj b/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/SeedApi.DynamicSnippets.csproj new file mode 100644 index 000000000000..3417db2e58e2 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/SeedApi.DynamicSnippets.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + 12 + enable + enable + + + + + + \ No newline at end of file diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/HeadersBuilderTests.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/HeadersBuilderTests.cs new file mode 100644 index 000000000000..bc0e7759e1f1 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/HeadersBuilderTests.cs @@ -0,0 +1,326 @@ +using NUnit.Framework; +using SeedBasicAuthOptional.Core; + +namespace SeedBasicAuthOptional.Test.Core; + +[TestFixture] +public class HeadersBuilderTests +{ + [Test] + public async global::System.Threading.Tasks.Task Add_SimpleHeaders() + { + var headers = await new HeadersBuilder.Builder() + .Add("Content-Type", "application/json") + .Add("Authorization", "Bearer token123") + .Add("X-API-Key", "key456") + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(headers.Count, Is.EqualTo(3)); + Assert.That(headers["Content-Type"], Is.EqualTo("application/json")); + Assert.That(headers["Authorization"], Is.EqualTo("Bearer token123")); + Assert.That(headers["X-API-Key"], Is.EqualTo("key456")); + } + + [Test] + public async global::System.Threading.Tasks.Task Add_NullValuesIgnored() + { + var headers = await new HeadersBuilder.Builder() + .Add("Header1", "value1") + .Add("Header2", null) + .Add("Header3", "value3") + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(headers.Count, Is.EqualTo(2)); + Assert.That(headers.ContainsKey("Header1"), Is.True); + Assert.That(headers.ContainsKey("Header2"), Is.False); + Assert.That(headers.ContainsKey("Header3"), Is.True); + } + + [Test] + public async global::System.Threading.Tasks.Task Add_OverwritesExistingHeader() + { + var headers = await new HeadersBuilder.Builder() + .Add("Content-Type", "application/json") + .Add("Content-Type", "application/xml") + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(headers.Count, Is.EqualTo(1)); + Assert.That(headers["Content-Type"], Is.EqualTo("application/xml")); + } + + [Test] + public async global::System.Threading.Tasks.Task Add_HeadersOverload_MergesExistingHeaders() + { + var existingHeaders = new Headers( + new Dictionary { { "Header1", "value1" }, { "Header2", "value2" } } + ); + + var result = await new HeadersBuilder.Builder() + .Add("Header3", "value3") + .Add(existingHeaders) + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(result.Count, Is.EqualTo(3)); + Assert.That(result["Header1"], Is.EqualTo("value1")); + Assert.That(result["Header2"], Is.EqualTo("value2")); + Assert.That(result["Header3"], Is.EqualTo("value3")); + } + + [Test] + public async global::System.Threading.Tasks.Task Add_HeadersOverload_OverwritesExistingHeaders() + { + var existingHeaders = new Headers( + new Dictionary { { "Header1", "override" } } + ); + + var result = await new HeadersBuilder.Builder() + .Add("Header1", "original") + .Add("Header2", "keep") + .Add(existingHeaders) + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(result.Count, Is.EqualTo(2)); + Assert.That(result["Header1"], Is.EqualTo("override")); + Assert.That(result["Header2"], Is.EqualTo("keep")); + } + + [Test] + public async global::System.Threading.Tasks.Task Add_HeadersOverload_NullHeadersIgnored() + { + var result = await new HeadersBuilder.Builder() + .Add("Header1", "value1") + .Add((Headers?)null) + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(result.Count, Is.EqualTo(1)); + Assert.That(result["Header1"], Is.EqualTo("value1")); + } + + [Test] + public async global::System.Threading.Tasks.Task Add_KeyValuePairOverload_AddsHeaders() + { + var additionalHeaders = new List> + { + new("Header1", "value1"), + new("Header2", "value2"), + }; + + var headers = await new HeadersBuilder.Builder() + .Add("Header3", "value3") + .Add(additionalHeaders) + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(headers.Count, Is.EqualTo(3)); + Assert.That(headers["Header1"], Is.EqualTo("value1")); + Assert.That(headers["Header2"], Is.EqualTo("value2")); + Assert.That(headers["Header3"], Is.EqualTo("value3")); + } + + [Test] + public async global::System.Threading.Tasks.Task Add_KeyValuePairOverload_IgnoresNullValues() + { + var additionalHeaders = new List> + { + new("Header1", "value1"), + new("Header2", null), // Should be ignored + }; + + var headers = await new HeadersBuilder.Builder() + .Add(additionalHeaders) + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(headers.Count, Is.EqualTo(1)); + Assert.That(headers.ContainsKey("Header2"), Is.False); + } + + [Test] + public async global::System.Threading.Tasks.Task Add_DictionaryOverload_AddsHeaders() + { + var dict = new Dictionary + { + { "Header1", "value1" }, + { "Header2", "value2" }, + }; + + var headers = await new HeadersBuilder.Builder() + .Add("Header3", "value3") + .Add(dict) + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(headers.Count, Is.EqualTo(3)); + Assert.That(headers["Header1"], Is.EqualTo("value1")); + Assert.That(headers["Header2"], Is.EqualTo("value2")); + Assert.That(headers["Header3"], Is.EqualTo("value3")); + } + + [Test] + public async global::System.Threading.Tasks.Task EmptyBuilder_ReturnsEmptyHeaders() + { + var headers = await new HeadersBuilder.Builder().BuildAsync().ConfigureAwait(false); + + Assert.That(headers.Count, Is.EqualTo(0)); + } + + [Test] + public async global::System.Threading.Tasks.Task OnlyNullValues_ReturnsEmptyHeaders() + { + var headers = await new HeadersBuilder.Builder() + .Add("Header1", null) + .Add("Header2", null) + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(headers.Count, Is.EqualTo(0)); + } + + [Test] + public async global::System.Threading.Tasks.Task ComplexMergingScenario() + { + // Simulates real SDK usage: endpoint headers + client headers + request options + var clientHeaders = new Headers( + new Dictionary + { + { "X-Client-Version", "1.0.0" }, + { "User-Agent", "MyClient/1.0" }, + } + ); + + var clientAdditionalHeaders = new List> + { + new("X-Custom-Header", "custom-value"), + }; + + var requestOptionsHeaders = new Headers( + new Dictionary + { + { "Authorization", "Bearer user-token" }, + { "User-Agent", "MyClient/2.0" }, // Override + } + ); + + var requestAdditionalHeaders = new List> + { + new("X-Request-ID", "req-123"), + new("X-Custom-Header", "overridden-value"), // Override + }; + + var headers = await new HeadersBuilder.Builder() + .Add("Content-Type", "application/json") // Endpoint header + .Add("X-Endpoint-ID", "endpoint-1") + .Add(clientHeaders) + .Add(clientAdditionalHeaders) + .Add(requestOptionsHeaders) + .Add(requestAdditionalHeaders) + .BuildAsync() + .ConfigureAwait(false); + + // Verify precedence + Assert.That(headers["Content-Type"], Is.EqualTo("application/json")); + Assert.That(headers["X-Endpoint-ID"], Is.EqualTo("endpoint-1")); + Assert.That(headers["X-Client-Version"], Is.EqualTo("1.0.0")); + Assert.That(headers["User-Agent"], Is.EqualTo("MyClient/2.0")); // Overridden + Assert.That(headers["Authorization"], Is.EqualTo("Bearer user-token")); + Assert.That(headers["X-Request-ID"], Is.EqualTo("req-123")); + Assert.That(headers["X-Custom-Header"], Is.EqualTo("overridden-value")); // Overridden + } + + [Test] + public async global::System.Threading.Tasks.Task Builder_WithCapacity() + { + // Test that capacity constructor works without errors + var headers = await new HeadersBuilder.Builder(capacity: 10) + .Add("Header1", "value1") + .Add("Header2", "value2") + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(headers.Count, Is.EqualTo(2)); + Assert.That(headers["Header1"], Is.EqualTo("value1")); + Assert.That(headers["Header2"], Is.EqualTo("value2")); + } + + [Test] + public async global::System.Threading.Tasks.Task Add_HeadersOverload_ResolvesDynamicHeaderValues() + { + // Test that BuildAsync properly resolves HeaderValue instances + var existingHeaders = new Headers(); + existingHeaders["DynamicHeader"] = + (Func>)( + () => global::System.Threading.Tasks.Task.FromResult("dynamic-value") + ); + + var result = await new HeadersBuilder.Builder() + .Add("StaticHeader", "static-value") + .Add(existingHeaders) + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(result.Count, Is.EqualTo(2)); + Assert.That(result["StaticHeader"], Is.EqualTo("static-value")); + Assert.That(result["DynamicHeader"], Is.EqualTo("dynamic-value")); + } + + [Test] + public async global::System.Threading.Tasks.Task MultipleSyncAdds() + { + var headers1 = new Headers(new Dictionary { { "H1", "v1" } }); + var headers2 = new Headers(new Dictionary { { "H2", "v2" } }); + var headers3 = new Headers(new Dictionary { { "H3", "v3" } }); + + var result = await new HeadersBuilder.Builder() + .Add(headers1) + .Add(headers2) + .Add(headers3) + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(result.Count, Is.EqualTo(3)); + Assert.That(result["H1"], Is.EqualTo("v1")); + Assert.That(result["H2"], Is.EqualTo("v2")); + Assert.That(result["H3"], Is.EqualTo("v3")); + } + + [Test] + public async global::System.Threading.Tasks.Task PrecedenceOrder_LatestWins() + { + // Test that later operations override earlier ones + var headers1 = new Headers(new Dictionary { { "Key", "value1" } }); + var headers2 = new Headers(new Dictionary { { "Key", "value2" } }); + var additional = new List> { new("Key", "value3") }; + + var result = await new HeadersBuilder.Builder() + .Add("Key", "value0") + .Add(headers1) + .Add(headers2) + .Add(additional) + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(result["Key"], Is.EqualTo("value3")); + } + + [Test] + public async global::System.Threading.Tasks.Task CaseInsensitiveKeys() + { + // Test that header keys are case-insensitive + var headers = await new HeadersBuilder.Builder() + .Add("content-type", "application/json") + .Add("Content-Type", "application/xml") // Should overwrite + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(headers.Count, Is.EqualTo(1)); + Assert.That(headers["content-type"], Is.EqualTo("application/xml")); + Assert.That(headers["Content-Type"], Is.EqualTo("application/xml")); + Assert.That(headers["CONTENT-TYPE"], Is.EqualTo("application/xml")); + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/Json/AdditionalPropertiesTests.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/Json/AdditionalPropertiesTests.cs new file mode 100644 index 000000000000..fbc8b32bd84e --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/Json/AdditionalPropertiesTests.cs @@ -0,0 +1,365 @@ +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedBasicAuthOptional.Core; + +namespace SeedBasicAuthOptional.Test.Core.Json; + +[TestFixture] +public class AdditionalPropertiesTests +{ + [Test] + public void Record_OnDeserialized_ShouldPopulateAdditionalProperties() + { + // Arrange + const string json = """ + { + "id": "1", + "category": "fiction", + "title": "The Hobbit" + } + """; + + // Act + var record = JsonUtils.Deserialize(json); + + // Assert + Assert.That(record, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(record.Id, Is.EqualTo("1")); + Assert.That(record.AdditionalProperties["category"].GetString(), Is.EqualTo("fiction")); + Assert.That(record.AdditionalProperties["title"].GetString(), Is.EqualTo("The Hobbit")); + }); + } + + [Test] + public void RecordWithWriteableAdditionalProperties_OnSerialization_ShouldIncludeAdditionalProperties() + { + // Arrange + var record = new WriteableRecord + { + Id = "1", + AdditionalProperties = { ["category"] = "fiction", ["title"] = "The Hobbit" }, + }; + + // Act + var json = JsonUtils.Serialize(record); + var deserializedRecord = JsonUtils.Deserialize(json); + + // Assert + Assert.That(deserializedRecord, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(deserializedRecord.Id, Is.EqualTo("1")); + Assert.That( + deserializedRecord.AdditionalProperties["category"], + Is.InstanceOf() + ); + Assert.That( + ((JsonElement)deserializedRecord.AdditionalProperties["category"]!).GetString(), + Is.EqualTo("fiction") + ); + Assert.That( + deserializedRecord.AdditionalProperties["title"], + Is.InstanceOf() + ); + Assert.That( + ((JsonElement)deserializedRecord.AdditionalProperties["title"]!).GetString(), + Is.EqualTo("The Hobbit") + ); + }); + } + + [Test] + public void ReadOnlyAdditionalProperties_ShouldRetrieveValuesCorrectly() + { + // Arrange + var extensionData = new Dictionary + { + ["key1"] = JsonUtils.SerializeToElement("value1"), + ["key2"] = JsonUtils.SerializeToElement(123), + }; + var readOnlyProps = new ReadOnlyAdditionalProperties(); + readOnlyProps.CopyFromExtensionData(extensionData); + + // Act & Assert + Assert.That(readOnlyProps["key1"].GetString(), Is.EqualTo("value1")); + Assert.That(readOnlyProps["key2"].GetInt32(), Is.EqualTo(123)); + } + + [Test] + public void AdditionalProperties_ShouldBehaveAsDictionary() + { + // Arrange + var additionalProps = new AdditionalProperties { ["key1"] = "value1", ["key2"] = 123 }; + + // Act + additionalProps["key3"] = true; + + // Assert + Assert.Multiple(() => + { + Assert.That(additionalProps["key1"], Is.EqualTo("value1")); + Assert.That(additionalProps["key2"], Is.EqualTo(123)); + Assert.That((bool)additionalProps["key3"]!, Is.True); + Assert.That(additionalProps.Count, Is.EqualTo(3)); + }); + } + + [Test] + public void AdditionalProperties_ToJsonObject_ShouldSerializeCorrectly() + { + // Arrange + var additionalProps = new AdditionalProperties { ["key1"] = "value1", ["key2"] = 123 }; + + // Act + var jsonObject = additionalProps.ToJsonObject(); + + Assert.Multiple(() => + { + // Assert + Assert.That(jsonObject["key1"]!.GetValue(), Is.EqualTo("value1")); + Assert.That(jsonObject["key2"]!.GetValue(), Is.EqualTo(123)); + }); + } + + [Test] + public void AdditionalProperties_MixReadAndWrite_ShouldOverwriteDeserializedProperty() + { + // Arrange + const string json = """ + { + "id": "1", + "category": "fiction", + "title": "The Hobbit" + } + """; + var record = JsonUtils.Deserialize(json); + + // Act + record.AdditionalProperties["category"] = "non-fiction"; + + // Assert + Assert.Multiple(() => + { + Assert.That(record, Is.Not.Null); + Assert.That(record.Id, Is.EqualTo("1")); + Assert.That(record.AdditionalProperties["category"], Is.EqualTo("non-fiction")); + Assert.That(record.AdditionalProperties["title"], Is.InstanceOf()); + Assert.That( + ((JsonElement)record.AdditionalProperties["title"]!).GetString(), + Is.EqualTo("The Hobbit") + ); + }); + } + + [Test] + public void RecordWithReadonlyAdditionalPropertiesInts_OnDeserialized_ShouldPopulateAdditionalProperties() + { + // Arrange + const string json = """ + { + "extra1": 42, + "extra2": 99 + } + """; + + // Act + var record = JsonUtils.Deserialize(json); + + // Assert + Assert.That(record, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(record.AdditionalProperties["extra1"], Is.EqualTo(42)); + Assert.That(record.AdditionalProperties["extra2"], Is.EqualTo(99)); + }); + } + + [Test] + public void RecordWithAdditionalPropertiesInts_OnSerialization_ShouldIncludeAdditionalProperties() + { + // Arrange + var record = new WriteableRecordWithInts + { + AdditionalProperties = { ["extra1"] = 42, ["extra2"] = 99 }, + }; + + // Act + var json = JsonUtils.Serialize(record); + var deserializedRecord = JsonUtils.Deserialize(json); + + // Assert + Assert.That(deserializedRecord, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(deserializedRecord.AdditionalProperties["extra1"], Is.EqualTo(42)); + Assert.That(deserializedRecord.AdditionalProperties["extra2"], Is.EqualTo(99)); + }); + } + + [Test] + public void RecordWithReadonlyAdditionalPropertiesDictionaries_OnDeserialized_ShouldPopulateAdditionalProperties() + { + // Arrange + const string json = """ + { + "extra1": { "key1": true, "key2": false }, + "extra2": { "key3": true } + } + """; + + // Act + var record = JsonUtils.Deserialize(json); + + // Assert + Assert.That(record, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(record.AdditionalProperties["extra1"]["key1"], Is.True); + Assert.That(record.AdditionalProperties["extra1"]["key2"], Is.False); + Assert.That(record.AdditionalProperties["extra2"]["key3"], Is.True); + }); + } + + [Test] + public void RecordWithAdditionalPropertiesDictionaries_OnSerialization_ShouldIncludeAdditionalProperties() + { + // Arrange + var record = new WriteableRecordWithDictionaries + { + AdditionalProperties = + { + ["extra1"] = new Dictionary { { "key1", true }, { "key2", false } }, + ["extra2"] = new Dictionary { { "key3", true } }, + }, + }; + + // Act + var json = JsonUtils.Serialize(record); + var deserializedRecord = JsonUtils.Deserialize(json); + + // Assert + Assert.That(deserializedRecord, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(deserializedRecord.AdditionalProperties["extra1"]["key1"], Is.True); + Assert.That(deserializedRecord.AdditionalProperties["extra1"]["key2"], Is.False); + Assert.That(deserializedRecord.AdditionalProperties["extra2"]["key3"], Is.True); + }); + } + + private record Record : IJsonOnDeserialized + { + [JsonPropertyName("id")] + public required string Id { get; set; } + + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; } = new(); + + void IJsonOnDeserialized.OnDeserialized() + { + AdditionalProperties.CopyFromExtensionData(_extensionData); + } + } + + private record WriteableRecord : IJsonOnDeserialized, IJsonOnSerializing + { + [JsonPropertyName("id")] + public required string Id { get; set; } + + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonIgnore] + public AdditionalProperties AdditionalProperties { get; set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() + { + AdditionalProperties.CopyFromExtensionData(_extensionData); + } + + void IJsonOnSerializing.OnSerializing() + { + AdditionalProperties.CopyToExtensionData(_extensionData); + } + } + + private record RecordWithInts : IJsonOnDeserialized + { + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; } = new(); + + void IJsonOnDeserialized.OnDeserialized() + { + AdditionalProperties.CopyFromExtensionData(_extensionData); + } + } + + private record WriteableRecordWithInts : IJsonOnDeserialized, IJsonOnSerializing + { + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonIgnore] + public AdditionalProperties AdditionalProperties { get; } = new(); + + void IJsonOnDeserialized.OnDeserialized() + { + AdditionalProperties.CopyFromExtensionData(_extensionData); + } + + void IJsonOnSerializing.OnSerializing() + { + AdditionalProperties.CopyToExtensionData(_extensionData); + } + } + + private record RecordWithDictionaries : IJsonOnDeserialized + { + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonIgnore] + public ReadOnlyAdditionalProperties< + Dictionary + > AdditionalProperties { get; } = new(); + + void IJsonOnDeserialized.OnDeserialized() + { + AdditionalProperties.CopyFromExtensionData(_extensionData); + } + } + + private record WriteableRecordWithDictionaries : IJsonOnDeserialized, IJsonOnSerializing + { + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonIgnore] + public AdditionalProperties> AdditionalProperties { get; } = new(); + + void IJsonOnDeserialized.OnDeserialized() + { + AdditionalProperties.CopyFromExtensionData(_extensionData); + } + + void IJsonOnSerializing.OnSerializing() + { + AdditionalProperties.CopyToExtensionData(_extensionData); + } + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/Json/DateOnlyJsonTests.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/Json/DateOnlyJsonTests.cs new file mode 100644 index 000000000000..df03e0d70b29 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/Json/DateOnlyJsonTests.cs @@ -0,0 +1,100 @@ +using NUnit.Framework; +using SeedBasicAuthOptional.Core; + +namespace SeedBasicAuthOptional.Test.Core.Json; + +[TestFixture] +public class DateOnlyJsonTests +{ + [Test] + public void SerializeDateOnly_ShouldMatchExpectedFormat() + { + (DateOnly dateOnly, string expected)[] testCases = + [ + (new DateOnly(2023, 10, 5), "\"2023-10-05\""), + (new DateOnly(2023, 1, 1), "\"2023-01-01\""), + (new DateOnly(2023, 12, 31), "\"2023-12-31\""), + (new DateOnly(2023, 6, 15), "\"2023-06-15\""), + (new DateOnly(2023, 3, 10), "\"2023-03-10\""), + ]; + foreach (var (dateOnly, expected) in testCases) + { + var json = JsonUtils.Serialize(dateOnly); + Assert.That(json, Is.EqualTo(expected)); + } + } + + [Test] + public void DeserializeDateOnly_ShouldMatchExpectedDateOnly() + { + (DateOnly expected, string json)[] testCases = + [ + (new DateOnly(2023, 10, 5), "\"2023-10-05\""), + (new DateOnly(2023, 1, 1), "\"2023-01-01\""), + (new DateOnly(2023, 12, 31), "\"2023-12-31\""), + (new DateOnly(2023, 6, 15), "\"2023-06-15\""), + (new DateOnly(2023, 3, 10), "\"2023-03-10\""), + ]; + + foreach (var (expected, json) in testCases) + { + var dateOnly = JsonUtils.Deserialize(json); + Assert.That(dateOnly, Is.EqualTo(expected)); + } + } + + [Test] + public void SerializeNullableDateOnly_ShouldMatchExpectedFormat() + { + (DateOnly? dateOnly, string expected)[] testCases = + [ + (new DateOnly(2023, 10, 5), "\"2023-10-05\""), + (null, "null"), + ]; + foreach (var (dateOnly, expected) in testCases) + { + var json = JsonUtils.Serialize(dateOnly); + Assert.That(json, Is.EqualTo(expected)); + } + } + + [Test] + public void DeserializeNullableDateOnly_ShouldMatchExpectedDateOnly() + { + (DateOnly? expected, string json)[] testCases = + [ + (new DateOnly(2023, 10, 5), "\"2023-10-05\""), + (null, "null"), + ]; + + foreach (var (expected, json) in testCases) + { + var dateOnly = JsonUtils.Deserialize(json); + Assert.That(dateOnly, Is.EqualTo(expected)); + } + } + + [Test] + public void ShouldSerializeDictionaryWithDateOnlyKey() + { + var key = new DateOnly(2023, 10, 5); + var dict = new Dictionary { { key, "value_a" } }; + var json = JsonUtils.Serialize(dict); + Assert.That(json, Does.Contain("2023-10-05")); + Assert.That(json, Does.Contain("value_a")); + } + + [Test] + public void ShouldDeserializeDictionaryWithDateOnlyKey() + { + var json = """ + { + "2023-10-05": "value_a" + } + """; + var dict = JsonUtils.Deserialize>(json); + Assert.That(dict, Is.Not.Null); + var key = new DateOnly(2023, 10, 5); + Assert.That(dict![key], Is.EqualTo("value_a")); + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/Json/DateTimeJsonTests.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/Json/DateTimeJsonTests.cs new file mode 100644 index 000000000000..6807d966d800 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/Json/DateTimeJsonTests.cs @@ -0,0 +1,134 @@ +using NUnit.Framework; +using SeedBasicAuthOptional.Core; + +namespace SeedBasicAuthOptional.Test.Core.Json; + +[TestFixture] +public class DateTimeJsonTests +{ + [Test] + public void SerializeDateTime_ShouldMatchExpectedFormat() + { + (DateTime dateTime, string expected)[] testCases = + [ + ( + new DateTime(2023, 10, 5, 14, 30, 0, DateTimeKind.Utc), + "\"2023-10-05T14:30:00.000Z\"" + ), + (new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc), "\"2023-01-01T00:00:00.000Z\""), + ( + new DateTime(2023, 12, 31, 23, 59, 59, DateTimeKind.Utc), + "\"2023-12-31T23:59:59.000Z\"" + ), + (new DateTime(2023, 6, 15, 12, 0, 0, DateTimeKind.Utc), "\"2023-06-15T12:00:00.000Z\""), + ( + new DateTime(2023, 3, 10, 8, 45, 30, DateTimeKind.Utc), + "\"2023-03-10T08:45:30.000Z\"" + ), + ( + new DateTime(2023, 3, 10, 8, 45, 30, 123, DateTimeKind.Utc), + "\"2023-03-10T08:45:30.123Z\"" + ), + ]; + foreach (var (dateTime, expected) in testCases) + { + var json = JsonUtils.Serialize(dateTime); + Assert.That(json, Is.EqualTo(expected)); + } + } + + [Test] + public void DeserializeDateTime_ShouldMatchExpectedDateTime() + { + (DateTime expected, string json)[] testCases = + [ + ( + new DateTime(2023, 10, 5, 14, 30, 0, DateTimeKind.Utc), + "\"2023-10-05T14:30:00.000Z\"" + ), + (new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc), "\"2023-01-01T00:00:00.000Z\""), + ( + new DateTime(2023, 12, 31, 23, 59, 59, DateTimeKind.Utc), + "\"2023-12-31T23:59:59.000Z\"" + ), + (new DateTime(2023, 6, 15, 12, 0, 0, DateTimeKind.Utc), "\"2023-06-15T12:00:00.000Z\""), + ( + new DateTime(2023, 3, 10, 8, 45, 30, DateTimeKind.Utc), + "\"2023-03-10T08:45:30.000Z\"" + ), + (new DateTime(2023, 3, 10, 8, 45, 30, DateTimeKind.Utc), "\"2023-03-10T08:45:30Z\""), + ( + new DateTime(2023, 3, 10, 8, 45, 30, 123, DateTimeKind.Utc), + "\"2023-03-10T08:45:30.123Z\"" + ), + ]; + + foreach (var (expected, json) in testCases) + { + var dateTime = JsonUtils.Deserialize(json); + Assert.That(dateTime, Is.EqualTo(expected)); + } + } + + [Test] + public void SerializeNullableDateTime_ShouldMatchExpectedFormat() + { + (DateTime? expected, string json)[] testCases = + [ + ( + new DateTime(2023, 10, 5, 14, 30, 0, DateTimeKind.Utc), + "\"2023-10-05T14:30:00.000Z\"" + ), + (null, "null"), + ]; + + foreach (var (expected, json) in testCases) + { + var dateTime = JsonUtils.Deserialize(json); + Assert.That(dateTime, Is.EqualTo(expected)); + } + } + + [Test] + public void DeserializeNullableDateTime_ShouldMatchExpectedDateTime() + { + (DateTime? expected, string json)[] testCases = + [ + ( + new DateTime(2023, 10, 5, 14, 30, 0, DateTimeKind.Utc), + "\"2023-10-05T14:30:00.000Z\"" + ), + (null, "null"), + ]; + + foreach (var (expected, json) in testCases) + { + var dateTime = JsonUtils.Deserialize(json); + Assert.That(dateTime, Is.EqualTo(expected)); + } + } + + [Test] + public void ShouldSerializeDictionaryWithDateTimeKey() + { + var key = new DateTime(2023, 10, 5, 14, 30, 0, DateTimeKind.Utc); + var dict = new Dictionary { { key, "value_a" } }; + var json = JsonUtils.Serialize(dict); + Assert.That(json, Does.Contain("2023-10-05T14:30:00.000Z")); + Assert.That(json, Does.Contain("value_a")); + } + + [Test] + public void ShouldDeserializeDictionaryWithDateTimeKey() + { + var json = """ + { + "2023-10-05T14:30:00.000Z": "value_a" + } + """; + var dict = JsonUtils.Deserialize>(json); + Assert.That(dict, Is.Not.Null); + var key = new DateTime(2023, 10, 5, 14, 30, 0, DateTimeKind.Utc); + Assert.That(dict![key], Is.EqualTo("value_a")); + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 000000000000..3d965c92fe55 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,160 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedBasicAuthOptional.Core; + +namespace SeedBasicAuthOptional.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + + [JsonPropertyName("read_only_nullable_list")] + [JsonAccess(JsonAccessType.ReadOnly)] + public IEnumerable? ReadOnlyNullableList { get; set; } + + [JsonPropertyName("read_only_list")] + [JsonAccess(JsonAccessType.ReadOnly)] + public IEnumerable ReadOnlyList { get; set; } = []; + + [JsonPropertyName("write_only_nullable_list")] + [JsonAccess(JsonAccessType.WriteOnly)] + public IEnumerable? WriteOnlyNullableList { get; set; } + + [JsonPropertyName("write_only_list")] + [JsonAccess(JsonAccessType.WriteOnly)] + public IEnumerable WriteOnlyList { get; set; } = []; + + [JsonPropertyName("normal_list")] + public IEnumerable NormalList { get; set; } = []; + + [JsonPropertyName("normal_nullable_list")] + public IEnumerable? NullableNormalList { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = """ + { + "read_only_prop": "read", + "write_only_prop": "write", + "normal_prop": "normal_prop", + "read_only_nullable_list": ["item1", "item2"], + "read_only_list": ["item3", "item4"], + "write_only_nullable_list": ["item5", "item6"], + "write_only_list": ["item7", "item8"], + "normal_list": ["normal1", "normal2"], + "normal_nullable_list": ["normal1", "normal2"] + } + """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + // String properties + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + + // List properties - read only + var nullableReadOnlyList = obj.ReadOnlyNullableList?.ToArray(); + Assert.That(nullableReadOnlyList, Is.Not.Null); + Assert.That(nullableReadOnlyList, Has.Length.EqualTo(2)); + Assert.That(nullableReadOnlyList![0], Is.EqualTo("item1")); + Assert.That(nullableReadOnlyList![1], Is.EqualTo("item2")); + + var readOnlyList = obj.ReadOnlyList.ToArray(); + Assert.That(readOnlyList, Is.Not.Null); + Assert.That(readOnlyList, Has.Length.EqualTo(2)); + Assert.That(readOnlyList[0], Is.EqualTo("item3")); + Assert.That(readOnlyList[1], Is.EqualTo("item4")); + + // List properties - write only + Assert.That(obj.WriteOnlyNullableList, Is.Null); + Assert.That(obj.WriteOnlyList, Is.Not.Null); + Assert.That(obj.WriteOnlyList, Is.Empty); + + // Normal list property + var normalList = obj.NormalList.ToArray(); + Assert.That(normalList, Is.Not.Null); + Assert.That(normalList, Has.Length.EqualTo(2)); + Assert.That(normalList[0], Is.EqualTo("normal1")); + Assert.That(normalList[1], Is.EqualTo("normal2")); + }); + + // Set up values for serialization + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + obj.WriteOnlyNullableList = new List { "write1", "write2" }; + obj.WriteOnlyList = new List { "write3", "write4" }; + obj.NormalList = new List { "new_normal" }; + obj.NullableNormalList = new List { "new_normal" }; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = """ + { + "write_only_prop": "write", + "normal_prop": "new_value", + "write_only_nullable_list": [ + "write1", + "write2" + ], + "write_only_list": [ + "write3", + "write4" + ], + "normal_list": [ + "new_normal" + ], + "normal_nullable_list": [ + "new_normal" + ] + } + """; + Assert.That(serializedJson, Is.EqualTo(expectedJson).IgnoreWhiteSpace); + } + + [Test] + public void JsonAccessAttribute_WithNullListsInJson_ShouldWorkAsExpected() + { + const string json = """ + { + "read_only_prop": "read", + "normal_prop": "normal_prop", + "read_only_nullable_list": null, + "read_only_list": [] + } + """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + // Read-only nullable list should be null when JSON contains null + var nullableReadOnlyList = obj.ReadOnlyNullableList?.ToArray(); + Assert.That(nullableReadOnlyList, Is.Null); + + // Read-only non-nullable list should never be null, but empty when JSON contains null + var readOnlyList = obj.ReadOnlyList.ToArray(); // This should be initialized to an empty list by default + Assert.That(readOnlyList, Is.Not.Null); + Assert.That(readOnlyList, Is.Empty); + }); + + // Serialize and verify read-only lists are not included + var serializedJson = JsonUtils.Serialize(obj); + Assert.That(serializedJson, Does.Not.Contain("read_only_prop")); + Assert.That(serializedJson, Does.Not.Contain("read_only_nullable_list")); + Assert.That(serializedJson, Does.Not.Contain("read_only_list")); + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/QueryStringBuilderTests.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/QueryStringBuilderTests.cs new file mode 100644 index 000000000000..2a84be60aa64 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/QueryStringBuilderTests.cs @@ -0,0 +1,560 @@ +using NUnit.Framework; +using SeedBasicAuthOptional.Core; + +namespace SeedBasicAuthOptional.Test.Core; + +[TestFixture] +public class QueryStringBuilderTests +{ + [Test] + public void Build_SimpleParameters() + { + var parameters = new List> + { + new("name", "John Doe"), + new("age", "30"), + new("city", "New York"), + }; + + var result = QueryStringBuilder.Build(parameters); + + Assert.That(result, Is.EqualTo("?name=John%20Doe&age=30&city=New%20York")); + } + + [Test] + public void Build_EmptyList_ReturnsEmptyString() + { + var parameters = new List>(); + + var result = QueryStringBuilder.Build(parameters); + + Assert.That(result, Is.EqualTo(string.Empty)); + } + + [Test] + public void Build_SpecialCharacters() + { + var parameters = new List> + { + new("email", "test@example.com"), + new("url", "https://example.com/path?query=value"), + new("special", "a+b=c&d"), + }; + + var result = QueryStringBuilder.Build(parameters); + + Assert.That( + result, + Is.EqualTo( + "?email=test%40example.com&url=https%3A%2F%2Fexample.com%2Fpath%3Fquery%3Dvalue&special=a%2Bb%3Dc%26d" + ) + ); + } + + [Test] + public void Build_UnicodeCharacters() + { + var parameters = new List> { new("greeting", "Hello 世界") }; + + var result = QueryStringBuilder.Build(parameters); + + // Verify the Chinese characters are properly UTF-8 encoded + Assert.That(result, Does.StartWith("?greeting=Hello%20")); + Assert.That(result, Does.Contain("%E4%B8%96%E7%95%8C")); // 世界 + } + + [Test] + public void Build_SessionSettings_DeepObject() + { + // Simulate session settings with nested properties + var sessionSettings = new + { + custom_session_id = "my-custom-session-id", + system_prompt = "You are a helpful assistant", + variables = new Dictionary + { + { "userName", "John" }, + { "userAge", 30 }, + { "isPremium", true }, + }, + }; + + // Build query parameters list + var queryParams = new List> { new("api_key", "test_key_123") }; + + // Add session_settings with prefix using the new overload + queryParams.AddRange( + QueryStringConverter.ToDeepObject("session_settings", sessionSettings) + ); + + var result = QueryStringBuilder.Build(queryParams); + + // Verify the result contains properly formatted deep object notation + // Note: Square brackets are URL-encoded as %5B and %5D + Assert.That(result, Does.StartWith("?api_key=test_key_123")); + Assert.That( + result, + Does.Contain("session_settings%5Bcustom_session_id%5D=my-custom-session-id") + ); + Assert.That( + result, + Does.Contain("session_settings%5Bsystem_prompt%5D=You%20are%20a%20helpful%20assistant") + ); + Assert.That(result, Does.Contain("session_settings%5Bvariables%5D%5BuserName%5D=John")); + Assert.That(result, Does.Contain("session_settings%5Bvariables%5D%5BuserAge%5D=30")); + Assert.That(result, Does.Contain("session_settings%5Bvariables%5D%5BisPremium%5D=true")); + + // Verify it's NOT JSON encoded (no braces or quotes in the original format) + Assert.That(result, Does.Not.Contain("%7B%22")); // Not {" sequence + } + + [Test] + public void Build_ChatApiLikeParameters() + { + // Simulate what ChatApi constructor does + var sessionSettings = new + { + system_prompt = "You are helpful", + variables = new Dictionary { { "name", "Alice" } }, + }; + + var queryParams = new List>(); + + // Simple parameters + var simpleParams = new Dictionary + { + { "access_token", "token123" }, + { "config_id", "config456" }, + { "api_key", "key789" }, + }; + queryParams.AddRange(QueryStringConverter.ToExplodedForm(simpleParams)); + + // Session settings as deep object with prefix + queryParams.AddRange( + QueryStringConverter.ToDeepObject("session_settings", sessionSettings) + ); + + var result = QueryStringBuilder.Build(queryParams); + + // Verify structure (square brackets are URL-encoded) + Assert.That(result, Does.StartWith("?")); + Assert.That(result, Does.Contain("access_token=token123")); + Assert.That(result, Does.Contain("config_id=config456")); + Assert.That(result, Does.Contain("api_key=key789")); + Assert.That( + result, + Does.Contain("session_settings%5Bsystem_prompt%5D=You%20are%20helpful") + ); + Assert.That(result, Does.Contain("session_settings%5Bvariables%5D%5Bname%5D=Alice")); + } + + [Test] + public void Build_ReservedCharacters_NotEncoded() + { + var parameters = new List> + { + new("path", "some-path"), + new("id", "123-456_789.test~value"), + }; + + var result = QueryStringBuilder.Build(parameters); + + // Unreserved characters: A-Z a-z 0-9 - _ . ~ + Assert.That(result, Is.EqualTo("?path=some-path&id=123-456_789.test~value")); + } + + [Test] + public void Builder_Add_SimpleParameters() + { + var result = new QueryStringBuilder.Builder() + .Add("name", "John Doe") + .Add("age", 30) + .Add("active", true) + .Build(); + + Assert.That(result, Does.Contain("name=John%20Doe")); + Assert.That(result, Does.Contain("age=30")); + Assert.That(result, Does.Contain("active=true")); + } + + [Test] + public void Builder_Add_NullValuesIgnored() + { + var result = new QueryStringBuilder.Builder() + .Add("name", "John") + .Add("middle", null) + .Add("age", 30) + .Build(); + + Assert.That(result, Does.Contain("name=John")); + Assert.That(result, Does.Contain("age=30")); + Assert.That(result, Does.Not.Contain("middle")); + } + + [Test] + public void Builder_AddDeepObject_WithPrefix() + { + var settings = new + { + custom_session_id = "id-123", + system_prompt = "You are helpful", + variables = new { name = "Alice", age = 25 }, + }; + + var result = new QueryStringBuilder.Builder() + .Add("api_key", "key123") + .AddDeepObject("session_settings", settings) + .Build(); + + Assert.That(result, Does.Contain("api_key=key123")); + Assert.That(result, Does.Contain("session_settings%5Bcustom_session_id%5D=id-123")); + Assert.That( + result, + Does.Contain("session_settings%5Bsystem_prompt%5D=You%20are%20helpful") + ); + Assert.That(result, Does.Contain("session_settings%5Bvariables%5D%5Bname%5D=Alice")); + Assert.That(result, Does.Contain("session_settings%5Bvariables%5D%5Bage%5D=25")); + } + + [Test] + public void Builder_AddDeepObject_NullIgnored() + { + var result = new QueryStringBuilder.Builder() + .Add("api_key", "key123") + .AddDeepObject("settings", null) + .Build(); + + Assert.That(result, Is.EqualTo("?api_key=key123")); + Assert.That(result, Does.Not.Contain("settings")); + } + + [Test] + public void Builder_AddExploded_WithPrefix() + { + var filter = new { status = "active", type = "user" }; + + var result = new QueryStringBuilder.Builder() + .Add("api_key", "key123") + .AddExploded("filter", filter) + .Build(); + + Assert.That(result, Does.Contain("api_key=key123")); + Assert.That(result, Does.Contain("filter%5Bstatus%5D=active")); + Assert.That(result, Does.Contain("filter%5Btype%5D=user")); + } + + [Test] + public void Builder_AddExploded_NullIgnored() + { + var result = new QueryStringBuilder.Builder() + .Add("api_key", "key123") + .AddExploded("filter", null) + .Build(); + + Assert.That(result, Is.EqualTo("?api_key=key123")); + Assert.That(result, Does.Not.Contain("filter")); + } + + [Test] + public void Builder_WithCapacity() + { + // Test that capacity constructor works without errors + var result = new QueryStringBuilder.Builder(capacity: 10) + .Add("param1", "value1") + .Add("param2", "value2") + .Build(); + + Assert.That(result, Does.Contain("param1=value1")); + Assert.That(result, Does.Contain("param2=value2")); + } + + [Test] + public void Builder_ChatApiLikeUsage() + { + // Simulate real usage from ChatApi + var sessionSettings = new + { + custom_session_id = "session-123", + variables = new Dictionary + { + { "userName", "John" }, + { "userAge", 30 }, + }, + }; + + var result = new QueryStringBuilder.Builder(capacity: 16) + .Add("access_token", "token123") + .Add("allow_connection", true) + .Add("config_id", "config456") + .Add("api_key", "key789") + .AddDeepObject("session_settings", sessionSettings) + .Build(); + + Assert.That(result, Does.StartWith("?")); + Assert.That(result, Does.Contain("access_token=token123")); + Assert.That(result, Does.Contain("allow_connection=true")); + Assert.That(result, Does.Contain("config_id=config456")); + Assert.That(result, Does.Contain("api_key=key789")); + Assert.That(result, Does.Contain("session_settings%5Bcustom_session_id%5D=session-123")); + Assert.That(result, Does.Contain("session_settings%5Bvariables%5D%5BuserName%5D=John")); + Assert.That(result, Does.Contain("session_settings%5Bvariables%5D%5BuserAge%5D=30")); + } + + [Test] + public void Builder_EmptyBuilder_ReturnsEmptyString() + { + var result = new QueryStringBuilder.Builder().Build(); + + Assert.That(result, Is.EqualTo(string.Empty)); + } + + [Test] + public void Builder_OnlyNullValues_ReturnsEmptyString() + { + var result = new QueryStringBuilder.Builder() + .Add("param1", null) + .Add("param2", null) + .AddDeepObject("settings", null) + .Build(); + + Assert.That(result, Is.EqualTo(string.Empty)); + } + + [Test] + public void Builder_Set_OverridesSingleValue() + { + var result = new QueryStringBuilder.Builder() + .Add("foo", "original") + .Set("foo", "override") + .Build(); + + Assert.That(result, Is.EqualTo("?foo=override")); + } + + [Test] + public void Builder_Set_OverridesMultipleValues() + { + var result = new QueryStringBuilder.Builder() + .Add("foo", "value1") + .Add("foo", "value2") + .Set("foo", "override") + .Build(); + + Assert.That(result, Is.EqualTo("?foo=override")); + } + + [Test] + public void Builder_Set_WithArray_CreatesMultipleParameters() + { + var result = new QueryStringBuilder.Builder() + .Add("foo", "original") + .Set("foo", new[] { "value1", "value2" }) + .Build(); + + Assert.That(result, Is.EqualTo("?foo=value1&foo=value2")); + } + + [Test] + public void Builder_Set_WithNull_RemovesParameter() + { + var result = new QueryStringBuilder.Builder() + .Add("foo", "original") + .Add("bar", "keep") + .Set("foo", null) + .Build(); + + Assert.That(result, Is.EqualTo("?bar=keep")); + } + + [Test] + public void Builder_MergeAdditional_WithSingleValues() + { + var additional = new List> + { + new("foo", "bar"), + new("baz", "qux"), + }; + + var result = new QueryStringBuilder.Builder() + .Add("existing", "value") + .MergeAdditional(additional) + .Build(); + + Assert.That(result, Does.Contain("existing=value")); + Assert.That(result, Does.Contain("foo=bar")); + Assert.That(result, Does.Contain("baz=qux")); + } + + [Test] + public void Builder_MergeAdditional_WithDuplicateKeys_CreatesList() + { + var additional = new List> + { + new("foo", "bar1"), + new("foo", "bar2"), + new("baz", "qux"), + }; + + var result = new QueryStringBuilder.Builder() + .Add("existing", "value") + .MergeAdditional(additional) + .Build(); + + Assert.That(result, Does.Contain("existing=value")); + Assert.That(result, Does.Contain("foo=bar1")); + Assert.That(result, Does.Contain("foo=bar2")); + Assert.That(result, Does.Contain("baz=qux")); + } + + [Test] + public void Builder_MergeAdditional_OverridesExistingParameters() + { + var additional = new List> { new("foo", "override") }; + + var result = new QueryStringBuilder.Builder() + .Add("foo", "original1") + .Add("foo", "original2") + .Add("bar", "keep") + .MergeAdditional(additional) + .Build(); + + Assert.That(result, Does.Contain("bar=keep")); + Assert.That(result, Does.Contain("foo=override")); + Assert.That(result, Does.Not.Contain("original1")); + Assert.That(result, Does.Not.Contain("original2")); + } + + [Test] + public void Builder_MergeAdditional_WithDuplicates_OverridesExisting() + { + var additional = new List> + { + new("foo", "new1"), + new("foo", "new2"), + new("foo", "new3"), + }; + + var result = new QueryStringBuilder.Builder() + .Add("foo", "original1") + .Add("foo", "original2") + .Add("bar", "keep") + .MergeAdditional(additional) + .Build(); + + Assert.That(result, Does.Contain("bar=keep")); + Assert.That(result, Does.Contain("foo=new1")); + Assert.That(result, Does.Contain("foo=new2")); + Assert.That(result, Does.Contain("foo=new3")); + Assert.That(result, Does.Not.Contain("original1")); + Assert.That(result, Does.Not.Contain("original2")); + } + + [Test] + public void Builder_MergeAdditional_WithNull_NoOp() + { + var result = new QueryStringBuilder.Builder() + .Add("foo", "value") + .MergeAdditional(null) + .Build(); + + Assert.That(result, Is.EqualTo("?foo=value")); + } + + [Test] + public void Builder_MergeAdditional_WithEmptyList_NoOp() + { + var additional = new List>(); + + var result = new QueryStringBuilder.Builder() + .Add("foo", "value") + .MergeAdditional(additional) + .Build(); + + Assert.That(result, Is.EqualTo("?foo=value")); + } + + [Test] + public void Builder_MergeAdditional_RealWorldScenario() + { + // SDK generates foo=foo1&foo=foo2 + var builder = new QueryStringBuilder.Builder() + .Add("foo", "foo1") + .Add("foo", "foo2") + .Add("bar", "baz"); + + // User provides foo=override in AdditionalQueryParameters + var additional = new List> { new("foo", "override") }; + + var result = builder.MergeAdditional(additional).Build(); + + // Result should be foo=override&bar=baz (user overrides SDK) + Assert.That(result, Does.Contain("bar=baz")); + Assert.That(result, Does.Contain("foo=override")); + Assert.That(result, Does.Not.Contain("foo1")); + Assert.That(result, Does.Not.Contain("foo2")); + } + + [Test] + public void Builder_MergeAdditional_UserProvidesMultipleValues() + { + // SDK generates no foo parameter + var builder = new QueryStringBuilder.Builder().Add("bar", "baz"); + + // User provides foo=bar1&foo=bar2 in AdditionalQueryParameters + var additional = new List> + { + new("foo", "bar1"), + new("foo", "bar2"), + }; + + var result = builder.MergeAdditional(additional).Build(); + + // Result should be bar=baz&foo=bar1&foo=bar2 + Assert.That(result, Does.Contain("bar=baz")); + Assert.That(result, Does.Contain("foo=bar1")); + Assert.That(result, Does.Contain("foo=bar2")); + } + + [Test] + public void Builder_Add_WithCollection_CreatesMultipleParameters() + { + var tags = new[] { "tag1", "tag2", "tag3" }; + var result = new QueryStringBuilder.Builder().Add("tag", tags).Build(); + + Assert.That(result, Does.Contain("tag=tag1")); + Assert.That(result, Does.Contain("tag=tag2")); + Assert.That(result, Does.Contain("tag=tag3")); + } + + [Test] + public void Builder_Add_WithList_CreatesMultipleParameters() + { + var ids = new List { 1, 2, 3 }; + var result = new QueryStringBuilder.Builder().Add("id", ids).Build(); + + Assert.That(result, Does.Contain("id=1")); + Assert.That(result, Does.Contain("id=2")); + Assert.That(result, Does.Contain("id=3")); + } + + [Test] + public void Builder_Set_WithCollection_ReplacesAllPreviousValues() + { + var result = new QueryStringBuilder.Builder() + .Add("id", 1) + .Add("id", 2) + .Set("id", new[] { 10, 20, 30 }) + .Build(); + + Assert.That(result, Does.Contain("id=10")); + Assert.That(result, Does.Contain("id=20")); + Assert.That(result, Does.Contain("id=30")); + // Check that old values are not present (use word boundaries to avoid false positives with id=10) + Assert.That(result, Does.Not.Contain("id=1&")); + Assert.That(result, Does.Not.Contain("id=2&")); + Assert.That(result, Does.Not.Contain("id=1?")); + Assert.That(result, Does.Not.Contain("id=2?")); + Assert.That(result, Does.Not.EndWith("id=1")); + Assert.That(result, Does.Not.EndWith("id=2")); + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/QueryStringConverterTests.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/QueryStringConverterTests.cs new file mode 100644 index 000000000000..9c137198ad52 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/QueryStringConverterTests.cs @@ -0,0 +1,158 @@ +using NUnit.Framework; +using SeedBasicAuthOptional.Core; + +namespace SeedBasicAuthOptional.Test.Core; + +[TestFixture] +public class QueryStringConverterTests +{ + [Test] + public void ToQueryStringCollection_Form() + { + var obj = new + { + Name = "John", + Age = 30, + Address = new + { + Street = "123 Main St", + City = "Anytown", + Coordinates = new[] { 39.781721f, -89.650148f }, + }, + Tags = new[] { "Developer", "Blogger" }, + }; + var result = QueryStringConverter.ToForm(obj); + var expected = new List> + { + new("Name", "John"), + new("Age", "30"), + new("Address[Street]", "123 Main St"), + new("Address[City]", "Anytown"), + new("Address[Coordinates]", "39.78172,-89.65015"), + new("Tags", "Developer,Blogger"), + }; + Assert.That(result, Is.EqualTo(expected)); + } + + [Test] + public void ToQueryStringCollection_ExplodedForm() + { + var obj = new + { + Name = "John", + Age = 30, + Address = new + { + Street = "123 Main St", + City = "Anytown", + Coordinates = new[] { 39.781721f, -89.650148f }, + }, + Tags = new[] { "Developer", "Blogger" }, + }; + var result = QueryStringConverter.ToExplodedForm(obj); + var expected = new List> + { + new("Name", "John"), + new("Age", "30"), + new("Address[Street]", "123 Main St"), + new("Address[City]", "Anytown"), + new("Address[Coordinates]", "39.78172"), + new("Address[Coordinates]", "-89.65015"), + new("Tags", "Developer"), + new("Tags", "Blogger"), + }; + Assert.That(result, Is.EqualTo(expected)); + } + + [Test] + public void ToQueryStringCollection_DeepObject() + { + var obj = new + { + Name = "John", + Age = 30, + Address = new + { + Street = "123 Main St", + City = "Anytown", + Coordinates = new[] { 39.781721f, -89.650148f }, + }, + Tags = new[] { "Developer", "Blogger" }, + }; + var result = QueryStringConverter.ToDeepObject(obj); + var expected = new List> + { + new("Name", "John"), + new("Age", "30"), + new("Address[Street]", "123 Main St"), + new("Address[City]", "Anytown"), + new("Address[Coordinates][0]", "39.78172"), + new("Address[Coordinates][1]", "-89.65015"), + new("Tags[0]", "Developer"), + new("Tags[1]", "Blogger"), + }; + Assert.That(result, Is.EqualTo(expected)); + } + + [Test] + public void ToQueryStringCollection_OnString_ThrowsException() + { + var exception = Assert.Throws(() => + QueryStringConverter.ToForm("invalid") + ); + Assert.That( + exception.Message, + Is.EqualTo( + "Only objects can be converted to query string collections. Given type is String." + ) + ); + } + + [Test] + public void ToQueryStringCollection_OnArray_ThrowsException() + { + var exception = Assert.Throws(() => + QueryStringConverter.ToForm(Array.Empty()) + ); + Assert.That( + exception.Message, + Is.EqualTo( + "Only objects can be converted to query string collections. Given type is Array." + ) + ); + } + + [Test] + public void ToQueryStringCollection_DeepObject_WithPrefix() + { + var obj = new + { + custom_session_id = "my-id", + system_prompt = "You are helpful", + variables = new { name = "Alice", age = 25 }, + }; + var result = QueryStringConverter.ToDeepObject("session_settings", obj); + var expected = new List> + { + new("session_settings[custom_session_id]", "my-id"), + new("session_settings[system_prompt]", "You are helpful"), + new("session_settings[variables][name]", "Alice"), + new("session_settings[variables][age]", "25"), + }; + Assert.That(result, Is.EqualTo(expected)); + } + + [Test] + public void ToQueryStringCollection_ExplodedForm_WithPrefix() + { + var obj = new { Name = "John", Tags = new[] { "Developer", "Blogger" } }; + var result = QueryStringConverter.ToExplodedForm("user", obj); + var expected = new List> + { + new("user[Name]", "John"), + new("user[Tags]", "Developer"), + new("user[Tags]", "Blogger"), + }; + Assert.That(result, Is.EqualTo(expected)); + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/RawClientTests/MultipartFormTests.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/RawClientTests/MultipartFormTests.cs new file mode 100644 index 000000000000..e65385d0e14d --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/RawClientTests/MultipartFormTests.cs @@ -0,0 +1,1121 @@ +using global::System.Net.Http; +using global::System.Text; +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedBasicAuthOptional.Core; +using SystemTask = global::System.Threading.Tasks.Task; + +namespace SeedBasicAuthOptional.Test.Core.RawClientTests; + +[TestFixture] +[Parallelizable(ParallelScope.Self)] +public class MultipartFormTests +{ + private static SimpleObject _simpleObject = new(); + + private static string _simpleFormEncoded = + "meta=data&Date=2023-10-01&Time=12:00:00&Duration=01:00:00&Id=1a1bb98f-47c6-407b-9481-78476affe52a&IsActive=true&Count=42&Initial=A&Values=data,2023-10-01,12:00:00,01:00:00,1a1bb98f-47c6-407b-9481-78476affe52a,true,42,A"; + + private static string _simpleExplodedFormEncoded = + "meta=data&Date=2023-10-01&Time=12:00:00&Duration=01:00:00&Id=1a1bb98f-47c6-407b-9481-78476affe52a&IsActive=true&Count=42&Initial=A&Values=data&Values=2023-10-01&Values=12:00:00&Values=01:00:00&Values=1a1bb98f-47c6-407b-9481-78476affe52a&Values=true&Values=42&Values=A"; + + private static ComplexObject _complexObject = new(); + + private static string _complexJson = """ + { + "meta": "data", + "Nested": { + "foo": "value" + }, + "NestedDictionary": { + "key": { + "foo": "value" + } + }, + "ListOfObjects": [ + { + "foo": "value" + }, + { + "foo": "value2" + } + ], + "Date": "2023-10-01", + "Time": "12:00:00", + "Duration": "01:00:00", + "Id": "1a1bb98f-47c6-407b-9481-78476affe52a", + "IsActive": true, + "Count": 42, + "Initial": "A" + } + """; + + [Test] + public async SystemTask ShouldAddStringPart() + { + const string partInput = "string content"; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddStringPart("string", partInput); + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: text/plain + Content-Disposition: form-data; name=string + + {partInput} + --{boundary}-- + """; + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddStringParts() + { + const string partInput = "string content"; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddStringParts("strings", [partInput, partInput]); + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: text/plain + Content-Disposition: form-data; name=strings + + {partInput} + --{boundary} + Content-Type: text/plain + Content-Disposition: form-data; name=strings + + {partInput} + --{boundary}-- + """; + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask GivenNull_ShouldNotAddStringPart() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddStringPart("string", null); + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + --{boundary}-- + """; + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddStringParts_WithNullsInList() + { + const string partInput = "string content"; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddStringParts("strings", [partInput, null, partInput]); + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: text/plain + Content-Disposition: form-data; name=strings + + {partInput} + --{boundary} + Content-Type: text/plain + Content-Disposition: form-data; name=strings + + {partInput} + --{boundary}-- + """; + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddStringPart_WithContentType() + { + const string partInput = "string content"; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddStringPart("string", partInput, "text/xml"); + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: text/xml + Content-Disposition: form-data; name=string + + {partInput} + --{boundary}-- + """; + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddStringPart_WithContentTypeAndCharset() + { + const string partInput = "string content"; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddStringPart("string", partInput, "text/xml; charset=utf-8"); + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: text/xml; charset=utf-8 + Content-Disposition: form-data; name=string + + {partInput} + --{boundary}-- + """; + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddStringParts_WithContentType() + { + const string partInput = "string content"; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddStringParts("strings", [partInput, partInput], "text/xml"); + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: text/xml + Content-Disposition: form-data; name=strings + + {partInput} + --{boundary} + Content-Type: text/xml + Content-Disposition: form-data; name=strings + + {partInput} + --{boundary}-- + """; + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddStringParts_WithContentTypeAndCharset() + { + const string partInput = "string content"; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddStringParts( + "strings", + [partInput, partInput], + "text/xml; charset=utf-8" + ); + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: text/xml; charset=utf-8 + Content-Disposition: form-data; name=strings + + {partInput} + --{boundary} + Content-Type: text/xml; charset=utf-8 + Content-Disposition: form-data; name=strings + + {partInput} + --{boundary}-- + """; + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFileParameter_WithFileName() + { + var (partInput, partExpectedString) = GetFileParameterTestData(); + var file = new FileParameter { Stream = partInput, FileName = "test.txt" }; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFileParameterPart("file", file); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/octet-stream + Content-Disposition: form-data; name=file; filename=test.txt; filename*=utf-8''test.txt + + {partExpectedString} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFileParameter_WithoutFileName() + { + var (partInput, partExpectedString) = GetFileParameterTestData(); + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFileParameterPart("file", partInput); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/octet-stream + Content-Disposition: form-data; name=file + + {partExpectedString} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFileParameter_WithContentType() + { + var (partInput, partExpectedString) = GetFileParameterTestData(); + var file = new FileParameter + { + Stream = partInput, + FileName = "test.txt", + ContentType = "text/plain", + }; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFileParameterPart("file", file, "ignored-fallback-content-type"); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: text/plain + Content-Disposition: form-data; name=file; filename=test.txt; filename*=utf-8''test.txt + + {partExpectedString} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFileParameter_WithContentTypeAndCharset() + { + var (partInput, partExpectedString) = GetFileParameterTestData(); + var file = new FileParameter + { + Stream = partInput, + FileName = "test.txt", + ContentType = "text/plain; charset=utf-8", + }; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFileParameterPart("file", file, "ignored-fallback-content-type"); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: text/plain; charset=utf-8 + Content-Disposition: form-data; name=file; filename=test.txt; filename*=utf-8''test.txt + + {partExpectedString} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFileParameter_WithFallbackContentType() + { + var (partInput, partExpectedString) = GetFileParameterTestData(); + var file = new FileParameter { Stream = partInput, FileName = "test.txt" }; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFileParameterPart("file", file, "text/plain"); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: text/plain + Content-Disposition: form-data; name=file; filename=test.txt; filename*=utf-8''test.txt + + {partExpectedString} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFileParameter_WithFallbackContentTypeAndCharset() + { + var (partInput, partExpectedString) = GetFileParameterTestData(); + var file = new FileParameter { Stream = partInput, FileName = "test.txt" }; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFileParameterPart("file", file, "text/plain; charset=utf-8"); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: text/plain; charset=utf-8 + Content-Disposition: form-data; name=file; filename=test.txt; filename*=utf-8''test.txt + + {partExpectedString} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFileParameters() + { + var (partInput1, partExpectedString1) = GetFileParameterTestData(); + var (partInput2, partExpectedString2) = GetFileParameterTestData(); + var file1 = new FileParameter { Stream = partInput1, FileName = "test1.txt" }; + var file2 = new FileParameter { Stream = partInput2, FileName = "test2.txt" }; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFileParameterParts("file", [file1, file2]); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/octet-stream + Content-Disposition: form-data; name=file; filename=test1.txt; filename*=utf-8''test1.txt + + {partExpectedString1} + --{boundary} + Content-Type: application/octet-stream + Content-Disposition: form-data; name=file; filename=test2.txt; filename*=utf-8''test2.txt + + {partExpectedString2} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFileParameters_WithNullsInList() + { + var (partInput1, partExpectedString1) = GetFileParameterTestData(); + var (partInput2, partExpectedString2) = GetFileParameterTestData(); + var file1 = new FileParameter { Stream = partInput1, FileName = "test1.txt" }; + var file2 = new FileParameter { Stream = partInput2, FileName = "test2.txt" }; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFileParameterParts("file", [file1, null, file2]); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/octet-stream + Content-Disposition: form-data; name=file; filename=test1.txt; filename*=utf-8''test1.txt + + {partExpectedString1} + --{boundary} + Content-Type: application/octet-stream + Content-Disposition: form-data; name=file; filename=test2.txt; filename*=utf-8''test2.txt + + {partExpectedString2} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask GivenNull_ShouldNotAddFileParameter() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFileParameterPart("file", null); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddJsonPart_WithComplexObject() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddJsonPart("object", _complexObject); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/json + Content-Disposition: form-data; name=object + + {_complexJson} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddJsonPart_WithComplexObjectList() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddJsonParts("objects", [_complexObject, _complexObject]); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/json + Content-Disposition: form-data; name=objects + + {_complexJson} + --{boundary} + Content-Type: application/json + Content-Disposition: form-data; name=objects + + {_complexJson} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask GivenNull_ShouldNotAddJsonPart() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddJsonPart("object", null); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddJsonParts_WithNullsInList() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddJsonParts("objects", [_complexObject, null]); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/json + Content-Disposition: form-data; name=objects + + {_complexJson} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddJsonParts_WithContentType() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddJsonParts("objects", [new { }], "application/json-patch+json"); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $$""" + --{{boundary}} + Content-Type: application/json-patch+json + Content-Disposition: form-data; name=objects + + {} + --{{boundary}}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFormEncodedParts_WithSimpleObject() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFormEncodedPart("object", _simpleObject); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=object + + {EscapeFormEncodedString(_simpleFormEncoded)} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFormEncodedParts_WithSimpleObjectList() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFormEncodedParts("objects", [_simpleObject, _simpleObject]); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=objects + + {EscapeFormEncodedString(_simpleFormEncoded)} + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=objects + + {EscapeFormEncodedString(_simpleFormEncoded)} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldNotAddFormEncodedParts_WithNull() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFormEncodedParts("object", null); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldNotAddFormEncodedParts_WithNullsInList() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFormEncodedParts("objects", [_simpleObject, null]); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=objects + + {EscapeFormEncodedString(_simpleFormEncoded)} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFormEncodedPart_WithContentType() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFormEncodedPart( + "objects", + new { foo = "bar" }, + "application/x-www-form-urlencoded" + ); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=objects + + foo=bar + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFormEncodedPart_WithContentTypeAndCharset() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFormEncodedPart( + "objects", + new { foo = "bar" }, + "application/x-www-form-urlencoded; charset=utf-8" + ); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded; charset=utf-8 + Content-Disposition: form-data; name=objects + + foo=bar + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFormEncodedParts_WithContentType() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFormEncodedParts( + "objects", + [new { foo = "bar" }], + "application/x-www-form-urlencoded" + ); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=objects + + foo=bar + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFormEncodedParts_WithContentTypeAndCharset() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFormEncodedParts( + "objects", + [new { foo = "bar" }], + "application/x-www-form-urlencoded; charset=utf-8" + ); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded; charset=utf-8 + Content-Disposition: form-data; name=objects + + foo=bar + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddExplodedFormEncodedParts_WithSimpleObject() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddExplodedFormEncodedPart("object", _simpleObject); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=object + + {EscapeFormEncodedString(_simpleExplodedFormEncoded)} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddExplodedFormEncodedParts_WithSimpleObjectList() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddExplodedFormEncodedParts("objects", [_simpleObject, _simpleObject]); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=objects + + {EscapeFormEncodedString(_simpleExplodedFormEncoded)} + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=objects + + {EscapeFormEncodedString(_simpleExplodedFormEncoded)} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldNotAddExplodedFormEncodedParts_WithNull() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddExplodedFormEncodedPart("object", null); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldNotAddExplodedFormEncodedParts_WithNullsInList() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddExplodedFormEncodedParts("objects", [_simpleObject, null]); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=objects + + {EscapeFormEncodedString(_simpleExplodedFormEncoded)} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddExplodedFormEncodedPart_WithContentType() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddExplodedFormEncodedPart( + "objects", + new { foo = "bar" }, + "application/x-www-form-urlencoded" + ); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=objects + + foo=bar + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddExplodedFormEncodedPart_WithContentTypeAndCharset() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddExplodedFormEncodedPart( + "objects", + new { foo = "bar" }, + "application/x-www-form-urlencoded; charset=utf-8" + ); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded; charset=utf-8 + Content-Disposition: form-data; name=objects + + foo=bar + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddExplodedFormEncodedParts_WithContentType() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddExplodedFormEncodedParts( + "objects", + [new { foo = "bar" }], + "application/x-www-form-urlencoded" + ); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=objects + + foo=bar + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddExplodedFormEncodedParts_WithContentTypeAndCharset() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddExplodedFormEncodedParts( + "objects", + [new { foo = "bar" }], + "application/x-www-form-urlencoded; charset=utf-8" + ); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded; charset=utf-8 + Content-Disposition: form-data; name=objects + + foo=bar + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + private static string EscapeFormEncodedString(string input) + { + return string.Join( + "&", + input + .Split('&') + .Select(x => x.Split('=')) + .Select(x => $"{Uri.EscapeDataString(x[0])}={Uri.EscapeDataString(x[1])}") + ); + } + + private static string GetBoundary(MultipartFormDataContent content) + { + return content + .Headers.ContentType?.Parameters.Single(p => + p.Name.Equals("boundary", StringComparison.OrdinalIgnoreCase) + ) + .Value?.Trim('"') + ?? throw new global::System.Exception("Boundary not found"); + } + + private static SeedBasicAuthOptional.Core.MultipartFormRequest CreateMultipartFormRequest() + { + return new SeedBasicAuthOptional.Core.MultipartFormRequest + { + BaseUrl = "https://localhost", + Method = HttpMethod.Post, + Path = "", + }; + } + + private static (Stream partInput, string partExpectedString) GetFileParameterTestData() + { + const string partExpectedString = "file content"; + var partInput = new MemoryStream(Encoding.Default.GetBytes(partExpectedString)); + return (partInput, partExpectedString); + } + + private class SimpleObject + { + [JsonPropertyName("meta")] + public string Meta { get; set; } = "data"; + public DateOnly Date { get; set; } = DateOnly.Parse("2023-10-01"); + public TimeOnly Time { get; set; } = TimeOnly.Parse("12:00:00"); + public TimeSpan Duration { get; set; } = TimeSpan.FromHours(1); + public Guid Id { get; set; } = Guid.Parse("1a1bb98f-47c6-407b-9481-78476affe52a"); + public bool IsActive { get; set; } = true; + public int Count { get; set; } = 42; + public char Initial { get; set; } = 'A'; + public IEnumerable Values { get; set; } = + [ + "data", + DateOnly.Parse("2023-10-01"), + TimeOnly.Parse("12:00:00"), + TimeSpan.FromHours(1), + Guid.Parse("1a1bb98f-47c6-407b-9481-78476affe52a"), + true, + 42, + 'A', + ]; + } + + private class ComplexObject + { + [JsonPropertyName("meta")] + public string Meta { get; set; } = "data"; + + public object Nested { get; set; } = new { foo = "value" }; + + public Dictionary NestedDictionary { get; set; } = + new() { { "key", new { foo = "value" } } }; + + public IEnumerable ListOfObjects { get; set; } = + new List { new { foo = "value" }, new { foo = "value2" } }; + + public DateOnly Date { get; set; } = DateOnly.Parse("2023-10-01"); + public TimeOnly Time { get; set; } = TimeOnly.Parse("12:00:00"); + public TimeSpan Duration { get; set; } = TimeSpan.FromHours(1); + public Guid Id { get; set; } = Guid.Parse("1a1bb98f-47c6-407b-9481-78476affe52a"); + public bool IsActive { get; set; } = true; + public int Count { get; set; } = 42; + public char Initial { get; set; } = 'A'; + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/RawClientTests/QueryParameterTests.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/RawClientTests/QueryParameterTests.cs new file mode 100644 index 000000000000..7362c5e111d5 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/RawClientTests/QueryParameterTests.cs @@ -0,0 +1,108 @@ +using NUnit.Framework; +using SeedBasicAuthOptional.Core; + +namespace SeedBasicAuthOptional.Test.Core.RawClientTests; + +[TestFixture] +[Parallelizable(ParallelScope.Self)] +public class QueryParameterTests +{ + [Test] + public void QueryParameters_BasicParameters() + { + var queryString = new QueryStringBuilder.Builder() + .Add("foo", "bar") + .Add("baz", "qux") + .Build(); + + Assert.That(queryString, Is.EqualTo("?foo=bar&baz=qux")); + } + + [Test] + public void QueryParameters_SpecialCharacterEscaping() + { + var queryString = new QueryStringBuilder.Builder() + .Add("email", "bob+test@example.com") + .Add("%Complete", "100") + .Add("space test", "hello world") + .Build(); + + Assert.That(queryString, Does.Contain("email=bob%2Btest%40example.com")); + Assert.That(queryString, Does.Contain("%25Complete=100")); + Assert.That(queryString, Does.Contain("space%20test=hello%20world")); + } + + [Test] + public void QueryParameters_MergeAdditionalParameters() + { + var queryString = new QueryStringBuilder.Builder() + .Add("sdk", "param") + .MergeAdditional(new List> { new("user", "value") }) + .Build(); + + Assert.That(queryString, Does.Contain("sdk=param")); + Assert.That(queryString, Does.Contain("user=value")); + } + + [Test] + public void QueryParameters_AdditionalOverridesSdk() + { + var queryString = new QueryStringBuilder.Builder() + .Add("foo", "sdk_value") + .MergeAdditional(new List> { new("foo", "user_override") }) + .Build(); + + Assert.That(queryString, Does.Contain("foo=user_override")); + Assert.That(queryString, Does.Not.Contain("sdk_value")); + } + + [Test] + public void QueryParameters_AdditionalMultipleValues() + { + var queryString = new QueryStringBuilder.Builder() + .Add("foo", "sdk_value") + .MergeAdditional( + new List> { new("foo", "user1"), new("foo", "user2") } + ) + .Build(); + + Assert.That(queryString, Does.Contain("foo=user1")); + Assert.That(queryString, Does.Contain("foo=user2")); + Assert.That(queryString, Does.Not.Contain("sdk_value")); + } + + [Test] + public void QueryParameters_OnlyAdditionalParameters() + { + var queryString = new QueryStringBuilder.Builder() + .MergeAdditional( + new List> { new("foo", "bar"), new("baz", "qux") } + ) + .Build(); + + Assert.That(queryString, Does.Contain("foo=bar")); + Assert.That(queryString, Does.Contain("baz=qux")); + } + + [Test] + public void QueryParameters_EmptyAdditionalParameters() + { + var queryString = new QueryStringBuilder.Builder() + .Add("foo", "bar") + .MergeAdditional(new List>()) + .Build(); + + Assert.That(queryString, Is.EqualTo("?foo=bar")); + } + + [Test] + public void QueryParameters_NullAdditionalParameters() + { + var queryString = new QueryStringBuilder.Builder() + .Add("foo", "bar") + .MergeAdditional(null) + .Build(); + + Assert.That(queryString, Is.EqualTo("?foo=bar")); + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/RawClientTests/RetriesTests.cs new file mode 100644 index 000000000000..bd03b5d5b477 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/RawClientTests/RetriesTests.cs @@ -0,0 +1,406 @@ +using global::System.Net.Http; +using global::System.Text.Json; +using NUnit.Framework; +using SeedBasicAuthOptional.Core; +using WireMock.Server; +using SystemTask = global::System.Threading.Tasks.Task; +using WireMockRequest = WireMock.RequestBuilders.Request; +using WireMockResponse = WireMock.ResponseBuilders.Response; + +namespace SeedBasicAuthOptional.Test.Core.RawClientTests; + +[TestFixture] +[Parallelizable(ParallelScope.Self)] +public class RetriesTests +{ + private const int MaxRetries = 3; + private WireMockServer _server; + private HttpClient _httpClient; + private RawClient _rawClient; + private string _baseUrl; + + [SetUp] + public void SetUp() + { + _server = WireMockServer.Start(); + _baseUrl = _server.Url ?? ""; + _httpClient = new HttpClient { BaseAddress = new Uri(_baseUrl) }; + _rawClient = new RawClient( + new ClientOptions { HttpClient = _httpClient, MaxRetries = MaxRetries } + ) + { + BaseRetryDelay = 0, + }; + } + + [Test] + [TestCase(408)] + [TestCase(429)] + [TestCase(500)] + [TestCase(504)] + public async SystemTask SendRequestAsync_ShouldRetry_OnRetryableStatusCodes(int statusCode) + { + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingGet()) + .InScenario("Retry") + .WillSetStateTo("Server Error") + .RespondWith(WireMockResponse.Create().WithStatusCode(statusCode)); + + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingGet()) + .InScenario("Retry") + .WhenStateIs("Server Error") + .WillSetStateTo("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(statusCode)); + + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingGet()) + .InScenario("Retry") + .WhenStateIs("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(200).WithBody("Success")); + + var request = new EmptyRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Get, + Path = "/test", + }; + + var response = await _rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(200)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + using (Assert.EnterMultipleScope()) + { + Assert.That(content, Is.EqualTo("Success")); + + Assert.That(_server.LogEntries, Has.Count.EqualTo(MaxRetries)); + } + } + + [Test] + [TestCase(400)] + [TestCase(409)] + public async SystemTask SendRequestAsync_ShouldRetry_OnNonRetryableStatusCodes(int statusCode) + { + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingGet()) + .InScenario("Retry") + .WillSetStateTo("Server Error") + .RespondWith(WireMockResponse.Create().WithStatusCode(statusCode).WithBody("Failure")); + + var request = new JsonRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Get, + Path = "/test", + Body = new { }, + }; + + var response = await _rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(statusCode)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + Assert.Multiple(() => + { + Assert.That(content, Is.EqualTo("Failure")); + + Assert.That(_server.LogEntries, Has.Count.EqualTo(1)); + }); + } + + [Test] + public async SystemTask SendRequestAsync_ShouldNotRetry_WithStreamRequest() + { + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) + .InScenario("Retry") + .WillSetStateTo("Server Error") + .RespondWith(WireMockResponse.Create().WithStatusCode(429).WithBody("Failure")); + + var request = new StreamRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Post, + Path = "/test", + Body = new MemoryStream(), + }; + + var response = await _rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(429)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + Assert.Multiple(() => + { + Assert.That(content, Is.EqualTo("Failure")); + Assert.That(_server.LogEntries, Has.Count.EqualTo(1)); + }); + } + + [Test] + public async SystemTask SendRequestAsync_ShouldNotRetry_WithMultiPartFormRequest_WithStream() + { + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) + .InScenario("Retry") + .WillSetStateTo("Server Error") + .RespondWith(WireMockResponse.Create().WithStatusCode(429).WithBody("Failure")); + + var request = new SeedBasicAuthOptional.Core.MultipartFormRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Post, + Path = "/test", + }; + request.AddFileParameterPart("file", new MemoryStream()); + + var response = await _rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(429)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + Assert.Multiple(() => + { + Assert.That(content, Is.EqualTo("Failure")); + Assert.That(_server.LogEntries, Has.Count.EqualTo(1)); + }); + } + + [Test] + public async SystemTask SendRequestAsync_ShouldRetry_WithMultiPartFormRequest_WithoutStream() + { + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) + .InScenario("Retry") + .WillSetStateTo("Server Error") + .RespondWith(WireMockResponse.Create().WithStatusCode(429)); + + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) + .InScenario("Retry") + .WhenStateIs("Server Error") + .WillSetStateTo("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(429)); + + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) + .InScenario("Retry") + .WhenStateIs("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(200).WithBody("Success")); + + var request = new SeedBasicAuthOptional.Core.MultipartFormRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Post, + Path = "/test", + }; + request.AddJsonPart("object", new { }); + + var response = await _rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(200)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + Assert.Multiple(() => + { + Assert.That(content, Is.EqualTo("Success")); + Assert.That(_server.LogEntries, Has.Count.EqualTo(MaxRetries)); + }); + } + + [Test] + public async SystemTask SendRequestAsync_ShouldRespectRetryAfterHeader_WithSecondsValue() + { + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingGet()) + .InScenario("RetryAfter") + .WillSetStateTo("Success") + .RespondWith( + WireMockResponse.Create().WithStatusCode(429).WithHeader("Retry-After", "1") + ); + + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingGet()) + .InScenario("RetryAfter") + .WhenStateIs("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(200).WithBody("Success")); + + var request = new EmptyRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Get, + Path = "/test", + }; + + var response = await _rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(200)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + Assert.Multiple(() => + { + Assert.That(content, Is.EqualTo("Success")); + Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); + }); + } + + [Test] + public async SystemTask SendRequestAsync_ShouldRespectRetryAfterHeader_WithHttpDateValue() + { + var retryAfterDate = DateTimeOffset.UtcNow.AddSeconds(1).ToString("R"); + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingGet()) + .InScenario("RetryAfterDate") + .WillSetStateTo("Success") + .RespondWith( + WireMockResponse + .Create() + .WithStatusCode(429) + .WithHeader("Retry-After", retryAfterDate) + ); + + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingGet()) + .InScenario("RetryAfterDate") + .WhenStateIs("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(200).WithBody("Success")); + + var request = new EmptyRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Get, + Path = "/test", + }; + + var response = await _rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(200)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + Assert.Multiple(() => + { + Assert.That(content, Is.EqualTo("Success")); + Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); + }); + } + + [Test] + public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() + { + var resetTime = DateTimeOffset.UtcNow.AddSeconds(1).ToUnixTimeSeconds().ToString(); + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingGet()) + .InScenario("RateLimitReset") + .WillSetStateTo("Success") + .RespondWith( + WireMockResponse + .Create() + .WithStatusCode(429) + .WithHeader("X-RateLimit-Reset", resetTime) + ); + + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingGet()) + .InScenario("RateLimitReset") + .WhenStateIs("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(200).WithBody("Success")); + + var request = new EmptyRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Get, + Path = "/test", + }; + + var response = await _rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(200)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + Assert.Multiple(() => + { + Assert.That(content, Is.EqualTo("Success")); + Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); + }); + } + + [Test] + public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() + { + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) + .InScenario("RetryWithBody") + .WillSetStateTo("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(500)); + + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) + .InScenario("RetryWithBody") + .WhenStateIs("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(200).WithBody("Success")); + + var request = new JsonRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Post, + Path = "/test", + Body = new { key = "value" }, + }; + + var response = await _rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(200)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + using (Assert.EnterMultipleScope()) + { + Assert.That(content, Is.EqualTo("Success")); + Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); + + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) + var retriedEntry = _server.LogEntries.ElementAt(1); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); + } + } + + [Test] + public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() + { + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) + .InScenario("RetryMultipart") + .WillSetStateTo("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(500)); + + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) + .InScenario("RetryMultipart") + .WhenStateIs("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(200).WithBody("Success")); + + var request = new SeedBasicAuthOptional.Core.MultipartFormRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Post, + Path = "/test", + }; + request.AddJsonPart("object", new { key = "value" }); + + var response = await _rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(200)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + using (Assert.EnterMultipleScope()) + { + Assert.That(content, Is.EqualTo("Success")); + Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); + + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) + var retriedEntry = _server.LogEntries.ElementAt(1); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); + } + } + + [TearDown] + public void TearDown() + { + _server.Dispose(); + _httpClient.Dispose(); + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/WithRawResponseTests.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/WithRawResponseTests.cs new file mode 100644 index 000000000000..07deaaab2932 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/WithRawResponseTests.cs @@ -0,0 +1,269 @@ +using global::System.Net; +using global::System.Net.Http.Headers; +using NUnit.Framework; +using SeedBasicAuthOptional; +using SeedBasicAuthOptional.Core; + +namespace SeedBasicAuthOptional.Test.Core; + +[TestFixture] +public class WithRawResponseTests +{ + [Test] + public async global::System.Threading.Tasks.Task WithRawResponseTask_DirectAwait_ReturnsData() + { + // Arrange + var expectedData = "test-data"; + var task = CreateWithRawResponseTask(expectedData, HttpStatusCode.OK); + + // Act + var result = await task; + + // Assert + Assert.That(result, Is.EqualTo(expectedData)); + } + + [Test] + public async global::System.Threading.Tasks.Task WithRawResponseTask_WithRawResponse_ReturnsDataAndMetadata() + { + // Arrange + var expectedData = "test-data"; + var expectedStatusCode = HttpStatusCode.Created; + var task = CreateWithRawResponseTask(expectedData, expectedStatusCode); + + // Act + var result = await task.WithRawResponse(); + + // Assert + Assert.That(result.Data, Is.EqualTo(expectedData)); + Assert.That(result.RawResponse.StatusCode, Is.EqualTo(expectedStatusCode)); + Assert.That(result.RawResponse.Url, Is.Not.Null); + } + + [Test] + public async global::System.Threading.Tasks.Task ResponseHeaders_TryGetValue_CaseInsensitive() + { + // Arrange + using var response = CreateHttpResponse(HttpStatusCode.OK); + response.Headers.Add("X-Request-Id", "12345"); + var headers = ResponseHeaders.FromHttpResponseMessage(response); + + // Act & Assert + Assert.That(headers.TryGetValue("X-Request-Id", out var value), Is.True); + Assert.That(value, Is.EqualTo("12345")); + + Assert.That(headers.TryGetValue("x-request-id", out value), Is.True); + Assert.That(value, Is.EqualTo("12345")); + + Assert.That(headers.TryGetValue("X-REQUEST-ID", out value), Is.True); + Assert.That(value, Is.EqualTo("12345")); + } + + [Test] + public async global::System.Threading.Tasks.Task ResponseHeaders_TryGetValues_ReturnsMultipleValues() + { + // Arrange + using var response = CreateHttpResponse(HttpStatusCode.OK); + response.Headers.Add("Set-Cookie", new[] { "cookie1=value1", "cookie2=value2" }); + var headers = ResponseHeaders.FromHttpResponseMessage(response); + + // Act + var success = headers.TryGetValues("Set-Cookie", out var values); + + // Assert + Assert.That(success, Is.True); + Assert.That(values, Is.Not.Null); + Assert.That(values!.Count(), Is.EqualTo(2)); + Assert.That(values, Does.Contain("cookie1=value1")); + Assert.That(values, Does.Contain("cookie2=value2")); + } + + [Test] + public async global::System.Threading.Tasks.Task ResponseHeaders_ContentType_ReturnsValue() + { + // Arrange + using var response = CreateHttpResponse(HttpStatusCode.OK); + response.Content = new StringContent( + "{}", + global::System.Text.Encoding.UTF8, + "application/json" + ); + var headers = ResponseHeaders.FromHttpResponseMessage(response); + + // Act + var contentType = headers.ContentType; + + // Assert + Assert.That(contentType, Is.Not.Null); + Assert.That(contentType, Does.Contain("application/json")); + } + + [Test] + public async global::System.Threading.Tasks.Task ResponseHeaders_ContentLength_ReturnsValue() + { + // Arrange + var content = "test content"; + using var response = CreateHttpResponse(HttpStatusCode.OK); + response.Content = new StringContent(content); + var headers = ResponseHeaders.FromHttpResponseMessage(response); + + // Act + var contentLength = headers.ContentLength; + + // Assert + Assert.That(contentLength, Is.Not.Null); + Assert.That(contentLength, Is.GreaterThan(0)); + } + + [Test] + public async global::System.Threading.Tasks.Task ResponseHeaders_Contains_ReturnsTrueForExistingHeader() + { + // Arrange + using var response = CreateHttpResponse(HttpStatusCode.OK); + response.Headers.Add("X-Custom-Header", "value"); + var headers = ResponseHeaders.FromHttpResponseMessage(response); + + // Act & Assert + Assert.That(headers.Contains("X-Custom-Header"), Is.True); + Assert.That(headers.Contains("x-custom-header"), Is.True); + Assert.That(headers.Contains("NonExistent"), Is.False); + } + + [Test] + public async global::System.Threading.Tasks.Task ResponseHeaders_Enumeration_IncludesAllHeaders() + { + // Arrange + using var response = CreateHttpResponse(HttpStatusCode.OK); + response.Headers.Add("X-Header-1", "value1"); + response.Headers.Add("X-Header-2", "value2"); + response.Content = new StringContent("test"); + var headers = ResponseHeaders.FromHttpResponseMessage(response); + + // Act + var allHeaders = headers.ToList(); + + // Assert + Assert.That(allHeaders.Count, Is.GreaterThan(0)); + Assert.That(allHeaders.Any(h => h.Name == "X-Header-1"), Is.True); + Assert.That(allHeaders.Any(h => h.Name == "X-Header-2"), Is.True); + } + + [Test] + public async global::System.Threading.Tasks.Task WithRawResponseTask_ErrorStatusCode_StillReturnsMetadata() + { + // Arrange + var expectedData = "error-data"; + var task = CreateWithRawResponseTask(expectedData, HttpStatusCode.BadRequest); + + // Act + var result = await task.WithRawResponse(); + + // Assert + Assert.That(result.Data, Is.EqualTo(expectedData)); + Assert.That(result.RawResponse.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); + } + + [Test] + public async global::System.Threading.Tasks.Task WithRawResponseTask_Url_IsPreserved() + { + // Arrange + var expectedUrl = new Uri("https://api.example.com/users/123"); + var task = CreateWithRawResponseTask("data", HttpStatusCode.OK, expectedUrl); + + // Act + var result = await task.WithRawResponse(); + + // Assert + Assert.That(result.RawResponse.Url, Is.EqualTo(expectedUrl)); + } + + [Test] + public async global::System.Threading.Tasks.Task ResponseHeaders_TryGetValue_NonExistentHeader_ReturnsFalse() + { + // Arrange + using var response = CreateHttpResponse(HttpStatusCode.OK); + var headers = ResponseHeaders.FromHttpResponseMessage(response); + + // Act + var success = headers.TryGetValue("X-NonExistent", out var value); + + // Assert + Assert.That(success, Is.False); + Assert.That(value, Is.Null); + } + + [Test] + public async global::System.Threading.Tasks.Task ResponseHeaders_TryGetValues_NonExistentHeader_ReturnsFalse() + { + // Arrange + using var response = CreateHttpResponse(HttpStatusCode.OK); + var headers = ResponseHeaders.FromHttpResponseMessage(response); + + // Act + var success = headers.TryGetValues("X-NonExistent", out var values); + + // Assert + Assert.That(success, Is.False); + Assert.That(values, Is.Null); + } + + [Test] + public async global::System.Threading.Tasks.Task WithRawResponseTask_ImplicitConversion_ToTask() + { + // Arrange + var expectedData = "test-data"; + var task = CreateWithRawResponseTask(expectedData, HttpStatusCode.OK); + + // Act - implicitly convert to Task + global::System.Threading.Tasks.Task regularTask = task; + var result = await regularTask; + + // Assert + Assert.That(result, Is.EqualTo(expectedData)); + } + + [Test] + public void WithRawResponseTask_ImplicitConversion_AssignToTaskVariable() + { + // Arrange + var expectedData = "test-data"; + var wrappedTask = CreateWithRawResponseTask(expectedData, HttpStatusCode.OK); + + // Act - assign to Task variable + global::System.Threading.Tasks.Task regularTask = wrappedTask; + + // Assert + Assert.That(regularTask, Is.Not.Null); + Assert.That(regularTask, Is.InstanceOf>()); + } + + // Helper methods + + private static WithRawResponseTask CreateWithRawResponseTask( + T data, + HttpStatusCode statusCode, + Uri? url = null + ) + { + url ??= new Uri("https://api.example.com/test"); + using var httpResponse = CreateHttpResponse(statusCode); + httpResponse.RequestMessage = new HttpRequestMessage(HttpMethod.Get, url); + + var rawResponse = new RawResponse + { + StatusCode = statusCode, + Url = url, + Headers = ResponseHeaders.FromHttpResponseMessage(httpResponse), + }; + + var withRawResponse = new WithRawResponse { Data = data, RawResponse = rawResponse }; + + var task = global::System.Threading.Tasks.Task.FromResult(withRawResponse); + return new WithRawResponseTask(task); + } + + private static HttpResponseMessage CreateHttpResponse(HttpStatusCode statusCode) + { + return new HttpResponseMessage(statusCode) { Content = new StringContent("") }; + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/SeedBasicAuthOptional.Test.Custom.props b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/SeedBasicAuthOptional.Test.Custom.props new file mode 100644 index 000000000000..aac9b5020d80 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/SeedBasicAuthOptional.Test.Custom.props @@ -0,0 +1,6 @@ + + diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/SeedBasicAuthOptional.Test.csproj b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/SeedBasicAuthOptional.Test.csproj new file mode 100644 index 000000000000..2ffd45f0bd14 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/SeedBasicAuthOptional.Test.csproj @@ -0,0 +1,39 @@ + + + net9.0 + 12 + enable + enable + false + true + true + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/TestClient.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/TestClient.cs new file mode 100644 index 000000000000..378fc6e838e2 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/TestClient.cs @@ -0,0 +1,6 @@ +using NUnit.Framework; + +namespace SeedBasicAuthOptional.Test; + +[TestFixture] +public class TestClient; diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Unit/MockServer/BaseMockServerTest.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Unit/MockServer/BaseMockServerTest.cs new file mode 100644 index 000000000000..c52d8138a3d3 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Unit/MockServer/BaseMockServerTest.cs @@ -0,0 +1,39 @@ +using NUnit.Framework; +using SeedBasicAuthOptional; +using WireMock.Logging; +using WireMock.Server; +using WireMock.Settings; + +namespace SeedBasicAuthOptional.Test.Unit.MockServer; + +public class BaseMockServerTest +{ + protected WireMockServer Server { get; set; } = null!; + + protected SeedBasicAuthOptionalClient Client { get; set; } = null!; + + protected RequestOptions RequestOptions { get; set; } = new(); + + [OneTimeSetUp] + public void GlobalSetup() + { + // Start the WireMock server + Server = WireMockServer.Start( + new WireMockServerSettings { Logger = new WireMockConsoleLogger() } + ); + + // Initialize the Client + Client = new SeedBasicAuthOptionalClient( + "USERNAME", + "PASSWORD", + clientOptions: new ClientOptions { BaseUrl = Server.Urls[0], MaxRetries = 0 } + ); + } + + [OneTimeTearDown] + public void GlobalTeardown() + { + Server.Stop(); + Server.Dispose(); + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Unit/MockServer/BasicAuth/GetWithBasicAuthTest.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Unit/MockServer/BasicAuth/GetWithBasicAuthTest.cs new file mode 100644 index 000000000000..15a25ad49511 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Unit/MockServer/BasicAuth/GetWithBasicAuthTest.cs @@ -0,0 +1,50 @@ +using NUnit.Framework; +using SeedBasicAuthOptional.Test.Unit.MockServer; +using SeedBasicAuthOptional.Test.Utils; + +namespace SeedBasicAuthOptional.Test.Unit.MockServer.BasicAuth; + +[TestFixture] +[Parallelizable(ParallelScope.Self)] +public class GetWithBasicAuthTest : BaseMockServerTest +{ + [NUnit.Framework.Test] + public async Task MockServerTest_1() + { + const string mockResponse = """ + true + """; + + Server + .Given(WireMock.RequestBuilders.Request.Create().WithPath("/basic-auth").UsingGet()) + .RespondWith( + WireMock + .ResponseBuilders.Response.Create() + .WithStatusCode(200) + .WithBody(mockResponse) + ); + + var response = await Client.BasicAuth.GetWithBasicAuthAsync(); + JsonAssert.AreEqual(response, mockResponse); + } + + [NUnit.Framework.Test] + public async Task MockServerTest_2() + { + const string mockResponse = """ + true + """; + + Server + .Given(WireMock.RequestBuilders.Request.Create().WithPath("/basic-auth").UsingGet()) + .RespondWith( + WireMock + .ResponseBuilders.Response.Create() + .WithStatusCode(200) + .WithBody(mockResponse) + ); + + var response = await Client.BasicAuth.GetWithBasicAuthAsync(); + JsonAssert.AreEqual(response, mockResponse); + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Unit/MockServer/BasicAuth/PostWithBasicAuthTest.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Unit/MockServer/BasicAuth/PostWithBasicAuthTest.cs new file mode 100644 index 000000000000..0eb28eb4a585 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Unit/MockServer/BasicAuth/PostWithBasicAuthTest.cs @@ -0,0 +1,78 @@ +using NUnit.Framework; +using SeedBasicAuthOptional.Test.Unit.MockServer; +using SeedBasicAuthOptional.Test.Utils; + +namespace SeedBasicAuthOptional.Test.Unit.MockServer.BasicAuth; + +[TestFixture] +[Parallelizable(ParallelScope.Self)] +public class PostWithBasicAuthTest : BaseMockServerTest +{ + [NUnit.Framework.Test] + public async Task MockServerTest_1() + { + const string requestJson = """ + { + "key": "value" + } + """; + + const string mockResponse = """ + true + """; + + Server + .Given( + WireMock + .RequestBuilders.Request.Create() + .WithPath("/basic-auth") + .UsingPost() + .WithBodyAsJson(requestJson) + ) + .RespondWith( + WireMock + .ResponseBuilders.Response.Create() + .WithStatusCode(200) + .WithBody(mockResponse) + ); + + var response = await Client.BasicAuth.PostWithBasicAuthAsync( + new Dictionary() { { "key", "value" } } + ); + JsonAssert.AreEqual(response, mockResponse); + } + + [NUnit.Framework.Test] + public async Task MockServerTest_2() + { + const string requestJson = """ + { + "key": "value" + } + """; + + const string mockResponse = """ + true + """; + + Server + .Given( + WireMock + .RequestBuilders.Request.Create() + .WithPath("/basic-auth") + .UsingPost() + .WithBodyAsJson(requestJson) + ) + .RespondWith( + WireMock + .ResponseBuilders.Response.Create() + .WithStatusCode(200) + .WithBody(mockResponse) + ); + + var response = await Client.BasicAuth.PostWithBasicAuthAsync( + new Dictionary() { { "key", "value" } } + ); + JsonAssert.AreEqual(response, mockResponse); + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/AdditionalPropertiesComparer.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/AdditionalPropertiesComparer.cs new file mode 100644 index 000000000000..1c5f0cf63e64 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/AdditionalPropertiesComparer.cs @@ -0,0 +1,219 @@ +using global::System.Text.Json; +using NUnit.Framework.Constraints; +using SeedBasicAuthOptional; +using SeedBasicAuthOptional.Core; + +namespace NUnit.Framework; + +/// +/// Extensions for EqualConstraint to handle AdditionalProperties values. +/// +public static class AdditionalPropertiesComparerExtensions +{ + /// + /// Modifies the EqualConstraint to handle AdditionalProperties instances by comparing their + /// serialized JSON representations. This handles the type mismatch between native C# types + /// and JsonElement values that occur when comparing manually constructed objects with + /// deserialized objects. + /// + /// The EqualConstraint to modify. + /// The same constraint instance for method chaining. + public static EqualConstraint UsingAdditionalPropertiesComparer(this EqualConstraint constraint) + { + constraint.Using( + (x, y) => + { + if (x.Count != y.Count) + { + return false; + } + + foreach (var key in x.Keys) + { + if (!y.ContainsKey(key)) + { + return false; + } + + var xElement = JsonUtils.SerializeToElement(x[key]); + var yElement = JsonUtils.SerializeToElement(y[key]); + + if (!JsonElementsAreEqual(xElement, yElement)) + { + return false; + } + } + + return true; + } + ); + + return constraint; + } + + /// + /// Modifies the EqualConstraint to handle Dictionary<string, object?> values by comparing + /// their serialized JSON representations. This handles the type mismatch between native C# types + /// and JsonElement values that occur when comparing manually constructed objects with + /// deserialized objects. + /// + /// The EqualConstraint to modify. + /// The same constraint instance for method chaining. + public static EqualConstraint UsingObjectDictionaryComparer(this EqualConstraint constraint) + { + constraint.Using>( + (x, y) => + { + if (x.Count != y.Count) + { + return false; + } + + foreach (var key in x.Keys) + { + if (!y.ContainsKey(key)) + { + return false; + } + + var xElement = JsonUtils.SerializeToElement(x[key]); + var yElement = JsonUtils.SerializeToElement(y[key]); + + if (!JsonElementsAreEqual(xElement, yElement)) + { + return false; + } + } + + return true; + } + ); + + return constraint; + } + + internal static bool JsonElementsAreEqualPublic(JsonElement x, JsonElement y) => + JsonElementsAreEqual(x, y); + + private static bool JsonElementsAreEqual(JsonElement x, JsonElement y) + { + if (x.ValueKind != y.ValueKind) + { + return false; + } + + return x.ValueKind switch + { + JsonValueKind.Object => CompareJsonObjects(x, y), + JsonValueKind.Array => CompareJsonArrays(x, y), + JsonValueKind.String => x.GetString() == y.GetString(), + JsonValueKind.Number => x.GetDecimal() == y.GetDecimal(), + JsonValueKind.True => true, + JsonValueKind.False => true, + JsonValueKind.Null => true, + _ => false, + }; + } + + private static bool CompareJsonObjects(JsonElement x, JsonElement y) + { + var xProps = new Dictionary(); + var yProps = new Dictionary(); + + foreach (var prop in x.EnumerateObject()) + xProps[prop.Name] = prop.Value; + + foreach (var prop in y.EnumerateObject()) + yProps[prop.Name] = prop.Value; + + if (xProps.Count != yProps.Count) + { + return false; + } + + foreach (var key in xProps.Keys) + { + if (!yProps.ContainsKey(key)) + { + return false; + } + + if (!JsonElementsAreEqual(xProps[key], yProps[key])) + { + return false; + } + } + + return true; + } + + private static bool CompareJsonArrays(JsonElement x, JsonElement y) + { + var xArray = x.EnumerateArray().ToList(); + var yArray = y.EnumerateArray().ToList(); + + if (xArray.Count != yArray.Count) + { + return false; + } + + for (var i = 0; i < xArray.Count; i++) + { + if (!JsonElementsAreEqual(xArray[i], yArray[i])) + { + return false; + } + } + + return true; + } + + /// + /// Modifies the EqualConstraint to handle cross-type comparisons involving JsonElement. + /// When UsingPropertiesComparer() walks object properties and encounters a property typed as + /// 'object', the expected side may be a Dictionary<object, object?> while the actual + /// (deserialized) side is a JsonElement. These typed predicates bridge that gap by serializing + /// the non-JsonElement side and comparing JSON representations. + /// + /// Uses typed Func<TExpected, TActual, bool> predicates instead of a non-generic + /// IComparer/IEqualityComparer so that NUnit's CanCompare type check ensures these only + /// fire when one side is a JsonElement, letting UsingPropertiesComparer() handle all + /// same-type comparisons normally. + /// + /// The EqualConstraint to modify. + /// The same constraint instance for method chaining. + public static EqualConstraint UsingJsonSerializationComparer(this EqualConstraint constraint) + { + // Handle: expected is non-JsonElement, actual is JsonElement + constraint.Using( + (actualJsonElement, expectedObj) => + { + try + { + var expectedElement = JsonUtils.SerializeToElement(expectedObj); + return JsonElementsAreEqualPublic(expectedElement, actualJsonElement); + } + catch + { + return false; + } + } + ); + // Handle reverse: expected is JsonElement, actual is non-JsonElement + constraint.Using( + (actualObj, expectedJsonElement) => + { + try + { + var actualElement = JsonUtils.SerializeToElement(actualObj); + return JsonElementsAreEqualPublic(expectedJsonElement, actualElement); + } + catch + { + return false; + } + } + ); + return constraint; + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/JsonAssert.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/JsonAssert.cs new file mode 100644 index 000000000000..cccd122c8ed1 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/JsonAssert.cs @@ -0,0 +1,29 @@ +using global::System.Text.Json; +using NUnit.Framework; +using SeedBasicAuthOptional.Core; + +namespace SeedBasicAuthOptional.Test.Utils; + +internal static class JsonAssert +{ + /// + /// Asserts that the serialized JSON of an object equals the expected JSON string. + /// Uses JsonElement comparison for reliable deep equality of collections and union types. + /// + internal static void AreEqual(object actual, string expectedJson) + { + var actualElement = JsonUtils.SerializeToElement(actual); + var expectedElement = JsonUtils.Deserialize(expectedJson); + Assert.That(actualElement, Is.EqualTo(expectedElement).UsingJsonElementComparer()); + } + + /// + /// Asserts that the given JSON string survives a deserialization/serialization round-trip + /// intact: deserializes to T then re-serializes and compares to the original JSON. + /// + internal static void Roundtrips(string json) + { + var deserialized = JsonUtils.Deserialize(json); + AreEqual(deserialized!, json); + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/JsonElementComparer.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/JsonElementComparer.cs new file mode 100644 index 000000000000..a37ef402c1ac --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/JsonElementComparer.cs @@ -0,0 +1,236 @@ +using global::System.Text.Json; +using NUnit.Framework.Constraints; + +namespace NUnit.Framework; + +/// +/// Extensions for EqualConstraint to handle JsonElement objects. +/// +public static class JsonElementComparerExtensions +{ + /// + /// Extension method for comparing JsonElement objects in NUnit tests. + /// Property order doesn't matter, but array order does matter. + /// Includes special handling for DateTime string formats. + /// + /// The Is.EqualTo() constraint instance. + /// A constraint that can compare JsonElements with detailed diffs. + public static EqualConstraint UsingJsonElementComparer(this EqualConstraint constraint) + { + return constraint.Using(new JsonElementComparer()); + } +} + +/// +/// Equality comparer for JsonElement with detailed reporting. +/// Property order doesn't matter, but array order does matter. +/// Now includes special handling for DateTime string formats with improved null handling. +/// +public class JsonElementComparer : IEqualityComparer +{ + private string _failurePath = string.Empty; + + /// + public bool Equals(JsonElement x, JsonElement y) + { + _failurePath = string.Empty; + return CompareJsonElements(x, y, string.Empty); + } + + /// + public int GetHashCode(JsonElement obj) + { + return JsonSerializer.Serialize(obj).GetHashCode(); + } + + private bool CompareJsonElements(JsonElement x, JsonElement y, string path) + { + // If value kinds don't match, they're not equivalent + if (x.ValueKind != y.ValueKind) + { + _failurePath = $"{path}: Expected {x.ValueKind} but got {y.ValueKind}"; + return false; + } + + switch (x.ValueKind) + { + case JsonValueKind.Object: + return CompareJsonObjects(x, y, path); + + case JsonValueKind.Array: + return CompareJsonArraysInOrder(x, y, path); + + case JsonValueKind.String: + string? xStr = x.GetString(); + string? yStr = y.GetString(); + + // Handle null strings + if (xStr is null && yStr is null) + return true; + + if (xStr is null || yStr is null) + { + _failurePath = + $"{path}: Expected {(xStr is null ? "null" : $"\"{xStr}\"")} but got {(yStr is null ? "null" : $"\"{yStr}\"")}"; + return false; + } + + // Check if they are identical strings + if (xStr == yStr) + return true; + + // Try to handle DateTime strings + if (IsLikelyDateTimeString(xStr) && IsLikelyDateTimeString(yStr)) + { + if (AreEquivalentDateTimeStrings(xStr, yStr)) + return true; + } + + _failurePath = $"{path}: Expected \"{xStr}\" but got \"{yStr}\""; + return false; + + case JsonValueKind.Number: + if (x.GetDecimal() != y.GetDecimal()) + { + _failurePath = $"{path}: Expected {x.GetDecimal()} but got {y.GetDecimal()}"; + return false; + } + + return true; + + case JsonValueKind.True: + case JsonValueKind.False: + if (x.GetBoolean() != y.GetBoolean()) + { + _failurePath = $"{path}: Expected {x.GetBoolean()} but got {y.GetBoolean()}"; + return false; + } + + return true; + + case JsonValueKind.Null: + return true; + + default: + _failurePath = $"{path}: Unsupported JsonValueKind {x.ValueKind}"; + return false; + } + } + + private bool IsLikelyDateTimeString(string? str) + { + // Simple heuristic to identify likely ISO date time strings + return str is not null + && (str.Contains("T") && (str.EndsWith("Z") || str.Contains("+") || str.Contains("-"))); + } + + private bool AreEquivalentDateTimeStrings(string str1, string str2) + { + // Try to parse both as DateTime + if (DateTime.TryParse(str1, out DateTime dt1) && DateTime.TryParse(str2, out DateTime dt2)) + { + return dt1 == dt2; + } + + return false; + } + + private bool CompareJsonObjects(JsonElement x, JsonElement y, string path) + { + // Create dictionaries for both JSON objects + var xProps = new Dictionary(); + var yProps = new Dictionary(); + + foreach (var prop in x.EnumerateObject()) + xProps[prop.Name] = prop.Value; + + foreach (var prop in y.EnumerateObject()) + yProps[prop.Name] = prop.Value; + + // Check if all properties in x exist in y + foreach (var key in xProps.Keys) + { + if (!yProps.ContainsKey(key)) + { + _failurePath = $"{path}: Missing property '{key}'"; + return false; + } + } + + // Check if y has extra properties + foreach (var key in yProps.Keys) + { + if (!xProps.ContainsKey(key)) + { + _failurePath = $"{path}: Unexpected property '{key}'"; + return false; + } + } + + // Compare each property value + foreach (var key in xProps.Keys) + { + var propPath = string.IsNullOrEmpty(path) ? key : $"{path}.{key}"; + if (!CompareJsonElements(xProps[key], yProps[key], propPath)) + { + return false; + } + } + + return true; + } + + private bool CompareJsonArraysInOrder(JsonElement x, JsonElement y, string path) + { + var xArray = x.EnumerateArray(); + var yArray = y.EnumerateArray(); + + // Count x elements + var xCount = 0; + var xElements = new List(); + foreach (var item in xArray) + { + xElements.Add(item); + xCount++; + } + + // Count y elements + var yCount = 0; + var yElements = new List(); + foreach (var item in yArray) + { + yElements.Add(item); + yCount++; + } + + // Check if counts match + if (xCount != yCount) + { + _failurePath = $"{path}: Expected {xCount} items but found {yCount}"; + return false; + } + + // Compare elements in order + for (var i = 0; i < xCount; i++) + { + var itemPath = $"{path}[{i}]"; + if (!CompareJsonElements(xElements[i], yElements[i], itemPath)) + { + return false; + } + } + + return true; + } + + /// + public override string ToString() + { + if (!string.IsNullOrEmpty(_failurePath)) + { + return $"JSON comparison failed at {_failurePath}"; + } + + return "JsonElementEqualityComparer"; + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/NUnitExtensions.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/NUnitExtensions.cs new file mode 100644 index 000000000000..816f4c010e6e --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/NUnitExtensions.cs @@ -0,0 +1,32 @@ +using NUnit.Framework.Constraints; + +namespace NUnit.Framework; + +/// +/// Extensions for NUnit constraints. +/// +public static class NUnitExtensions +{ + /// + /// Modifies the EqualConstraint to use our own set of default comparers. + /// + /// + /// + public static EqualConstraint UsingDefaults(this EqualConstraint constraint) => + constraint + .UsingPropertiesComparer() + .UsingReadOnlyMemoryComparer() + .UsingReadOnlyMemoryComparer() + .UsingReadOnlyMemoryComparer() + .UsingReadOnlyMemoryComparer() + .UsingReadOnlyMemoryComparer() + .UsingReadOnlyMemoryComparer() + .UsingReadOnlyMemoryComparer() + .UsingReadOnlyMemoryComparer() + .UsingOneOfComparer() + .UsingJsonElementComparer() + .UsingOptionalComparer() + .UsingObjectDictionaryComparer() + .UsingAdditionalPropertiesComparer() + .UsingJsonSerializationComparer(); +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/OneOfComparer.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/OneOfComparer.cs new file mode 100644 index 000000000000..767439174363 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/OneOfComparer.cs @@ -0,0 +1,86 @@ +using NUnit.Framework.Constraints; +using OneOf; + +namespace NUnit.Framework; + +/// +/// Extensions for EqualConstraint to handle OneOf values. +/// +public static class EqualConstraintExtensions +{ + /// + /// Modifies the EqualConstraint to handle OneOf instances by comparing their inner values. + /// This works alongside other comparison modifiers like UsingPropertiesComparer. + /// + /// The EqualConstraint to modify. + /// The same constraint instance for method chaining. + public static EqualConstraint UsingOneOfComparer(this EqualConstraint constraint) + { + // Register a comparer factory for IOneOf types + constraint.Using( + (x, y) => + { + // ReSharper disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (x.Value is null && y.Value is null) + { + return true; + } + + if (x.Value is null) + { + return false; + } + + var propertiesComparer = new NUnitEqualityComparer(); + var tolerance = Tolerance.Default; + propertiesComparer.CompareProperties = true; + // Add OneOf comparer to handle nested OneOf values (e.g., in Lists) + propertiesComparer.ExternalComparers.Add( + new OneOfEqualityAdapter(propertiesComparer) + ); + return propertiesComparer.AreEqual(x.Value, y.Value, ref tolerance); + } + ); + + return constraint; + } + + /// + /// EqualityAdapter for comparing IOneOf instances within NUnitEqualityComparer. + /// This enables recursive comparison of nested OneOf values. + /// + private class OneOfEqualityAdapter : EqualityAdapter + { + private readonly NUnitEqualityComparer _comparer; + + public OneOfEqualityAdapter(NUnitEqualityComparer comparer) + { + _comparer = comparer; + } + + public override bool CanCompare(object? x, object? y) + { + return x is IOneOf && y is IOneOf; + } + + public override bool AreEqual(object? x, object? y) + { + var oneOfX = (IOneOf?)x; + var oneOfY = (IOneOf?)y; + + // ReSharper disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (oneOfX?.Value is null && oneOfY?.Value is null) + { + return true; + } + + if (oneOfX?.Value is null || oneOfY?.Value is null) + { + return false; + } + + var tolerance = Tolerance.Default; + return _comparer.AreEqual(oneOfX.Value, oneOfY.Value, ref tolerance); + } + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/OptionalComparer.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/OptionalComparer.cs new file mode 100644 index 000000000000..1cac67cf25d2 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/OptionalComparer.cs @@ -0,0 +1,104 @@ +using NUnit.Framework.Constraints; +using OneOf; +using SeedBasicAuthOptional.Core; + +namespace NUnit.Framework; + +/// +/// Extensions for EqualConstraint to handle Optional values. +/// +public static class OptionalComparerExtensions +{ + /// + /// Modifies the EqualConstraint to handle Optional instances by comparing their IsDefined state and inner values. + /// This works alongside other comparison modifiers like UsingPropertiesComparer. + /// + /// The EqualConstraint to modify. + /// The same constraint instance for method chaining. + public static EqualConstraint UsingOptionalComparer(this EqualConstraint constraint) + { + // Register a comparer factory for IOptional types + constraint.Using( + (x, y) => + { + // Both must have the same IsDefined state + if (x.IsDefined != y.IsDefined) + { + return false; + } + + // If both are undefined, they're equal + if (!x.IsDefined) + { + return true; + } + + // Both are defined, compare their boxed values + var xValue = x.GetBoxedValue(); + var yValue = y.GetBoxedValue(); + + // ReSharper disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (xValue is null && yValue is null) + { + return true; + } + + if (xValue is null || yValue is null) + { + return false; + } + + // Use NUnit's property comparer for the inner values + var propertiesComparer = new NUnitEqualityComparer(); + var tolerance = Tolerance.Default; + propertiesComparer.CompareProperties = true; + // Add OneOf comparer to handle nested OneOf values (e.g., in Lists within Optional) + propertiesComparer.ExternalComparers.Add( + new OneOfEqualityAdapter(propertiesComparer) + ); + return propertiesComparer.AreEqual(xValue, yValue, ref tolerance); + } + ); + + return constraint; + } + + /// + /// EqualityAdapter for comparing IOneOf instances within NUnitEqualityComparer. + /// This enables recursive comparison of nested OneOf values within Optional types. + /// + private class OneOfEqualityAdapter : EqualityAdapter + { + private readonly NUnitEqualityComparer _comparer; + + public OneOfEqualityAdapter(NUnitEqualityComparer comparer) + { + _comparer = comparer; + } + + public override bool CanCompare(object? x, object? y) + { + return x is IOneOf && y is IOneOf; + } + + public override bool AreEqual(object? x, object? y) + { + var oneOfX = (IOneOf?)x; + var oneOfY = (IOneOf?)y; + + // ReSharper disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (oneOfX?.Value is null && oneOfY?.Value is null) + { + return true; + } + + if (oneOfX?.Value is null || oneOfY?.Value is null) + { + return false; + } + + var tolerance = Tolerance.Default; + return _comparer.AreEqual(oneOfX.Value, oneOfY.Value, ref tolerance); + } + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/ReadOnlyMemoryComparer.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/ReadOnlyMemoryComparer.cs new file mode 100644 index 000000000000..fc0b595a5e54 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/ReadOnlyMemoryComparer.cs @@ -0,0 +1,87 @@ +using NUnit.Framework.Constraints; + +namespace NUnit.Framework; + +/// +/// Extensions for NUnit constraints. +/// +public static class ReadOnlyMemoryComparerExtensions +{ + /// + /// Extension method for comparing ReadOnlyMemory<T> in NUnit tests. + /// + /// The type of elements in the ReadOnlyMemory. + /// The Is.EqualTo() constraint instance. + /// A constraint that can compare ReadOnlyMemory<T>. + public static EqualConstraint UsingReadOnlyMemoryComparer(this EqualConstraint constraint) + where T : IComparable + { + return constraint.Using(new ReadOnlyMemoryComparer()); + } +} + +/// +/// Comparer for ReadOnlyMemory<T>. Compares sequences by value. +/// +/// +/// The type of elements in the ReadOnlyMemory. +/// +public class ReadOnlyMemoryComparer : IComparer> + where T : IComparable +{ + /// + public int Compare(ReadOnlyMemory x, ReadOnlyMemory y) + { + // Check if sequences are equal + var xSpan = x.Span; + var ySpan = y.Span; + + // Optimized case for IEquatable implementations + if (typeof(IEquatable).IsAssignableFrom(typeof(T))) + { + var areEqual = xSpan.SequenceEqual(ySpan); + if (areEqual) + { + return 0; // Sequences are equal + } + } + else + { + // Manual equality check for non-IEquatable types + if (xSpan.Length == ySpan.Length) + { + var areEqual = true; + for (var i = 0; i < xSpan.Length; i++) + { + if (!EqualityComparer.Default.Equals(xSpan[i], ySpan[i])) + { + areEqual = false; + break; + } + } + + if (areEqual) + { + return 0; // Sequences are equal + } + } + } + + // For non-equal sequences, we need to return a consistent ordering + // First compare lengths + if (x.Length != y.Length) + return x.Length.CompareTo(y.Length); + + // Same length but different content - compare first differing element + for (var i = 0; i < x.Length; i++) + { + if (!EqualityComparer.Default.Equals(xSpan[i], ySpan[i])) + { + return xSpan[i].CompareTo(ySpan[i]); + } + } + + // Should never reach here if not equal + return 0; + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/BasicAuth/BasicAuthClient.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/BasicAuth/BasicAuthClient.cs new file mode 100644 index 000000000000..d78685d4ec41 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/BasicAuth/BasicAuthClient.cs @@ -0,0 +1,207 @@ +using global::System.Text.Json; +using SeedBasicAuthOptional.Core; + +namespace SeedBasicAuthOptional; + +public partial class BasicAuthClient : IBasicAuthClient +{ + private readonly RawClient _client; + + internal BasicAuthClient(RawClient client) + { + _client = client; + } + + private async Task> GetWithBasicAuthAsyncCore( + RequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + var _headers = await new SeedBasicAuthOptional.Core.HeadersBuilder.Builder() + .Add(_client.Options.Headers) + .Add(_client.Options.AdditionalHeaders) + .Add(options?.AdditionalHeaders) + .BuildAsync() + .ConfigureAwait(false); + var response = await _client + .SendRequestAsync( + new JsonRequest + { + Method = HttpMethod.Get, + Path = "basic-auth", + Headers = _headers, + Options = options, + }, + cancellationToken + ) + .ConfigureAwait(false); + if (response.StatusCode is >= 200 and < 400) + { + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); + try + { + var responseData = JsonUtils.Deserialize(responseBody)!; + return new WithRawResponse() + { + Data = responseData, + RawResponse = new RawResponse() + { + StatusCode = response.Raw.StatusCode, + Url = response.Raw.RequestMessage?.RequestUri ?? new Uri("about:blank"), + Headers = ResponseHeaders.FromHttpResponseMessage(response.Raw), + }, + }; + } + catch (JsonException e) + { + throw new SeedBasicAuthOptionalApiException( + "Failed to deserialize response", + response.StatusCode, + responseBody, + e + ); + } + } + { + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); + try + { + switch (response.StatusCode) + { + case 401: + throw new UnauthorizedRequest( + JsonUtils.Deserialize(responseBody) + ); + } + } + catch (JsonException) + { + // unable to map error response, throwing generic error + } + throw new SeedBasicAuthOptionalApiException( + $"Error with status code {response.StatusCode}", + response.StatusCode, + responseBody + ); + } + } + + private async Task> PostWithBasicAuthAsyncCore( + object request, + RequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + var _headers = await new SeedBasicAuthOptional.Core.HeadersBuilder.Builder() + .Add(_client.Options.Headers) + .Add(_client.Options.AdditionalHeaders) + .Add(options?.AdditionalHeaders) + .BuildAsync() + .ConfigureAwait(false); + var response = await _client + .SendRequestAsync( + new JsonRequest + { + Method = HttpMethod.Post, + Path = "basic-auth", + Body = request, + Headers = _headers, + Options = options, + }, + cancellationToken + ) + .ConfigureAwait(false); + if (response.StatusCode is >= 200 and < 400) + { + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); + try + { + var responseData = JsonUtils.Deserialize(responseBody)!; + return new WithRawResponse() + { + Data = responseData, + RawResponse = new RawResponse() + { + StatusCode = response.Raw.StatusCode, + Url = response.Raw.RequestMessage?.RequestUri ?? new Uri("about:blank"), + Headers = ResponseHeaders.FromHttpResponseMessage(response.Raw), + }, + }; + } + catch (JsonException e) + { + throw new SeedBasicAuthOptionalApiException( + "Failed to deserialize response", + response.StatusCode, + responseBody, + e + ); + } + } + { + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); + try + { + switch (response.StatusCode) + { + case 401: + throw new UnauthorizedRequest( + JsonUtils.Deserialize(responseBody) + ); + case 400: + throw new BadRequest(JsonUtils.Deserialize(responseBody)); + } + } + catch (JsonException) + { + // unable to map error response, throwing generic error + } + throw new SeedBasicAuthOptionalApiException( + $"Error with status code {response.StatusCode}", + response.StatusCode, + responseBody + ); + } + } + + /// + /// GET request with basic auth scheme + /// + /// + /// await client.BasicAuth.GetWithBasicAuthAsync(); + /// + public WithRawResponseTask GetWithBasicAuthAsync( + RequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + return new WithRawResponseTask(GetWithBasicAuthAsyncCore(options, cancellationToken)); + } + + /// + /// POST request with basic auth scheme + /// + /// + /// await client.BasicAuth.PostWithBasicAuthAsync( + /// new Dictionary<object, object?>() { { "key", "value" } } + /// ); + /// + public WithRawResponseTask PostWithBasicAuthAsync( + object request, + RequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + return new WithRawResponseTask( + PostWithBasicAuthAsyncCore(request, options, cancellationToken) + ); + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/BasicAuth/IBasicAuthClient.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/BasicAuth/IBasicAuthClient.cs new file mode 100644 index 000000000000..aca0fbc2578b --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/BasicAuth/IBasicAuthClient.cs @@ -0,0 +1,21 @@ +namespace SeedBasicAuthOptional; + +public partial interface IBasicAuthClient +{ + /// + /// GET request with basic auth scheme + /// + WithRawResponseTask GetWithBasicAuthAsync( + RequestOptions? options = null, + CancellationToken cancellationToken = default + ); + + /// + /// POST request with basic auth scheme + /// + WithRawResponseTask PostWithBasicAuthAsync( + object request, + RequestOptions? options = null, + CancellationToken cancellationToken = default + ); +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/ApiResponse.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/ApiResponse.cs new file mode 100644 index 000000000000..f033f46040d8 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/ApiResponse.cs @@ -0,0 +1,13 @@ +using global::System.Net.Http; + +namespace SeedBasicAuthOptional.Core; + +/// +/// The response object returned from the API. +/// +internal record ApiResponse +{ + internal required int StatusCode { get; init; } + + internal required HttpResponseMessage Raw { get; init; } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/BaseRequest.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/BaseRequest.cs new file mode 100644 index 000000000000..ad45d88fcc0e --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/BaseRequest.cs @@ -0,0 +1,67 @@ +using global::System.Net.Http; +using global::System.Net.Http.Headers; +using global::System.Text; + +namespace SeedBasicAuthOptional.Core; + +internal abstract record BaseRequest +{ + internal string? BaseUrl { get; init; } + + internal required HttpMethod Method { get; init; } + + internal required string Path { get; init; } + + internal string? ContentType { get; init; } + + /// + /// The query string for this request (including the leading '?' if non-empty). + /// + internal string? QueryString { get; init; } + + internal Dictionary Headers { get; init; } = + new(StringComparer.OrdinalIgnoreCase); + + internal IRequestOptions? Options { get; init; } + + internal abstract HttpContent? CreateContent(); + + protected static ( + Encoding encoding, + string? charset, + string mediaType + ) ParseContentTypeOrDefault( + string? contentType, + Encoding encodingFallback, + string mediaTypeFallback + ) + { + var encoding = encodingFallback; + var mediaType = mediaTypeFallback; + string? charset = null; + if (string.IsNullOrEmpty(contentType)) + { + return (encoding, charset, mediaType); + } + + if (!MediaTypeHeaderValue.TryParse(contentType, out var mediaTypeHeaderValue)) + { + return (encoding, charset, mediaType); + } + + if (!string.IsNullOrEmpty(mediaTypeHeaderValue.CharSet)) + { + charset = mediaTypeHeaderValue.CharSet; + encoding = Encoding.GetEncoding(mediaTypeHeaderValue.CharSet); + } + + if (!string.IsNullOrEmpty(mediaTypeHeaderValue.MediaType)) + { + mediaType = mediaTypeHeaderValue.MediaType; + } + + return (encoding, charset, mediaType); + } + + protected static Encoding Utf8NoBom => EncodingCache.Utf8NoBom; +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/CollectionItemSerializer.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/CollectionItemSerializer.cs new file mode 100644 index 000000000000..730f1e27b265 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/CollectionItemSerializer.cs @@ -0,0 +1,91 @@ +using global::System.Text.Json; +using global::System.Text.Json.Serialization; + +namespace SeedBasicAuthOptional.Core; + +/// +/// Json collection converter. +/// +/// Type of item to convert. +/// Converter to use for individual items. +internal class CollectionItemSerializer + : JsonConverter> + where TConverterType : JsonConverter, new() +{ + private static readonly TConverterType _converter = new TConverterType(); + + /// + /// Reads a json string and deserializes it into an object. + /// + /// Json reader. + /// Type to convert. + /// Serializer options. + /// Created object. + public override IEnumerable? Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + if (reader.TokenType == JsonTokenType.Null) + { + return default; + } + + var jsonSerializerOptions = new JsonSerializerOptions(options); + jsonSerializerOptions.Converters.Clear(); + jsonSerializerOptions.Converters.Add(_converter); + + var returnValue = new List(); + + while (reader.TokenType != JsonTokenType.EndArray) + { + if (reader.TokenType != JsonTokenType.StartArray) + { + var item = (TDatatype)( + JsonSerializer.Deserialize(ref reader, typeof(TDatatype), jsonSerializerOptions) + ?? throw new global::System.Exception( + $"Failed to deserialize collection item of type {typeof(TDatatype)}" + ) + ); + returnValue.Add(item); + } + + reader.Read(); + } + + return returnValue; + } + + /// + /// Writes a json string. + /// + /// Json writer. + /// Value to write. + /// Serializer options. + public override void Write( + Utf8JsonWriter writer, + IEnumerable? value, + JsonSerializerOptions options + ) + { + if (value is null) + { + writer.WriteNullValue(); + return; + } + + var jsonSerializerOptions = new JsonSerializerOptions(options); + jsonSerializerOptions.Converters.Clear(); + jsonSerializerOptions.Converters.Add(_converter); + + writer.WriteStartArray(); + + foreach (var data in value) + { + JsonSerializer.Serialize(writer, data, jsonSerializerOptions); + } + + writer.WriteEndArray(); + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Constants.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Constants.cs new file mode 100644 index 000000000000..b7f86c54a04c --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Constants.cs @@ -0,0 +1,7 @@ +namespace SeedBasicAuthOptional.Core; + +internal static class Constants +{ + public const string DateTimeFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.fffK"; + public const string DateFormat = "yyyy-MM-dd"; +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/DateOnlyConverter.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/DateOnlyConverter.cs new file mode 100644 index 000000000000..cb4e399cca83 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/DateOnlyConverter.cs @@ -0,0 +1,747 @@ +// ReSharper disable All +#pragma warning disable + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using global::System.Diagnostics; +using global::System.Diagnostics.CodeAnalysis; +using global::System.Globalization; +using global::System.Runtime.CompilerServices; +using global::System.Runtime.InteropServices; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; + +// ReSharper disable SuggestVarOrType_SimpleTypes +// ReSharper disable SuggestVarOrType_BuiltInTypes + +namespace SeedBasicAuthOptional.Core +{ + /// + /// Custom converter for handling the data type with the System.Text.Json library. + /// + /// + /// This class backported from: + /// + /// System.Text.Json.Serialization.Converters.DateOnlyConverter + /// + public sealed class DateOnlyConverter : JsonConverter + { + private const int FormatLength = 10; // YYYY-MM-DD + + private const int MaxEscapedFormatLength = + FormatLength * JsonConstants.MaxExpansionFactorWhileEscaping; + + /// + public override DateOnly Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + if (reader.TokenType != JsonTokenType.String) + { + ThrowHelper.ThrowInvalidOperationException_ExpectedString(reader.TokenType); + } + + return ReadCore(ref reader); + } + + /// + public override DateOnly ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + Debug.Assert(reader.TokenType == JsonTokenType.PropertyName); + return ReadCore(ref reader); + } + + private static DateOnly ReadCore(ref Utf8JsonReader reader) + { + if ( + !JsonHelpers.IsInRangeInclusive( + reader.ValueLength(), + FormatLength, + MaxEscapedFormatLength + ) + ) + { + ThrowHelper.ThrowFormatException(DataType.DateOnly); + } + + scoped ReadOnlySpan source; + if (!reader.HasValueSequence && !reader.ValueIsEscaped) + { + source = reader.ValueSpan; + } + else + { + Span stackSpan = stackalloc byte[MaxEscapedFormatLength]; + int bytesWritten = reader.CopyString(stackSpan); + source = stackSpan.Slice(0, bytesWritten); + } + + if (!JsonHelpers.TryParseAsIso(source, out DateOnly value)) + { + ThrowHelper.ThrowFormatException(DataType.DateOnly); + } + + return value; + } + + /// + public override void Write( + Utf8JsonWriter writer, + DateOnly value, + JsonSerializerOptions options + ) + { +#if NET8_0_OR_GREATER + Span buffer = stackalloc byte[FormatLength]; +#else + Span buffer = stackalloc char[FormatLength]; +#endif + // ReSharper disable once RedundantAssignment + bool formattedSuccessfully = value.TryFormat( + buffer, + out int charsWritten, + "O".AsSpan(), + CultureInfo.InvariantCulture + ); + Debug.Assert(formattedSuccessfully && charsWritten == FormatLength); + writer.WriteStringValue(buffer); + } + + /// + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + DateOnly value, + JsonSerializerOptions options + ) + { +#if NET8_0_OR_GREATER + Span buffer = stackalloc byte[FormatLength]; +#else + Span buffer = stackalloc char[FormatLength]; +#endif + // ReSharper disable once RedundantAssignment + bool formattedSuccessfully = value.TryFormat( + buffer, + out int charsWritten, + "O".AsSpan(), + CultureInfo.InvariantCulture + ); + Debug.Assert(formattedSuccessfully && charsWritten == FormatLength); + writer.WritePropertyName(buffer); + } + } + + internal static class JsonConstants + { + // The maximum number of fraction digits the Json DateTime parser allows + public const int DateTimeParseNumFractionDigits = 16; + + // In the worst case, an ASCII character represented as a single utf-8 byte could expand 6x when escaped. + public const int MaxExpansionFactorWhileEscaping = 6; + + // The largest fraction expressible by TimeSpan and DateTime formats + public const int MaxDateTimeFraction = 9_999_999; + + // TimeSpan and DateTime formats allow exactly up to many digits for specifying the fraction after the seconds. + public const int DateTimeNumFractionDigits = 7; + + public const byte UtcOffsetToken = (byte)'Z'; + + public const byte TimePrefix = (byte)'T'; + + public const byte Period = (byte)'.'; + + public const byte Hyphen = (byte)'-'; + + public const byte Colon = (byte)':'; + + public const byte Plus = (byte)'+'; + } + + // ReSharper disable SuggestVarOrType_Elsewhere + // ReSharper disable SuggestVarOrType_SimpleTypes + // ReSharper disable SuggestVarOrType_BuiltInTypes + + internal static class JsonHelpers + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsInRangeInclusive(int value, int lowerBound, int upperBound) => + (uint)(value - lowerBound) <= (uint)(upperBound - lowerBound); + + public static bool IsDigit(byte value) => (uint)(value - '0') <= '9' - '0'; + + [StructLayout(LayoutKind.Auto)] + private struct DateTimeParseData + { + public int Year; + public int Month; + public int Day; + public bool IsCalendarDateOnly; + public int Hour; + public int Minute; + public int Second; + public int Fraction; // This value should never be greater than 9_999_999. + public int OffsetHours; + public int OffsetMinutes; + + // ReSharper disable once NotAccessedField.Local + public byte OffsetToken; + } + + public static bool TryParseAsIso(ReadOnlySpan source, out DateOnly value) + { + if ( + TryParseDateTimeOffset(source, out DateTimeParseData parseData) + && parseData.IsCalendarDateOnly + && TryCreateDateTime(parseData, DateTimeKind.Unspecified, out DateTime dateTime) + ) + { + value = DateOnly.FromDateTime(dateTime); + return true; + } + + value = default; + return false; + } + + /// + /// ISO 8601 date time parser (ISO 8601-1:2019). + /// + /// The date/time to parse in UTF-8 format. + /// The parsed for the given . + /// + /// Supports extended calendar date (5.2.2.1) and complete (5.4.2.1) calendar date/time of day + /// representations with optional specification of seconds and fractional seconds. + /// + /// Times can be explicitly specified as UTC ("Z" - 5.3.3) or offsets from UTC ("+/-hh:mm" 5.3.4.2). + /// If unspecified they are considered to be local per spec. + /// + /// Examples: (TZD is either "Z" or hh:mm offset from UTC) + /// + /// YYYY-MM-DD (e.g. 1997-07-16) + /// YYYY-MM-DDThh:mm (e.g. 1997-07-16T19:20) + /// YYYY-MM-DDThh:mm:ss (e.g. 1997-07-16T19:20:30) + /// YYYY-MM-DDThh:mm:ss.s (e.g. 1997-07-16T19:20:30.45) + /// YYYY-MM-DDThh:mmTZD (e.g. 1997-07-16T19:20+01:00) + /// YYYY-MM-DDThh:mm:ssTZD (e.g. 1997-07-16T19:20:3001:00) + /// YYYY-MM-DDThh:mm:ss.sTZD (e.g. 1997-07-16T19:20:30.45Z) + /// + /// Generally speaking we always require the "extended" option when one exists (3.1.3.5). + /// The extended variants have separator characters between components ('-', ':', '.', etc.). + /// Spaces are not permitted. + /// + /// "true" if successfully parsed. + private static bool TryParseDateTimeOffset( + ReadOnlySpan source, + out DateTimeParseData parseData + ) + { + parseData = default; + + // too short datetime + Debug.Assert(source.Length >= 10); + + // Parse the calendar date + // ----------------------- + // ISO 8601-1:2019 5.2.2.1b "Calendar date complete extended format" + // [dateX] = [year]["-"][month]["-"][day] + // [year] = [YYYY] [0000 - 9999] (4.3.2) + // [month] = [MM] [01 - 12] (4.3.3) + // [day] = [DD] [01 - 28, 29, 30, 31] (4.3.4) + // + // Note: 5.2.2.2 "Representations with reduced precision" allows for + // just [year]["-"][month] (a) and just [year] (b), but we currently + // don't permit it. + + { + uint digit1 = source[0] - (uint)'0'; + uint digit2 = source[1] - (uint)'0'; + uint digit3 = source[2] - (uint)'0'; + uint digit4 = source[3] - (uint)'0'; + + if (digit1 > 9 || digit2 > 9 || digit3 > 9 || digit4 > 9) + { + return false; + } + + parseData.Year = (int)(digit1 * 1000 + digit2 * 100 + digit3 * 10 + digit4); + } + + if ( + source[4] != JsonConstants.Hyphen + || !TryGetNextTwoDigits(source.Slice(start: 5, length: 2), ref parseData.Month) + || source[7] != JsonConstants.Hyphen + || !TryGetNextTwoDigits(source.Slice(start: 8, length: 2), ref parseData.Day) + ) + { + return false; + } + + // We now have YYYY-MM-DD [dateX] + // ReSharper disable once ConvertIfStatementToSwitchStatement + if (source.Length == 10) + { + parseData.IsCalendarDateOnly = true; + return true; + } + + // Parse the time of day + // --------------------- + // + // ISO 8601-1:2019 5.3.1.2b "Local time of day complete extended format" + // [timeX] = ["T"][hour][":"][min][":"][sec] + // [hour] = [hh] [00 - 23] (4.3.8a) + // [minute] = [mm] [00 - 59] (4.3.9a) + // [sec] = [ss] [00 - 59, 60 with a leap second] (4.3.10a) + // + // ISO 8601-1:2019 5.3.3 "UTC of day" + // [timeX]["Z"] + // + // ISO 8601-1:2019 5.3.4.2 "Local time of day with the time shift between + // local timescale and UTC" (Extended format) + // + // [shiftX] = ["+"|"-"][hour][":"][min] + // + // Notes: + // + // "T" is optional per spec, but _only_ when times are used alone. In our + // case, we're reading out a complete date & time and as such require "T". + // (5.4.2.1b). + // + // For [timeX] We allow seconds to be omitted per 5.3.1.3a "Representations + // with reduced precision". 5.3.1.3b allows just specifying the hour, but + // we currently don't permit this. + // + // Decimal fractions are allowed for hours, minutes and seconds (5.3.14). + // We only allow fractions for seconds currently. Lower order components + // can't follow, i.e. you can have T23.3, but not T23.3:04. There must be + // one digit, but the max number of digits is implementation defined. We + // currently allow up to 16 digits of fractional seconds only. While we + // support 16 fractional digits we only parse the first seven, anything + // past that is considered a zero. This is to stay compatible with the + // DateTime implementation which is limited to this resolution. + + if (source.Length < 16) + { + // Source does not have enough characters for YYYY-MM-DDThh:mm + return false; + } + + // Parse THH:MM (e.g. "T10:32") + if ( + source[10] != JsonConstants.TimePrefix + || source[13] != JsonConstants.Colon + || !TryGetNextTwoDigits(source.Slice(start: 11, length: 2), ref parseData.Hour) + || !TryGetNextTwoDigits(source.Slice(start: 14, length: 2), ref parseData.Minute) + ) + { + return false; + } + + // We now have YYYY-MM-DDThh:mm + Debug.Assert(source.Length >= 16); + if (source.Length == 16) + { + return true; + } + + byte curByte = source[16]; + int sourceIndex = 17; + + // Either a TZD ['Z'|'+'|'-'] or a seconds separator [':'] is valid at this point + switch (curByte) + { + case JsonConstants.UtcOffsetToken: + parseData.OffsetToken = JsonConstants.UtcOffsetToken; + return sourceIndex == source.Length; + case JsonConstants.Plus: + case JsonConstants.Hyphen: + parseData.OffsetToken = curByte; + return ParseOffset(ref parseData, source.Slice(sourceIndex)); + case JsonConstants.Colon: + break; + default: + return false; + } + + // Try reading the seconds + if ( + source.Length < 19 + || !TryGetNextTwoDigits(source.Slice(start: 17, length: 2), ref parseData.Second) + ) + { + return false; + } + + // We now have YYYY-MM-DDThh:mm:ss + Debug.Assert(source.Length >= 19); + if (source.Length == 19) + { + return true; + } + + curByte = source[19]; + sourceIndex = 20; + + // Either a TZD ['Z'|'+'|'-'] or a seconds decimal fraction separator ['.'] is valid at this point + switch (curByte) + { + case JsonConstants.UtcOffsetToken: + parseData.OffsetToken = JsonConstants.UtcOffsetToken; + return sourceIndex == source.Length; + case JsonConstants.Plus: + case JsonConstants.Hyphen: + parseData.OffsetToken = curByte; + return ParseOffset(ref parseData, source.Slice(sourceIndex)); + case JsonConstants.Period: + break; + default: + return false; + } + + // Source does not have enough characters for second fractions (i.e. ".s") + // YYYY-MM-DDThh:mm:ss.s + if (source.Length < 21) + { + return false; + } + + // Parse fraction. This value should never be greater than 9_999_999 + int numDigitsRead = 0; + int fractionEnd = Math.Min( + sourceIndex + JsonConstants.DateTimeParseNumFractionDigits, + source.Length + ); + + while (sourceIndex < fractionEnd && IsDigit(curByte = source[sourceIndex])) + { + if (numDigitsRead < JsonConstants.DateTimeNumFractionDigits) + { + parseData.Fraction = parseData.Fraction * 10 + (int)(curByte - (uint)'0'); + numDigitsRead++; + } + + sourceIndex++; + } + + if (parseData.Fraction != 0) + { + while (numDigitsRead < JsonConstants.DateTimeNumFractionDigits) + { + parseData.Fraction *= 10; + numDigitsRead++; + } + } + + // We now have YYYY-MM-DDThh:mm:ss.s + Debug.Assert(sourceIndex <= source.Length); + if (sourceIndex == source.Length) + { + return true; + } + + curByte = source[sourceIndex++]; + + // TZD ['Z'|'+'|'-'] is valid at this point + switch (curByte) + { + case JsonConstants.UtcOffsetToken: + parseData.OffsetToken = JsonConstants.UtcOffsetToken; + return sourceIndex == source.Length; + case JsonConstants.Plus: + case JsonConstants.Hyphen: + parseData.OffsetToken = curByte; + return ParseOffset(ref parseData, source.Slice(sourceIndex)); + default: + return false; + } + + static bool ParseOffset(ref DateTimeParseData parseData, ReadOnlySpan offsetData) + { + // Parse the hours for the offset + if ( + offsetData.Length < 2 + || !TryGetNextTwoDigits(offsetData.Slice(0, 2), ref parseData.OffsetHours) + ) + { + return false; + } + + // We now have YYYY-MM-DDThh:mm:ss.s+|-hh + + if (offsetData.Length == 2) + { + // Just hours offset specified + return true; + } + + // Ensure we have enough for ":mm" + return offsetData.Length == 5 + && offsetData[2] == JsonConstants.Colon + && TryGetNextTwoDigits(offsetData.Slice(3), ref parseData.OffsetMinutes); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + // ReSharper disable once RedundantAssignment + private static bool TryGetNextTwoDigits(ReadOnlySpan source, ref int value) + { + Debug.Assert(source.Length == 2); + + uint digit1 = source[0] - (uint)'0'; + uint digit2 = source[1] - (uint)'0'; + + if (digit1 > 9 || digit2 > 9) + { + value = 0; + return false; + } + + value = (int)(digit1 * 10 + digit2); + return true; + } + + // The following methods are borrowed verbatim from src/Common/src/CoreLib/System/Buffers/Text/Utf8Parser/Utf8Parser.Date.Helpers.cs + + /// + /// Overflow-safe DateTime factory. + /// + private static bool TryCreateDateTime( + DateTimeParseData parseData, + DateTimeKind kind, + out DateTime value + ) + { + if (parseData.Year == 0) + { + value = default; + return false; + } + + Debug.Assert(parseData.Year <= 9999); // All of our callers to date parse the year from fixed 4-digit fields so this value is trusted. + + if ((uint)parseData.Month - 1 >= 12) + { + value = default; + return false; + } + + uint dayMinusOne = (uint)parseData.Day - 1; + if ( + dayMinusOne >= 28 + && dayMinusOne >= DateTime.DaysInMonth(parseData.Year, parseData.Month) + ) + { + value = default; + return false; + } + + if ((uint)parseData.Hour > 23) + { + value = default; + return false; + } + + if ((uint)parseData.Minute > 59) + { + value = default; + return false; + } + + // This needs to allow leap seconds when appropriate. + // See https://github.com/dotnet/runtime/issues/30135. + if ((uint)parseData.Second > 59) + { + value = default; + return false; + } + + Debug.Assert(parseData.Fraction is >= 0 and <= JsonConstants.MaxDateTimeFraction); // All of our callers to date parse the fraction from fixed 7-digit fields so this value is trusted. + + ReadOnlySpan days = DateTime.IsLeapYear(parseData.Year) + ? DaysToMonth366 + : DaysToMonth365; + int yearMinusOne = parseData.Year - 1; + int totalDays = + yearMinusOne * 365 + + yearMinusOne / 4 + - yearMinusOne / 100 + + yearMinusOne / 400 + + days[parseData.Month - 1] + + parseData.Day + - 1; + long ticks = totalDays * TimeSpan.TicksPerDay; + int totalSeconds = parseData.Hour * 3600 + parseData.Minute * 60 + parseData.Second; + ticks += totalSeconds * TimeSpan.TicksPerSecond; + ticks += parseData.Fraction; + value = new DateTime(ticks: ticks, kind: kind); + return true; + } + + private static ReadOnlySpan DaysToMonth365 => + [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365]; + private static ReadOnlySpan DaysToMonth366 => + [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366]; + } + + internal static class ThrowHelper + { + private const string ExceptionSourceValueToRethrowAsJsonException = + "System.Text.Json.Rethrowable"; + + [DoesNotReturn] + public static void ThrowInvalidOperationException_ExpectedString(JsonTokenType tokenType) + { + throw GetInvalidOperationException("string", tokenType); + } + + public static void ThrowFormatException(DataType dataType) + { + throw new FormatException(SR.Format(SR.UnsupportedFormat, dataType)) + { + Source = ExceptionSourceValueToRethrowAsJsonException, + }; + } + + private static global::System.Exception GetInvalidOperationException( + string message, + JsonTokenType tokenType + ) + { + return GetInvalidOperationException(SR.Format(SR.InvalidCast, tokenType, message)); + } + + private static InvalidOperationException GetInvalidOperationException(string message) + { + return new InvalidOperationException(message) + { + Source = ExceptionSourceValueToRethrowAsJsonException, + }; + } + } + + internal static class Utf8JsonReaderExtensions + { + internal static int ValueLength(this Utf8JsonReader reader) => + reader.HasValueSequence + ? checked((int)reader.ValueSequence.Length) + : reader.ValueSpan.Length; + } + + internal enum DataType + { + TimeOnly, + DateOnly, + } + + [SuppressMessage("ReSharper", "InconsistentNaming")] + internal static class SR + { + private static readonly bool s_usingResourceKeys = + AppContext.TryGetSwitch( + "System.Resources.UseSystemResourceKeys", + out bool usingResourceKeys + ) && usingResourceKeys; + + public static string UnsupportedFormat => Strings.UnsupportedFormat; + + public static string InvalidCast => Strings.InvalidCast; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static string Format(string resourceFormat, object? p1) => + s_usingResourceKeys + ? string.Join(", ", resourceFormat, p1) + : string.Format(resourceFormat, p1); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static string Format(string resourceFormat, object? p1, object? p2) => + s_usingResourceKeys + ? string.Join(", ", resourceFormat, p1, p2) + : string.Format(resourceFormat, p1, p2); + } + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute( + "System.Resources.Tools.StronglyTypedResourceBuilder", + "17.0.0.0" + )] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Strings + { + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute( + "Microsoft.Performance", + "CA1811:AvoidUncalledPrivateCode" + )] + internal Strings() { } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute( + global::System.ComponentModel.EditorBrowsableState.Advanced + )] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if (object.ReferenceEquals(resourceMan, null)) + { + global::System.Resources.ResourceManager temp = + new global::System.Resources.ResourceManager( + "System.Text.Json.Resources.Strings", + typeof(Strings).Assembly + ); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute( + global::System.ComponentModel.EditorBrowsableState.Advanced + )] + internal static global::System.Globalization.CultureInfo Culture + { + get { return resourceCulture; } + set { resourceCulture = value; } + } + + /// + /// Looks up a localized string similar to Cannot get the value of a token type '{0}' as a {1}.. + /// + internal static string InvalidCast + { + get { return ResourceManager.GetString("InvalidCast", resourceCulture); } + } + + /// + /// Looks up a localized string similar to The JSON value is not in a supported {0} format.. + /// + internal static string UnsupportedFormat + { + get { return ResourceManager.GetString("UnsupportedFormat", resourceCulture); } + } + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/DateTimeSerializer.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/DateTimeSerializer.cs new file mode 100644 index 000000000000..27b62074d137 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/DateTimeSerializer.cs @@ -0,0 +1,40 @@ +using global::System.Globalization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; + +namespace SeedBasicAuthOptional.Core; + +internal class DateTimeSerializer : JsonConverter +{ + public override DateTime Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + return DateTime.Parse(reader.GetString()!, null, DateTimeStyles.RoundtripKind); + } + + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString(Constants.DateTimeFormat)); + } + + public override DateTime ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + return DateTime.Parse(reader.GetString()!, null, DateTimeStyles.RoundtripKind); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + DateTime value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.ToString(Constants.DateTimeFormat)); + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/EmptyRequest.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/EmptyRequest.cs new file mode 100644 index 000000000000..384327b783ab --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/EmptyRequest.cs @@ -0,0 +1,11 @@ +using global::System.Net.Http; + +namespace SeedBasicAuthOptional.Core; + +/// +/// The request object to send without a request body. +/// +internal record EmptyRequest : BaseRequest +{ + internal override HttpContent? CreateContent() => null; +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/EncodingCache.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/EncodingCache.cs new file mode 100644 index 000000000000..ae6139d66855 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/EncodingCache.cs @@ -0,0 +1,11 @@ +using global::System.Text; + +namespace SeedBasicAuthOptional.Core; + +internal static class EncodingCache +{ + internal static readonly Encoding Utf8NoBom = new UTF8Encoding( + encoderShouldEmitUTF8Identifier: false, + throwOnInvalidBytes: true + ); +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Extensions.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Extensions.cs new file mode 100644 index 000000000000..11f9d385c5ef --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Extensions.cs @@ -0,0 +1,55 @@ +using global::System.Diagnostics.CodeAnalysis; +using global::System.Runtime.Serialization; + +namespace SeedBasicAuthOptional.Core; + +internal static class Extensions +{ + public static string Stringify(this Enum value) + { + var field = value.GetType().GetField(value.ToString()); + if (field is not null) + { + var attribute = (EnumMemberAttribute?) + global::System.Attribute.GetCustomAttribute(field, typeof(EnumMemberAttribute)); + return attribute?.Value ?? value.ToString(); + } + return value.ToString(); + } + + /// + /// Asserts that a condition is true, throwing an exception with the specified message if it is false. + /// + /// The condition to assert. + /// The exception message if the assertion fails. + /// Thrown when the condition is false. + internal static void Assert(this object value, bool condition, string message) + { + if (!condition) + { + throw new global::System.Exception(message); + } + } + + /// + /// Asserts that a value is not null, throwing an exception with the specified message if it is null. + /// + /// The type of the value to assert. + /// The value to assert is not null. + /// The exception message if the assertion fails. + /// The non-null value. + /// Thrown when the value is null. + internal static TValue Assert( + this object _unused, + [NotNull] TValue? value, + string message + ) + where TValue : class + { + if (value is null) + { + throw new global::System.Exception(message); + } + return value; + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/FormUrlEncoder.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/FormUrlEncoder.cs new file mode 100644 index 000000000000..954003fc2dc1 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/FormUrlEncoder.cs @@ -0,0 +1,33 @@ +using global::System.Net.Http; + +namespace SeedBasicAuthOptional.Core; + +/// +/// Encodes an object into a form URL-encoded content. +/// +public static class FormUrlEncoder +{ + /// + /// Encodes an object into a form URL-encoded content using Deep Object notation. + /// + /// Object to form URL-encode. You can pass in an object or dictionary, but not lists, strings, or primitives. + /// Throws when passing in a list, a string, or a primitive value. + internal static FormUrlEncodedContent EncodeAsDeepObject(object value) => + new(QueryStringConverter.ToDeepObject(value)); + + /// + /// Encodes an object into a form URL-encoded content using Exploded Form notation. + /// + /// Object to form URL-encode. You can pass in an object or dictionary, but not lists, strings, or primitives. + /// Throws when passing in a list, a string, or a primitive value. + internal static FormUrlEncodedContent EncodeAsExplodedForm(object value) => + new(QueryStringConverter.ToExplodedForm(value)); + + /// + /// Encodes an object into a form URL-encoded content using Form notation without exploding parameters. + /// + /// Object to form URL-encode. You can pass in an object or dictionary, but not lists, strings, or primitives. + /// Throws when passing in a list, a string, or a primitive value. + internal static FormUrlEncodedContent EncodeAsForm(object value) => + new(QueryStringConverter.ToForm(value)); +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/HeaderValue.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/HeaderValue.cs new file mode 100644 index 000000000000..35d9400e7864 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/HeaderValue.cs @@ -0,0 +1,52 @@ +namespace SeedBasicAuthOptional.Core; + +internal sealed class HeaderValue +{ + private readonly Func> _resolver; + + public HeaderValue(string value) + { + _resolver = () => new global::System.Threading.Tasks.ValueTask(value); + } + + public HeaderValue(Func value) + { + _resolver = () => new global::System.Threading.Tasks.ValueTask(value()); + } + + public HeaderValue(Func> value) + { + _resolver = value; + } + + public HeaderValue(Func> value) + { + _resolver = () => new global::System.Threading.Tasks.ValueTask(value()); + } + + public static implicit operator HeaderValue(string value) => new(value); + + public static implicit operator HeaderValue(Func value) => new(value); + + public static implicit operator HeaderValue( + Func> value + ) => new(value); + + public static implicit operator HeaderValue( + Func> value + ) => new(value); + + public static HeaderValue FromString(string value) => new(value); + + public static HeaderValue FromFunc(Func value) => new(value); + + public static HeaderValue FromValueTaskFunc( + Func> value + ) => new(value); + + public static HeaderValue FromTaskFunc( + Func> value + ) => new(value); + + internal global::System.Threading.Tasks.ValueTask ResolveAsync() => _resolver(); +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Headers.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Headers.cs new file mode 100644 index 000000000000..878c9821ffc7 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Headers.cs @@ -0,0 +1,28 @@ +namespace SeedBasicAuthOptional.Core; + +/// +/// Represents the headers sent with the request. +/// +internal sealed class Headers : Dictionary +{ + internal Headers() { } + + /// + /// Initializes a new instance of the Headers class with the specified value. + /// + /// + internal Headers(Dictionary value) + { + foreach (var kvp in value) + { + this[kvp.Key] = kvp.Value; + } + } + + /// + /// Initializes a new instance of the Headers class with the specified value. + /// + /// + internal Headers(IEnumerable> value) + : base(value.ToDictionary(e => e.Key, e => e.Value)) { } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/HeadersBuilder.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/HeadersBuilder.cs new file mode 100644 index 000000000000..b4e0dbee737e --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/HeadersBuilder.cs @@ -0,0 +1,197 @@ +namespace SeedBasicAuthOptional.Core; + +/// +/// Fluent builder for constructing HTTP headers with support for merging from multiple sources. +/// Provides a clean API for building headers with proper precedence handling. +/// +internal static class HeadersBuilder +{ + /// + /// Fluent builder for constructing HTTP headers. + /// + public sealed class Builder + { + private readonly Dictionary _headers; + + /// + /// Initializes a new instance with default capacity. + /// Uses case-insensitive header name comparison. + /// + public Builder() + { + _headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + /// + /// Initializes a new instance with the specified initial capacity. + /// Uses case-insensitive header name comparison. + /// + public Builder(int capacity) + { + _headers = new Dictionary( + capacity, + StringComparer.OrdinalIgnoreCase + ); + } + + /// + /// Adds a header with the specified key and value. + /// If a header with the same key already exists, it will be overwritten. + /// Null values are ignored. + /// + /// The header name. + /// The header value. Null values are ignored. + /// This builder instance for method chaining. + public Builder Add(string key, string? value) + { + if (value is not null) + { + _headers[key] = (value); + } + return this; + } + + /// + /// Adds a header with the specified key and object value. + /// The value will be converted to string using ValueConvert for consistent serialization. + /// If a header with the same key already exists, it will be overwritten. + /// Null values are ignored. + /// + /// The header name. + /// The header value. Null values are ignored. + /// This builder instance for method chaining. + public Builder Add(string key, object? value) + { + if (value is null) + { + return this; + } + + // Use ValueConvert for consistent serialization across headers, query params, and path params + var stringValue = ValueConvert.ToString(value); + if (stringValue is not null) + { + _headers[key] = (stringValue); + } + return this; + } + + /// + /// Adds multiple headers from a Headers dictionary. + /// HeaderValue instances are stored and will be resolved when BuildAsync() is called. + /// Overwrites any existing headers with the same key. + /// Null entries are ignored. + /// + /// The headers to add. Null is treated as empty. + /// This builder instance for method chaining. + public Builder Add(Headers? headers) + { + if (headers is null) + { + return this; + } + + foreach (var header in headers) + { + _headers[header.Key] = header.Value; + } + + return this; + } + + /// + /// Adds multiple headers from a Headers dictionary, excluding the Authorization header. + /// This is useful for endpoints that don't require authentication, to avoid triggering + /// lazy auth token resolution. + /// HeaderValue instances are stored and will be resolved when BuildAsync() is called. + /// Overwrites any existing headers with the same key. + /// Null entries are ignored. + /// + /// The headers to add. Null is treated as empty. + /// This builder instance for method chaining. + public Builder AddWithoutAuth(Headers? headers) + { + if (headers is null) + { + return this; + } + + foreach (var header in headers) + { + if (header.Key.Equals("Authorization", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + _headers[header.Key] = header.Value; + } + + return this; + } + + /// + /// Adds multiple headers from a key-value pair collection. + /// Overwrites any existing headers with the same key. + /// Null values are ignored. + /// + /// The headers to add. Null is treated as empty. + /// This builder instance for method chaining. + public Builder Add(IEnumerable>? headers) + { + if (headers is null) + { + return this; + } + + foreach (var header in headers) + { + if (header.Value is not null) + { + _headers[header.Key] = (header.Value); + } + } + + return this; + } + + /// + /// Adds multiple headers from a dictionary. + /// Overwrites any existing headers with the same key. + /// + /// The headers to add. Null is treated as empty. + /// This builder instance for method chaining. + public Builder Add(Dictionary? headers) + { + if (headers is null) + { + return this; + } + + foreach (var header in headers) + { + _headers[header.Key] = (header.Value); + } + + return this; + } + + /// + /// Asynchronously builds the final headers dictionary containing all merged headers. + /// Resolves all HeaderValue instances that may contain async operations. + /// Returns a case-insensitive dictionary. + /// + /// A task that represents the asynchronous operation, containing a case-insensitive dictionary of headers. + public async global::System.Threading.Tasks.Task> BuildAsync() + { + var headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var kvp in _headers) + { + var value = await kvp.Value.ResolveAsync().ConfigureAwait(false); + if (value is not null) + { + headers[kvp.Key] = value; + } + } + return headers; + } + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/HttpContentExtensions.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/HttpContentExtensions.cs new file mode 100644 index 000000000000..65f5ce6554f6 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/HttpContentExtensions.cs @@ -0,0 +1,20 @@ +#if !NET5_0_OR_GREATER +namespace SeedBasicAuthOptional.Core; + +/// +/// Polyfill extension providing a ReadAsStringAsync(CancellationToken) overload +/// for target frameworks older than .NET 5, where only the parameterless +/// ReadAsStringAsync() is available. +/// +internal static class HttpContentExtensions +{ + internal static Task ReadAsStringAsync( + this HttpContent httpContent, + CancellationToken cancellationToken + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return httpContent.ReadAsStringAsync(); + } +} +#endif diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/HttpMethodExtensions.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/HttpMethodExtensions.cs new file mode 100644 index 000000000000..ad77851023d7 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/HttpMethodExtensions.cs @@ -0,0 +1,8 @@ +using global::System.Net.Http; + +namespace SeedBasicAuthOptional.Core; + +internal static class HttpMethodExtensions +{ + public static readonly HttpMethod Patch = new("PATCH"); +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/IIsRetryableContent.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/IIsRetryableContent.cs new file mode 100644 index 000000000000..e64c9b5a5c89 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/IIsRetryableContent.cs @@ -0,0 +1,6 @@ +namespace SeedBasicAuthOptional.Core; + +public interface IIsRetryableContent +{ + public bool IsRetryable { get; } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/IRequestOptions.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/IRequestOptions.cs new file mode 100644 index 000000000000..0386a81482b4 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/IRequestOptions.cs @@ -0,0 +1,83 @@ +namespace SeedBasicAuthOptional.Core; + +internal interface IRequestOptions +{ + /// + /// The Base URL for the API. + /// + public string? BaseUrl { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// The http client used to make requests. + /// + public HttpClient? HttpClient { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// Additional headers to be sent with the request. + /// Headers previously set with matching keys will be overwritten. + /// + public IEnumerable> AdditionalHeaders { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// The max number of retries to attempt. + /// + public int? MaxRetries { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// The timeout for the request. + /// + public TimeSpan? Timeout { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// Additional query parameters sent with the request. + /// + public IEnumerable> AdditionalQueryParameters { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// Additional body properties sent with the request. + /// This is only applied to JSON requests. + /// + public object? AdditionalBodyProperties { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/JsonAccessAttribute.cs new file mode 100644 index 000000000000..bef7f86c5d27 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/JsonAccessAttribute.cs @@ -0,0 +1,15 @@ +namespace SeedBasicAuthOptional.Core; + +[global::System.AttributeUsage( + global::System.AttributeTargets.Property | global::System.AttributeTargets.Field +)] +internal class JsonAccessAttribute(JsonAccessType accessType) : global::System.Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/JsonConfiguration.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/JsonConfiguration.cs new file mode 100644 index 000000000000..c984380d74b2 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/JsonConfiguration.cs @@ -0,0 +1,275 @@ +using global::System.Reflection; +using global::System.Text.Encodings.Web; +using global::System.Text.Json; +using global::System.Text.Json.Nodes; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; + +namespace SeedBasicAuthOptional.Core; + +internal static partial class JsonOptions +{ + internal static readonly JsonSerializerOptions JsonSerializerOptions; + internal static readonly JsonSerializerOptions JsonSerializerOptionsRelaxedEscaping; + + static JsonOptions() + { + var options = new JsonSerializerOptions + { + Converters = + { + new DateTimeSerializer(), +#if USE_PORTABLE_DATE_ONLY + new DateOnlyConverter(), +#endif + new OneOfSerializer(), + new OptionalJsonConverterFactory(), + }, +#if DEBUG + WriteIndented = true, +#endif + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + NullableOptionalModifier, + JsonAccessAndIgnoreModifier, + HandleExtensionDataFields, + }, + }, + }; + ConfigureJsonSerializerOptions(options); + JsonSerializerOptions = options; + + var relaxedOptions = new JsonSerializerOptions(options) + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + }; + JsonSerializerOptionsRelaxedEscaping = relaxedOptions; + } + + private static void NullableOptionalModifier(JsonTypeInfo typeInfo) + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var property in typeInfo.Properties) + { + var propertyInfo = property.AttributeProvider as global::System.Reflection.PropertyInfo; + + if (propertyInfo is null) + continue; + + // Check for ReadOnly JsonAccessAttribute - it overrides Optional/Nullable behavior + var jsonAccessAttribute = propertyInfo.GetCustomAttribute(); + if (jsonAccessAttribute?.AccessType == JsonAccessType.ReadOnly) + { + // ReadOnly means "never serialize", which completely overrides Optional/Nullable. + // Skip Optional/Nullable processing since JsonAccessAndIgnoreModifier + // will set ShouldSerialize = false anyway. + continue; + } + // Note: WriteOnly doesn't conflict with Optional/Nullable since it only + // affects deserialization (Set), not serialization (ShouldSerialize) + + var isOptionalType = + property.PropertyType.IsGenericType + && property.PropertyType.GetGenericTypeDefinition() == typeof(Optional<>); + + var hasOptionalAttribute = + propertyInfo.GetCustomAttribute() is not null; + var hasNullableAttribute = + propertyInfo.GetCustomAttribute() is not null; + + if (isOptionalType && hasOptionalAttribute) + { + var originalGetter = property.Get; + if (originalGetter is not null) + { + var capturedIsNullable = hasNullableAttribute; + + property.ShouldSerialize = (obj, value) => + { + var optionalValue = originalGetter(obj); + if (optionalValue is not IOptional optional) + return false; + + if (!optional.IsDefined) + return false; + + if (!capturedIsNullable) + { + var innerValue = optional.GetBoxedValue(); + if (innerValue is null) + return false; + } + + return true; + }; + } + } + else if (hasNullableAttribute) + { + // Force serialization of nullable properties even when null + property.ShouldSerialize = (obj, value) => true; + } + } + } + + private static void JsonAccessAndIgnoreModifier(JsonTypeInfo typeInfo) + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes(typeof(JsonAccessAttribute), true) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute is not null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + var jsonIgnoreAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes(typeof(JsonIgnoreAttribute), true) + .OfType() + .FirstOrDefault(); + + if (jsonIgnoreAttribute is not null) + { + propertyInfo.IsRequired = false; + } + } + } + + private static void HandleExtensionDataFields(JsonTypeInfo typeInfo) + { + if ( + typeInfo.Kind == JsonTypeInfoKind.Object + && typeInfo.Properties.All(prop => !prop.IsExtensionData) + ) + { + var extensionProp = typeInfo + .Type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic) + .FirstOrDefault(prop => + prop.GetCustomAttribute() is not null + ); + + if (extensionProp is not null) + { + var jsonPropertyInfo = typeInfo.CreateJsonPropertyInfo( + extensionProp.FieldType, + extensionProp.Name + ); + jsonPropertyInfo.Get = extensionProp.GetValue; + jsonPropertyInfo.Set = extensionProp.SetValue; + jsonPropertyInfo.IsExtensionData = true; + typeInfo.Properties.Add(jsonPropertyInfo); + } + } + } + + static partial void ConfigureJsonSerializerOptions(JsonSerializerOptions defaultOptions); +} + +internal static class JsonUtils +{ + internal static string Serialize(T obj) => + JsonSerializer.Serialize(obj, JsonOptions.JsonSerializerOptions); + + internal static string Serialize(object obj, global::System.Type type) => + JsonSerializer.Serialize(obj, type, JsonOptions.JsonSerializerOptions); + + internal static string SerializeRelaxedEscaping(T obj) => + JsonSerializer.Serialize(obj, JsonOptions.JsonSerializerOptionsRelaxedEscaping); + + internal static string SerializeRelaxedEscaping(object obj, global::System.Type type) => + JsonSerializer.Serialize(obj, type, JsonOptions.JsonSerializerOptionsRelaxedEscaping); + + internal static JsonElement SerializeToElement(T obj) => + JsonSerializer.SerializeToElement(obj, JsonOptions.JsonSerializerOptions); + + internal static JsonElement SerializeToElement(object obj, global::System.Type type) => + JsonSerializer.SerializeToElement(obj, type, JsonOptions.JsonSerializerOptions); + + internal static JsonDocument SerializeToDocument(T obj) => + JsonSerializer.SerializeToDocument(obj, JsonOptions.JsonSerializerOptions); + + internal static JsonNode? SerializeToNode(T obj) => + JsonSerializer.SerializeToNode(obj, JsonOptions.JsonSerializerOptions); + + internal static byte[] SerializeToUtf8Bytes(T obj) => + JsonSerializer.SerializeToUtf8Bytes(obj, JsonOptions.JsonSerializerOptions); + + internal static string SerializeWithAdditionalProperties( + T obj, + object? additionalProperties = null + ) + { + if (additionalProperties is null) + { + return Serialize(obj); + } + var additionalPropertiesJsonNode = SerializeToNode(additionalProperties); + if (additionalPropertiesJsonNode is not JsonObject additionalPropertiesJsonObject) + { + throw new InvalidOperationException( + "The additional properties must serialize to a JSON object." + ); + } + var jsonNode = SerializeToNode(obj); + if (jsonNode is not JsonObject jsonObject) + { + throw new InvalidOperationException( + "The serialized object must be a JSON object to add properties." + ); + } + MergeJsonObjects(jsonObject, additionalPropertiesJsonObject); + return jsonObject.ToJsonString(JsonOptions.JsonSerializerOptions); + } + + private static void MergeJsonObjects(JsonObject baseObject, JsonObject overrideObject) + { + foreach (var property in overrideObject) + { + if (!baseObject.TryGetPropertyValue(property.Key, out JsonNode? existingValue)) + { + baseObject[property.Key] = property.Value is not null + ? JsonNode.Parse(property.Value.ToJsonString()) + : null; + continue; + } + if ( + existingValue is JsonObject nestedBaseObject + && property.Value is JsonObject nestedOverrideObject + ) + { + // If both values are objects, recursively merge them. + MergeJsonObjects(nestedBaseObject, nestedOverrideObject); + continue; + } + // Otherwise, the overrideObject takes precedence. + baseObject[property.Key] = property.Value is not null + ? JsonNode.Parse(property.Value.ToJsonString()) + : null; + } + } + + internal static T Deserialize(string json) => + JsonSerializer.Deserialize(json, JsonOptions.JsonSerializerOptions)!; +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/JsonRequest.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/JsonRequest.cs new file mode 100644 index 000000000000..900a41b8e024 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/JsonRequest.cs @@ -0,0 +1,36 @@ +using global::System.Net.Http; + +namespace SeedBasicAuthOptional.Core; + +/// +/// The request object to be sent for JSON APIs. +/// +internal record JsonRequest : BaseRequest +{ + internal object? Body { get; init; } + + internal override HttpContent? CreateContent() + { + if (Body is null && Options?.AdditionalBodyProperties is null) + { + return null; + } + + var (encoding, charset, mediaType) = ParseContentTypeOrDefault( + ContentType, + Utf8NoBom, + "application/json" + ); + var content = new StringContent( + JsonUtils.SerializeWithAdditionalProperties(Body, Options?.AdditionalBodyProperties), + encoding, + mediaType + ); + if (string.IsNullOrEmpty(charset) && content.Headers.ContentType is not null) + { + content.Headers.ContentType.CharSet = ""; + } + + return content; + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/MultipartFormRequest.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/MultipartFormRequest.cs new file mode 100644 index 000000000000..3f61e753b056 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/MultipartFormRequest.cs @@ -0,0 +1,294 @@ +using global::System.Net.Http; +using global::System.Net.Http.Headers; + +namespace SeedBasicAuthOptional.Core; + +/// +/// The request object to be sent for multipart form data. +/// +internal record MultipartFormRequest : BaseRequest +{ + private readonly List> _partAdders = []; + + internal void AddJsonPart(string name, object? value) => AddJsonPart(name, value, null); + + internal void AddJsonPart(string name, object? value, string? contentType) + { + if (value is null) + { + return; + } + + _partAdders.Add(form => + { + var (encoding, charset, mediaType) = ParseContentTypeOrDefault( + contentType, + Utf8NoBom, + "application/json" + ); + var content = new StringContent(JsonUtils.Serialize(value), encoding, mediaType); + if (string.IsNullOrEmpty(charset) && content.Headers.ContentType is not null) + { + content.Headers.ContentType.CharSet = ""; + } + + form.Add(content, name); + }); + } + + internal void AddJsonParts(string name, IEnumerable? value) => + AddJsonParts(name, value, null); + + internal void AddJsonParts(string name, IEnumerable? value, string? contentType) + { + if (value is null) + { + return; + } + + foreach (var item in value) + { + AddJsonPart(name, item, contentType); + } + } + + internal void AddJsonParts(string name, IEnumerable? value) => + AddJsonParts(name, value, null); + + internal void AddJsonParts(string name, IEnumerable? value, string? contentType) + { + if (value is null) + { + return; + } + + foreach (var item in value) + { + AddJsonPart(name, item, contentType); + } + } + + internal void AddStringPart(string name, object? value) => AddStringPart(name, value, null); + + internal void AddStringPart(string name, object? value, string? contentType) + { + if (value is null) + { + return; + } + + AddStringPart(name, ValueConvert.ToString(value), contentType); + } + + internal void AddStringPart(string name, string? value) => AddStringPart(name, value, null); + + internal void AddStringPart(string name, string? value, string? contentType) + { + if (value is null) + { + return; + } + + _partAdders.Add(form => + { + var (encoding, charset, mediaType) = ParseContentTypeOrDefault( + contentType, + Utf8NoBom, + "text/plain" + ); + var content = new StringContent(value, encoding, mediaType); + if (string.IsNullOrEmpty(charset) && content.Headers.ContentType is not null) + { + content.Headers.ContentType.CharSet = ""; + } + + form.Add(content, name); + }); + } + + internal void AddStringParts(string name, IEnumerable? value) => + AddStringParts(name, value, null); + + internal void AddStringParts(string name, IEnumerable? value, string? contentType) + { + if (value is null) + { + return; + } + + AddStringPart(name, ValueConvert.ToString(value), contentType); + } + + internal void AddStringParts(string name, IEnumerable? value) => + AddStringParts(name, value, null); + + internal void AddStringParts(string name, IEnumerable? value, string? contentType) + { + if (value is null) + { + return; + } + + foreach (var item in value) + { + AddStringPart(name, item, contentType); + } + } + + internal void AddStreamPart(string name, Stream? stream, string? fileName) => + AddStreamPart(name, stream, fileName, null); + + internal void AddStreamPart(string name, Stream? stream, string? fileName, string? contentType) + { + if (stream is null) + { + return; + } + + _partAdders.Add(form => + { + var content = new StreamContent(stream) + { + Headers = + { + ContentType = MediaTypeHeaderValue.Parse( + contentType ?? "application/octet-stream" + ), + }, + }; + + if (fileName is not null) + { + form.Add(content, name, fileName); + } + else + { + form.Add(content, name); + } + }); + } + + internal void AddFileParameterPart(string name, Stream? stream) => + AddStreamPart(name, stream, null, null); + + internal void AddFileParameterPart(string name, FileParameter? file) => + AddFileParameterPart(name, file, null); + + internal void AddFileParameterPart( + string name, + FileParameter? file, + string? fallbackContentType + ) => + AddStreamPart(name, file?.Stream, file?.FileName, file?.ContentType ?? fallbackContentType); + + internal void AddFileParameterParts(string name, IEnumerable? files) => + AddFileParameterParts(name, files, null); + + internal void AddFileParameterParts( + string name, + IEnumerable? files, + string? fallbackContentType + ) + { + if (files is null) + { + return; + } + + foreach (var file in files) + { + AddFileParameterPart(name, file, fallbackContentType); + } + } + + internal void AddFormEncodedPart(string name, object? value) => + AddFormEncodedPart(name, value, null); + + internal void AddFormEncodedPart(string name, object? value, string? contentType) + { + if (value is null) + { + return; + } + + _partAdders.Add(form => + { + var content = FormUrlEncoder.EncodeAsForm(value); + if (!string.IsNullOrEmpty(contentType)) + { + content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); + } + + form.Add(content, name); + }); + } + + internal void AddFormEncodedParts(string name, IEnumerable? value) => + AddFormEncodedParts(name, value, null); + + internal void AddFormEncodedParts(string name, IEnumerable? value, string? contentType) + { + if (value is null) + { + return; + } + + foreach (var item in value) + { + AddFormEncodedPart(name, item, contentType); + } + } + + internal void AddExplodedFormEncodedPart(string name, object? value) => + AddExplodedFormEncodedPart(name, value, null); + + internal void AddExplodedFormEncodedPart(string name, object? value, string? contentType) + { + if (value is null) + { + return; + } + + _partAdders.Add(form => + { + var content = FormUrlEncoder.EncodeAsExplodedForm(value); + if (!string.IsNullOrEmpty(contentType)) + { + content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); + } + + form.Add(content, name); + }); + } + + internal void AddExplodedFormEncodedParts(string name, IEnumerable? value) => + AddExplodedFormEncodedParts(name, value, null); + + internal void AddExplodedFormEncodedParts( + string name, + IEnumerable? value, + string? contentType + ) + { + if (value is null) + { + return; + } + + foreach (var item in value) + { + AddExplodedFormEncodedPart(name, item, contentType); + } + } + + internal override HttpContent CreateContent() + { + var form = new MultipartFormDataContent(); + foreach (var adder in _partAdders) + { + adder(form); + } + + return form; + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/NullableAttribute.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/NullableAttribute.cs new file mode 100644 index 000000000000..c29115cb9074 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/NullableAttribute.cs @@ -0,0 +1,18 @@ +namespace SeedBasicAuthOptional.Core; + +/// +/// Marks a property as nullable in the OpenAPI specification. +/// When applied to Optional properties, this indicates that null values should be +/// written to JSON when the optional is defined with null. +/// +/// +/// For regular (required) properties: +/// - Without [Nullable]: null values are invalid (omit from JSON at runtime) +/// - With [Nullable]: null values are written to JSON +/// +/// For Optional properties (also marked with [Optional]): +/// - Without [Nullable]: Optional.Of(null) → omit from JSON (runtime edge case) +/// - With [Nullable]: Optional.Of(null) → write null to JSON +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Property, AllowMultiple = false)] +public class NullableAttribute : global::System.Attribute { } diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/OneOfSerializer.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/OneOfSerializer.cs new file mode 100644 index 000000000000..9aacfd4e5fa4 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/OneOfSerializer.cs @@ -0,0 +1,145 @@ +using global::System.Reflection; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using OneOf; + +namespace SeedBasicAuthOptional.Core; + +internal class OneOfSerializer : JsonConverter +{ + public override IOneOf? Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + if (reader.TokenType is JsonTokenType.Null) + return default; + + foreach (var (type, cast) in GetOneOfTypes(typeToConvert)) + { + try + { + var readerCopy = reader; + var result = JsonSerializer.Deserialize(ref readerCopy, type, options); + reader.Skip(); + return (IOneOf)cast.Invoke(null, [result])!; + } + catch (JsonException) { } + } + + throw new JsonException( + $"Cannot deserialize into one of the supported types for {typeToConvert}" + ); + } + + public override void Write(Utf8JsonWriter writer, IOneOf value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value.Value, options); + } + + public override IOneOf ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = reader.GetString(); + if (stringValue == null) + throw new JsonException("Cannot deserialize null property name into OneOf type"); + + // Try to deserialize the string value into one of the supported types + foreach (var (type, cast) in GetOneOfTypes(typeToConvert)) + { + try + { + // For primitive types, try direct conversion + if (type == typeof(string)) + { + return (IOneOf)cast.Invoke(null, [stringValue])!; + } + + // For other types, try to deserialize from JSON string + var result = JsonSerializer.Deserialize($"\"{stringValue}\"", type, options); + if (result != null) + { + return (IOneOf)cast.Invoke(null, [result])!; + } + } + catch { } + } + + // If no type-specific deserialization worked, default to string if available + var stringType = GetOneOfTypes(typeToConvert).FirstOrDefault(t => t.type == typeof(string)); + if (stringType != default) + { + return (IOneOf)stringType.cast.Invoke(null, [stringValue])!; + } + + throw new JsonException( + $"Cannot deserialize dictionary key '{stringValue}' into one of the supported types for {typeToConvert}" + ); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + IOneOf value, + JsonSerializerOptions options + ) + { + // Serialize the underlying value to a string suitable for use as a dictionary key + var stringValue = value.Value?.ToString() ?? "null"; + writer.WritePropertyName(stringValue); + } + + private static (global::System.Type type, MethodInfo cast)[] GetOneOfTypes( + global::System.Type typeToConvert + ) + { + var type = typeToConvert; + if (Nullable.GetUnderlyingType(type) is { } underlyingType) + { + type = underlyingType; + } + + var casts = type.GetRuntimeMethods() + .Where(m => m.IsSpecialName && m.Name == "op_Implicit") + .ToArray(); + while (type is not null) + { + if ( + type.IsGenericType + && (type.Name.StartsWith("OneOf`") || type.Name.StartsWith("OneOfBase`")) + ) + { + var genericArguments = type.GetGenericArguments(); + if (genericArguments.Length == 1) + { + return [(genericArguments[0], casts[0])]; + } + + // if object type is present, make sure it is last + var indexOfObjectType = Array.IndexOf(genericArguments, typeof(object)); + if (indexOfObjectType != -1 && genericArguments.Length - 1 != indexOfObjectType) + { + genericArguments = genericArguments + .OrderBy(t => t == typeof(object) ? 1 : 0) + .ToArray(); + } + + return genericArguments + .Select(t => (t, casts.First(c => c.GetParameters()[0].ParameterType == t))) + .ToArray(); + } + + type = type.BaseType; + } + + throw new InvalidOperationException($"{type} isn't OneOf or OneOfBase"); + } + + public override bool CanConvert(global::System.Type typeToConvert) + { + return typeof(IOneOf).IsAssignableFrom(typeToConvert); + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Optional.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Optional.cs new file mode 100644 index 000000000000..0dac756de991 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Optional.cs @@ -0,0 +1,474 @@ +using global::System.Text.Json; +using global::System.Text.Json.Serialization; + +namespace SeedBasicAuthOptional.Core; + +/// +/// Non-generic interface for Optional types to enable reflection-free checks. +/// +public interface IOptional +{ + /// + /// Returns true if the value is defined (set), even if the value is null. + /// + bool IsDefined { get; } + + /// + /// Gets the boxed value. Returns null if undefined or if the value is null. + /// + object? GetBoxedValue(); +} + +/// +/// Represents a field that can be "not set" (undefined) vs "explicitly set" (defined). +/// Use this for HTTP PATCH requests where you need to distinguish between: +/// +/// Undefined: Don't send this field (leave it unchanged on the server) +/// Defined with null: Send null (clear the field on the server) +/// Defined with value: Send the value (update the field on the server) +/// +/// +/// The type of the value. Use nullable types (T?) for fields that can be null. +/// +/// For nullable string fields, use Optional<string?>: +/// +/// public class UpdateUserRequest +/// { +/// public Optional<string?> Name { get; set; } = Optional<string?>.Undefined; +/// } +/// +/// var request = new UpdateUserRequest +/// { +/// Name = "John" // Will send: { "name": "John" } +/// }; +/// +/// var request2 = new UpdateUserRequest +/// { +/// Name = Optional<string?>.Of(null) // Will send: { "name": null } +/// }; +/// +/// var request3 = new UpdateUserRequest(); // Will send: {} (name not included) +/// +/// +public readonly struct Optional : IOptional, IEquatable> +{ + private readonly T _value; + private readonly bool _isDefined; + + private Optional(T value, bool isDefined) + { + _value = value; + _isDefined = isDefined; + } + + /// + /// Creates an undefined value - the field will not be included in the HTTP request. + /// Use this as the default value for optional fields. + /// + /// + /// + /// public Optional<string?> Email { get; set; } = Optional<string?>.Undefined; + /// + /// + public static Optional Undefined => new(default!, false); + + /// + /// Creates a defined value - the field will be included in the HTTP request. + /// The value can be null if T is a nullable type. + /// + /// The value to set. Can be null if T is nullable (e.g., string?, int?). + /// + /// + /// // Set to a value + /// request.Name = Optional<string?>.Of("John"); + /// + /// // Set to null (clears the field) + /// request.Email = Optional<string?>.Of(null); + /// + /// // Or use implicit conversion + /// request.Name = "John"; // Same as Of("John") + /// request.Email = null; // Same as Of(null) + /// + /// + public static Optional Of(T value) => new(value, true); + + /// + /// Returns true if the field is defined (set), even if the value is null. + /// Use this to determine if the field should be included in the HTTP request. + /// + /// + /// + /// if (request.Name.IsDefined) + /// { + /// requestBody["name"] = request.Name.Value; // Include in request (can be null) + /// } + /// + /// + public bool IsDefined => _isDefined; + + /// + /// Returns true if the field is undefined (not set). + /// Use this to check if the field should be excluded from the HTTP request. + /// + /// + /// + /// if (request.Email.IsUndefined) + /// { + /// // Don't include email in the request - leave it unchanged + /// } + /// + /// + public bool IsUndefined => !_isDefined; + + /// + /// Gets the value. The value may be null if T is a nullable type. + /// + /// Thrown if the value is undefined. + /// + /// Always check before accessing Value, or use instead. + /// + /// + /// + /// if (request.Name.IsDefined) + /// { + /// string? name = request.Name.Value; // Safe - can be null if Optional<string?> + /// } + /// + /// // Or check for null explicitly + /// if (request.Email.IsDefined && request.Email.Value is null) + /// { + /// // Email is explicitly set to null (clear it) + /// } + /// + /// + public T Value + { + get + { + if (!_isDefined) + throw new InvalidOperationException("Optional value is undefined"); + return _value; + } + } + + /// + /// Gets the value if defined, otherwise returns the specified default value. + /// Note: If the value is defined as null, this returns null (not the default). + /// + /// The value to return if undefined. + /// The actual value if defined (can be null), otherwise the default value. + /// + /// + /// string name = request.Name.GetValueOrDefault("Anonymous"); + /// // If Name is undefined: returns "Anonymous" + /// // If Name is Of(null): returns null + /// // If Name is Of("John"): returns "John" + /// + /// + public T GetValueOrDefault(T defaultValue = default!) + { + return _isDefined ? _value : defaultValue; + } + + /// + /// Tries to get the value. Returns true if the value is defined (even if null). + /// + /// + /// When this method returns, contains the value if defined, or default(T) if undefined. + /// The value may be null if T is nullable. + /// + /// True if the value is defined; otherwise, false. + /// + /// + /// if (request.Email.TryGetValue(out var email)) + /// { + /// requestBody["email"] = email; // email can be null + /// } + /// else + /// { + /// // Email is undefined - don't include in request + /// } + /// + /// + public bool TryGetValue(out T value) + { + if (_isDefined) + { + value = _value; + return true; + } + value = default!; + return false; + } + + /// + /// Implicitly converts a value to Optional<T>.Of(value). + /// This allows natural assignment: request.Name = "John" instead of request.Name = Optional<string?>.Of("John"). + /// + /// The value to convert (can be null if T is nullable). + public static implicit operator Optional(T value) => Of(value); + + /// + /// Returns a string representation of this Optional value. + /// + /// "Undefined" if not set, or "Defined(value)" if set. + public override string ToString() => _isDefined ? $"Defined({_value})" : "Undefined"; + + /// + /// Gets the boxed value. Returns null if undefined or if the value is null. + /// + public object? GetBoxedValue() + { + if (!_isDefined) + return null; + return _value; + } + + /// + public bool Equals(Optional other) => + _isDefined == other._isDefined && EqualityComparer.Default.Equals(_value, other._value); + + /// + public override bool Equals(object? obj) => obj is Optional other && Equals(other); + + /// + public override int GetHashCode() + { + if (!_isDefined) + return 0; + unchecked + { + int hash = 17; + hash = hash * 31 + 1; // _isDefined = true + hash = hash * 31 + (_value is null ? 0 : _value.GetHashCode()); + return hash; + } + } + + /// + /// Determines whether two Optional values are equal. + /// + /// The first Optional to compare. + /// The second Optional to compare. + /// True if the Optional values are equal; otherwise, false. + public static bool operator ==(Optional left, Optional right) => left.Equals(right); + + /// + /// Determines whether two Optional values are not equal. + /// + /// The first Optional to compare. + /// The second Optional to compare. + /// True if the Optional values are not equal; otherwise, false. + public static bool operator !=(Optional left, Optional right) => !left.Equals(right); +} + +/// +/// Extension methods for Optional to simplify common operations. +/// +public static class OptionalExtensions +{ + /// + /// Adds the value to a dictionary if the optional is defined (even if the value is null). + /// This is useful for building JSON request payloads where null values should be included. + /// + /// The type of the optional value. + /// The optional value to add. + /// The dictionary to add to. + /// The key to use in the dictionary. + /// + /// + /// var dict = new Dictionary<string, object?>(); + /// request.Name.AddTo(dict, "name"); // Adds only if Name.IsDefined + /// request.Email.AddTo(dict, "email"); // Adds only if Email.IsDefined + /// + /// + public static void AddTo( + this Optional optional, + Dictionary dictionary, + string key + ) + { + if (optional.IsDefined) + { + dictionary[key] = optional.Value; + } + } + + /// + /// Executes an action if the optional is defined. + /// + /// The type of the optional value. + /// The optional value. + /// The action to execute with the value. + /// + /// + /// request.Name.IfDefined(name => Console.WriteLine($"Name: {name}")); + /// + /// + public static void IfDefined(this Optional optional, Action action) + { + if (optional.IsDefined) + { + action(optional.Value); + } + } + + /// + /// Maps the value to a new type if the optional is defined, otherwise returns undefined. + /// + /// The type of the original value. + /// The type to map to. + /// The optional value to map. + /// The mapping function. + /// An optional containing the mapped value if defined, otherwise undefined. + /// + /// + /// Optional<string?> name = Optional<string?>.Of("John"); + /// Optional<int> length = name.Map(n => n?.Length ?? 0); // Optional.Of(4) + /// + /// + public static Optional Map( + this Optional optional, + Func mapper + ) + { + return optional.IsDefined + ? Optional.Of(mapper(optional.Value)) + : Optional.Undefined; + } + + /// + /// Adds a nullable value to a dictionary only if it is not null. + /// This is useful for regular nullable properties where null means "omit from request". + /// + /// The type of the value (must be a reference type or Nullable). + /// The nullable value to add. + /// The dictionary to add to. + /// The key to use in the dictionary. + /// + /// + /// var dict = new Dictionary<string, object?>(); + /// request.Description.AddIfNotNull(dict, "description"); // Only adds if not null + /// request.Score.AddIfNotNull(dict, "score"); // Only adds if not null + /// + /// + public static void AddIfNotNull( + this T? value, + Dictionary dictionary, + string key + ) + where T : class + { + if (value is not null) + { + dictionary[key] = value; + } + } + + /// + /// Adds a nullable value type to a dictionary only if it has a value. + /// This is useful for regular nullable properties where null means "omit from request". + /// + /// The underlying value type. + /// The nullable value to add. + /// The dictionary to add to. + /// The key to use in the dictionary. + /// + /// + /// var dict = new Dictionary<string, object?>(); + /// request.Age.AddIfNotNull(dict, "age"); // Only adds if HasValue + /// request.Score.AddIfNotNull(dict, "score"); // Only adds if HasValue + /// + /// + public static void AddIfNotNull( + this T? value, + Dictionary dictionary, + string key + ) + where T : struct + { + if (value.HasValue) + { + dictionary[key] = value.Value; + } + } +} + +/// +/// JSON converter factory for Optional that handles undefined vs null correctly. +/// Uses a TypeInfoResolver to conditionally include/exclude properties based on Optional.IsDefined. +/// +public class OptionalJsonConverterFactory : JsonConverterFactory +{ + public override bool CanConvert(global::System.Type typeToConvert) + { + if (!typeToConvert.IsGenericType) + return false; + + return typeToConvert.GetGenericTypeDefinition() == typeof(Optional<>); + } + + public override JsonConverter? CreateConverter( + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var valueType = typeToConvert.GetGenericArguments()[0]; + var converterType = typeof(OptionalJsonConverter<>).MakeGenericType(valueType); + return (JsonConverter?)global::System.Activator.CreateInstance(converterType); + } +} + +/// +/// JSON converter for Optional that unwraps the value during serialization. +/// The actual property skipping is handled by the OptionalTypeInfoResolver. +/// +public class OptionalJsonConverter : JsonConverter> +{ + public override Optional Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + if (reader.TokenType == JsonTokenType.Null) + { + return Optional.Of(default!); + } + + var value = JsonSerializer.Deserialize(ref reader, options); + return Optional.Of(value!); + } + + public override void Write( + Utf8JsonWriter writer, + Optional value, + JsonSerializerOptions options + ) + { + // This will be called by the serializer + // We need to unwrap and serialize the inner value + // The TypeInfoResolver will handle skipping undefined values + + if (value.IsUndefined) + { + // This shouldn't be called for undefined values due to ShouldSerialize + // But if it is, write null and let the resolver filter it + writer.WriteNullValue(); + return; + } + + // Get the inner value + var innerValue = value.Value; + + // Write null directly if the value is null (don't use JsonSerializer.Serialize for null) + if (innerValue is null) + { + writer.WriteNullValue(); + return; + } + + // Serialize the unwrapped value + JsonSerializer.Serialize(writer, innerValue, options); + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/OptionalAttribute.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/OptionalAttribute.cs new file mode 100644 index 000000000000..2867248f37dc --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/OptionalAttribute.cs @@ -0,0 +1,17 @@ +namespace SeedBasicAuthOptional.Core; + +/// +/// Marks a property as optional in the OpenAPI specification. +/// Optional properties use the Optional type and can be undefined (not present in JSON). +/// +/// +/// Properties marked with [Optional] should use the Optional type: +/// - Undefined: Optional.Undefined → omitted from JSON +/// - Defined: Optional.Of(value) → written to JSON +/// +/// Combine with [Nullable] to allow null values: +/// - [Optional, Nullable] Optional → can be undefined, null, or a value +/// - [Optional] Optional → can be undefined or a value (null is invalid) +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Property, AllowMultiple = false)] +public class OptionalAttribute : global::System.Attribute { } diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/AdditionalProperties.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/AdditionalProperties.cs new file mode 100644 index 000000000000..cfa2218695c6 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/AdditionalProperties.cs @@ -0,0 +1,353 @@ +using global::System.Collections; +using global::System.Collections.ObjectModel; +using global::System.Text.Json; +using global::System.Text.Json.Nodes; +using SeedBasicAuthOptional.Core; + +namespace SeedBasicAuthOptional; + +public record ReadOnlyAdditionalProperties : ReadOnlyAdditionalProperties +{ + internal ReadOnlyAdditionalProperties() { } + + internal ReadOnlyAdditionalProperties(IDictionary properties) + : base(properties) { } +} + +public record ReadOnlyAdditionalProperties : IReadOnlyDictionary +{ + private readonly Dictionary _extensionData = new(); + private readonly Dictionary _convertedCache = new(); + + internal ReadOnlyAdditionalProperties() + { + _extensionData = new Dictionary(); + _convertedCache = new Dictionary(); + } + + internal ReadOnlyAdditionalProperties(IDictionary properties) + { + _extensionData = new Dictionary(properties.Count); + _convertedCache = new Dictionary(properties.Count); + foreach (var kvp in properties) + { + if (kvp.Value is JsonElement element) + { + _extensionData.Add(kvp.Key, element); + } + else + { + _extensionData[kvp.Key] = JsonUtils.SerializeToElement(kvp.Value); + } + + _convertedCache[kvp.Key] = kvp.Value; + } + } + + private static T ConvertToT(JsonElement value) + { + if (typeof(T) == typeof(JsonElement)) + { + return (T)(object)value; + } + + return value.Deserialize(JsonOptions.JsonSerializerOptions)!; + } + + internal void CopyFromExtensionData(IDictionary extensionData) + { + _extensionData.Clear(); + _convertedCache.Clear(); + foreach (var kvp in extensionData) + { + _extensionData[kvp.Key] = kvp.Value; + if (kvp.Value is T value) + { + _convertedCache[kvp.Key] = value; + } + } + } + + private T GetCached(string key) + { + if (_convertedCache.TryGetValue(key, out var cached)) + { + return cached; + } + + var value = ConvertToT(_extensionData[key]); + _convertedCache[key] = value; + return value; + } + + public IEnumerator> GetEnumerator() + { + return _extensionData + .Select(kvp => new KeyValuePair(kvp.Key, GetCached(kvp.Key))) + .GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public int Count => _extensionData.Count; + + public bool ContainsKey(string key) => _extensionData.ContainsKey(key); + + public bool TryGetValue(string key, out T value) + { + if (_convertedCache.TryGetValue(key, out value!)) + { + return true; + } + + if (_extensionData.TryGetValue(key, out var element)) + { + value = ConvertToT(element); + _convertedCache[key] = value; + return true; + } + + return false; + } + + public T this[string key] => GetCached(key); + + public IEnumerable Keys => _extensionData.Keys; + + public IEnumerable Values => Keys.Select(GetCached); +} + +public record AdditionalProperties : AdditionalProperties +{ + public AdditionalProperties() { } + + public AdditionalProperties(IDictionary properties) + : base(properties) { } +} + +public record AdditionalProperties : IDictionary +{ + private readonly Dictionary _extensionData; + private readonly Dictionary _convertedCache; + + public AdditionalProperties() + { + _extensionData = new Dictionary(); + _convertedCache = new Dictionary(); + } + + public AdditionalProperties(IDictionary properties) + { + _extensionData = new Dictionary(properties.Count); + _convertedCache = new Dictionary(properties.Count); + foreach (var kvp in properties) + { + _extensionData[kvp.Key] = kvp.Value; + _convertedCache[kvp.Key] = kvp.Value; + } + } + + private static T ConvertToT(object? extensionDataValue) + { + return extensionDataValue switch + { + T value => value, + JsonElement jsonElement => jsonElement.Deserialize( + JsonOptions.JsonSerializerOptions + )!, + JsonNode jsonNode => jsonNode.Deserialize(JsonOptions.JsonSerializerOptions)!, + _ => JsonUtils + .SerializeToElement(extensionDataValue) + .Deserialize(JsonOptions.JsonSerializerOptions)!, + }; + } + + internal void CopyFromExtensionData(IDictionary extensionData) + { + _extensionData.Clear(); + _convertedCache.Clear(); + foreach (var kvp in extensionData) + { + _extensionData[kvp.Key] = kvp.Value; + if (kvp.Value is T value) + { + _convertedCache[kvp.Key] = value; + } + } + } + + internal void CopyToExtensionData(IDictionary extensionData) + { + extensionData.Clear(); + foreach (var kvp in _extensionData) + { + extensionData[kvp.Key] = kvp.Value; + } + } + + public JsonObject ToJsonObject() => + ( + JsonUtils.SerializeToNode(_extensionData) + ?? throw new InvalidOperationException( + "Failed to serialize AdditionalProperties to JSON Node." + ) + ).AsObject(); + + public JsonNode ToJsonNode() => + JsonUtils.SerializeToNode(_extensionData) + ?? throw new InvalidOperationException( + "Failed to serialize AdditionalProperties to JSON Node." + ); + + public JsonElement ToJsonElement() => JsonUtils.SerializeToElement(_extensionData); + + public JsonDocument ToJsonDocument() => JsonUtils.SerializeToDocument(_extensionData); + + public IReadOnlyDictionary ToJsonElementDictionary() + { + return new ReadOnlyDictionary( + _extensionData.ToDictionary( + kvp => kvp.Key, + kvp => + { + if (kvp.Value is JsonElement jsonElement) + { + return jsonElement; + } + + return JsonUtils.SerializeToElement(kvp.Value); + } + ) + ); + } + + public ICollection Keys => _extensionData.Keys; + + public ICollection Values + { + get + { + var values = new T[_extensionData.Count]; + var i = 0; + foreach (var key in Keys) + { + values[i++] = GetCached(key); + } + + return values; + } + } + + private T GetCached(string key) + { + if (_convertedCache.TryGetValue(key, out var value)) + { + return value; + } + + value = ConvertToT(_extensionData[key]); + _convertedCache.Add(key, value); + return value; + } + + private void SetCached(string key, T value) + { + _extensionData[key] = value; + _convertedCache[key] = value; + } + + private void AddCached(string key, T value) + { + _extensionData.Add(key, value); + _convertedCache.Add(key, value); + } + + private bool RemoveCached(string key) + { + var isRemoved = _extensionData.Remove(key); + _convertedCache.Remove(key); + return isRemoved; + } + + public int Count => _extensionData.Count; + public bool IsReadOnly => false; + + public T this[string key] + { + get => GetCached(key); + set => SetCached(key, value); + } + + public void Add(string key, T value) => AddCached(key, value); + + public void Add(KeyValuePair item) => AddCached(item.Key, item.Value); + + public bool Remove(string key) => RemoveCached(key); + + public bool Remove(KeyValuePair item) => RemoveCached(item.Key); + + public bool ContainsKey(string key) => _extensionData.ContainsKey(key); + + public bool Contains(KeyValuePair item) + { + return _extensionData.ContainsKey(item.Key) + && EqualityComparer.Default.Equals(GetCached(item.Key), item.Value); + } + + public bool TryGetValue(string key, out T value) + { + if (_convertedCache.TryGetValue(key, out value!)) + { + return true; + } + + if (_extensionData.TryGetValue(key, out var extensionDataValue)) + { + value = ConvertToT(extensionDataValue); + _convertedCache[key] = value; + return true; + } + + return false; + } + + public void Clear() + { + _extensionData.Clear(); + _convertedCache.Clear(); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + if (array is null) + { + throw new ArgumentNullException(nameof(array)); + } + + if (arrayIndex < 0 || arrayIndex > array.Length) + { + throw new ArgumentOutOfRangeException(nameof(arrayIndex)); + } + + if (array.Length - arrayIndex < _extensionData.Count) + { + throw new ArgumentException( + "The array does not have enough space to copy the elements." + ); + } + + foreach (var kvp in _extensionData) + { + array[arrayIndex++] = new KeyValuePair(kvp.Key, GetCached(kvp.Key)); + } + } + + public IEnumerator> GetEnumerator() + { + return _extensionData + .Select(kvp => new KeyValuePair(kvp.Key, GetCached(kvp.Key))) + .GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/ClientOptions.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/ClientOptions.cs new file mode 100644 index 000000000000..095b5e14bc8c --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/ClientOptions.cs @@ -0,0 +1,84 @@ +using SeedBasicAuthOptional.Core; + +namespace SeedBasicAuthOptional; + +[Serializable] +public partial class ClientOptions +{ + /// + /// The http headers sent with the request. + /// + internal Headers Headers { get; init; } = new(); + + /// + /// The Base URL for the API. + /// + public string BaseUrl { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = ""; + + /// + /// The http client used to make requests. + /// + public HttpClient HttpClient { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = new HttpClient(); + + /// + /// Additional headers to be sent with HTTP requests. + /// Headers with matching keys will be overwritten by headers set on the request. + /// + public IEnumerable> AdditionalHeaders { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = []; + + /// + /// The max number of retries to attempt. + /// + public int MaxRetries { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = 2; + + /// + /// The timeout for the request. + /// + public TimeSpan Timeout { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = TimeSpan.FromSeconds(30); + + /// + /// Clones this and returns a new instance + /// + internal ClientOptions Clone() + { + return new ClientOptions + { + BaseUrl = BaseUrl, + HttpClient = HttpClient, + MaxRetries = MaxRetries, + Timeout = Timeout, + Headers = new Headers(new Dictionary(Headers)), + AdditionalHeaders = AdditionalHeaders, + }; + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/FileParameter.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/FileParameter.cs new file mode 100644 index 000000000000..59afdfed2d7d --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/FileParameter.cs @@ -0,0 +1,63 @@ +namespace SeedBasicAuthOptional; + +/// +/// File parameter for uploading files. +/// +public record FileParameter : IDisposable +#if NET6_0_OR_GREATER + , IAsyncDisposable +#endif +{ + private bool _disposed; + + /// + /// The name of the file to be uploaded. + /// + public string? FileName { get; set; } + + /// + /// The content type of the file to be uploaded. + /// + public string? ContentType { get; set; } + + /// + /// The content of the file to be uploaded. + /// + public required Stream Stream { get; set; } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + if (disposing) + { + Stream.Dispose(); + } + + _disposed = true; + } + +#if NET6_0_OR_GREATER + /// + public async ValueTask DisposeAsync() + { + if (!_disposed) + { + await Stream.DisposeAsync().ConfigureAwait(false); + _disposed = true; + } + + GC.SuppressFinalize(this); + } +#endif + + public static implicit operator FileParameter(Stream stream) => new() { Stream = stream }; +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/RawResponse.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/RawResponse.cs new file mode 100644 index 000000000000..0bfb9f0a87e1 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/RawResponse.cs @@ -0,0 +1,24 @@ +using global::System.Net; + +namespace SeedBasicAuthOptional; + +/// +/// Contains HTTP response metadata including status code, URL, and headers. +/// +public record RawResponse +{ + /// + /// The HTTP status code of the response. + /// + public required HttpStatusCode StatusCode { get; init; } + + /// + /// The request URL that generated this response. + /// + public required Uri Url { get; init; } + + /// + /// The HTTP response headers. + /// + public required Core.ResponseHeaders Headers { get; init; } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/RequestOptions.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/RequestOptions.cs new file mode 100644 index 000000000000..b31eb653991f --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/RequestOptions.cs @@ -0,0 +1,86 @@ +using SeedBasicAuthOptional.Core; + +namespace SeedBasicAuthOptional; + +[Serializable] +public partial class RequestOptions : IRequestOptions +{ + /// + /// The Base URL for the API. + /// + public string? BaseUrl { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// The http client used to make requests. + /// + public HttpClient? HttpClient { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// Additional headers to be sent with the request. + /// Headers previously set with matching keys will be overwritten. + /// + public IEnumerable> AdditionalHeaders { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = []; + + /// + /// The max number of retries to attempt. + /// + public int? MaxRetries { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// The timeout for the request. + /// + public TimeSpan? Timeout { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// Additional query parameters sent with the request. + /// + public IEnumerable> AdditionalQueryParameters { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = Enumerable.Empty>(); + + /// + /// Additional body properties sent with the request. + /// This is only applied to JSON requests. + /// + public object? AdditionalBodyProperties { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/SeedBasicAuthOptionalApiException.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/SeedBasicAuthOptionalApiException.cs new file mode 100644 index 000000000000..776d063187d7 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/SeedBasicAuthOptionalApiException.cs @@ -0,0 +1,22 @@ +namespace SeedBasicAuthOptional; + +/// +/// This exception type will be thrown for any non-2XX API responses. +/// +public class SeedBasicAuthOptionalApiException( + string message, + int statusCode, + object body, + Exception? innerException = null +) : SeedBasicAuthOptionalException(message, innerException) +{ + /// + /// The error code of the response that triggered the exception. + /// + public int StatusCode => statusCode; + + /// + /// The body of the response that triggered the exception. + /// + public object Body => body; +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/SeedBasicAuthOptionalException.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/SeedBasicAuthOptionalException.cs new file mode 100644 index 000000000000..bf72810d1158 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/SeedBasicAuthOptionalException.cs @@ -0,0 +1,7 @@ +namespace SeedBasicAuthOptional; + +/// +/// Base exception class for all exceptions thrown by the SDK. +/// +public class SeedBasicAuthOptionalException(string message, Exception? innerException = null) + : Exception(message, innerException); diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/Version.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/Version.cs new file mode 100644 index 000000000000..a15c59ea7031 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/Version.cs @@ -0,0 +1,7 @@ +namespace SeedBasicAuthOptional; + +[Serializable] +internal class Version +{ + public const string Current = "0.0.1"; +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/WithRawResponse.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/WithRawResponse.cs new file mode 100644 index 000000000000..f50dbfdb493b --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/WithRawResponse.cs @@ -0,0 +1,18 @@ +namespace SeedBasicAuthOptional; + +/// +/// Wraps a parsed response value with its raw HTTP response metadata. +/// +/// The type of the parsed response data. +public readonly struct WithRawResponse +{ + /// + /// The parsed response data. + /// + public required T Data { get; init; } + + /// + /// The raw HTTP response metadata. + /// + public required RawResponse RawResponse { get; init; } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/WithRawResponseTask.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/WithRawResponseTask.cs new file mode 100644 index 000000000000..ce2975aac51c --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/WithRawResponseTask.cs @@ -0,0 +1,144 @@ +using global::System.Runtime.CompilerServices; + +namespace SeedBasicAuthOptional; + +/// +/// A task-like type that wraps Task<WithRawResponse<T>> and provides dual-mode awaiting: +/// - Direct await yields just T (zero-allocation path for common case) +/// - .WithRawResponse() yields WithRawResponse<T> (when raw response metadata is needed) +/// +/// The type of the parsed response data. +public readonly struct WithRawResponseTask +{ + private readonly global::System.Threading.Tasks.Task> _task; + + /// + /// Creates a new WithRawResponseTask wrapping the given task. + /// + public WithRawResponseTask(global::System.Threading.Tasks.Task> task) + { + _task = task; + } + + /// + /// Returns the underlying task that yields both the data and raw response metadata. + /// + public global::System.Threading.Tasks.Task> WithRawResponse() => _task; + + /// + /// Gets the custom awaiter that unwraps to just T when awaited. + /// + public Awaiter GetAwaiter() => new(_task.GetAwaiter()); + + /// + /// Configures the awaiter to continue on the captured context or not. + /// + public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext) => + new(_task.ConfigureAwait(continueOnCapturedContext)); + + /// + /// Implicitly converts WithRawResponseTask<T> to global::System.Threading.Tasks.Task<T> for backward compatibility. + /// The resulting task will yield just the data when awaited. + /// + public static implicit operator global::System.Threading.Tasks.Task( + WithRawResponseTask task + ) + { + return task._task.ContinueWith( + t => t.Result.Data, + TaskContinuationOptions.ExecuteSynchronously + ); + } + + /// + /// Custom awaiter that unwraps WithRawResponse<T> to just T. + /// + public readonly struct Awaiter : ICriticalNotifyCompletion + { + private readonly TaskAwaiter> _awaiter; + + internal Awaiter(TaskAwaiter> awaiter) + { + _awaiter = awaiter; + } + + /// + /// Gets whether the underlying task has completed. + /// + public bool IsCompleted => _awaiter.IsCompleted; + + /// + /// Gets the result, unwrapping to just the data. + /// + public T GetResult() => _awaiter.GetResult().Data; + + /// + /// Schedules the continuation action. + /// + public void OnCompleted(global::System.Action continuation) => + _awaiter.OnCompleted(continuation); + + /// + /// Schedules the continuation action without capturing the execution context. + /// + public void UnsafeOnCompleted(global::System.Action continuation) => + _awaiter.UnsafeOnCompleted(continuation); + } + + /// + /// Awaitable type returned by ConfigureAwait that unwraps to just T. + /// + public readonly struct ConfiguredTaskAwaitable + { + private readonly ConfiguredTaskAwaitable> _configuredTask; + + internal ConfiguredTaskAwaitable(ConfiguredTaskAwaitable> configuredTask) + { + _configuredTask = configuredTask; + } + + /// + /// Gets the configured awaiter that unwraps to just T. + /// + public ConfiguredAwaiter GetAwaiter() => new(_configuredTask.GetAwaiter()); + + /// + /// Custom configured awaiter that unwraps WithRawResponse<T> to just T. + /// + public readonly struct ConfiguredAwaiter : ICriticalNotifyCompletion + { + private readonly ConfiguredTaskAwaitable< + WithRawResponse + >.ConfiguredTaskAwaiter _awaiter; + + internal ConfiguredAwaiter( + ConfiguredTaskAwaitable>.ConfiguredTaskAwaiter awaiter + ) + { + _awaiter = awaiter; + } + + /// + /// Gets whether the underlying task has completed. + /// + public bool IsCompleted => _awaiter.IsCompleted; + + /// + /// Gets the result, unwrapping to just the data. + /// + public T GetResult() => _awaiter.GetResult().Data; + + /// + /// Schedules the continuation action. + /// + public void OnCompleted(global::System.Action continuation) => + _awaiter.OnCompleted(continuation); + + /// + /// Schedules the continuation action without capturing the execution context. + /// + public void UnsafeOnCompleted(global::System.Action continuation) => + _awaiter.UnsafeOnCompleted(continuation); + } + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/QueryStringBuilder.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/QueryStringBuilder.cs new file mode 100644 index 000000000000..b2eaac863b68 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/QueryStringBuilder.cs @@ -0,0 +1,469 @@ +using global::System.Buffers; +using global::System.Runtime.CompilerServices; +#if !NET6_0_OR_GREATER +using global::System.Text; +#endif + +namespace SeedBasicAuthOptional.Core; + +/// +/// High-performance query string builder with cross-platform optimizations. +/// Uses span-based APIs on .NET 6+ and StringBuilder fallback for older targets. +/// +internal static class QueryStringBuilder +{ +#if NET8_0_OR_GREATER + private static readonly SearchValues UnreservedChars = SearchValues.Create( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~" + ); +#else + private const string UnreservedChars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~"; +#endif + +#if NET7_0_OR_GREATER + private static ReadOnlySpan UpperHexChars => "0123456789ABCDEF"u8; +#else + private static readonly byte[] UpperHexChars = + { + (byte)'0', + (byte)'1', + (byte)'2', + (byte)'3', + (byte)'4', + (byte)'5', + (byte)'6', + (byte)'7', + (byte)'8', + (byte)'9', + (byte)'A', + (byte)'B', + (byte)'C', + (byte)'D', + (byte)'E', + (byte)'F', + }; +#endif + + /// + /// Builds a query string from the provided parameters. + /// +#if NET6_0_OR_GREATER + public static string Build(ReadOnlySpan> parameters) + { + if (parameters.IsEmpty) + return string.Empty; + + var estimatedLength = EstimateLength(parameters); + if (estimatedLength == 0) + return string.Empty; + + var bufferSize = Math.Min(estimatedLength * 3, 8192); + var buffer = ArrayPool.Shared.Rent(bufferSize); + + try + { + var written = BuildCore(parameters, buffer); + return new string(buffer.AsSpan(0, written)); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + private static int EstimateLength(ReadOnlySpan> parameters) + { + var estimatedLength = 0; + foreach (var kvp in parameters) + { + estimatedLength += kvp.Key.Length + kvp.Value.Length + 2; + } + return estimatedLength; + } +#endif + + /// + /// Builds a query string from the provided parameters. + /// + public static string Build(IEnumerable> parameters) + { +#if NET6_0_OR_GREATER + // Try to get span access for collections that support it + if (parameters is ICollection> collection) + { + if (collection.Count == 0) + return string.Empty; + + var array = ArrayPool>.Shared.Rent(collection.Count); + try + { + collection.CopyTo(array, 0); + return Build(array.AsSpan(0, collection.Count)); + } + finally + { + ArrayPool>.Shared.Return(array); + } + } + + // Fallback for non-collection enumerables + using var enumerator = parameters.GetEnumerator(); + if (!enumerator.MoveNext()) + return string.Empty; + + var buffer = ArrayPool.Shared.Rent(4096); + try + { + var position = 0; + var first = true; + + do + { + var kvp = enumerator.Current; + + // Ensure capacity (worst case: 3x for encoding + separators) + var required = (kvp.Key.Length + kvp.Value.Length + 2) * 3; + if (position + required > buffer.Length) + { + var newBuffer = ArrayPool.Shared.Rent(buffer.Length * 2); + buffer.AsSpan(0, position).CopyTo(newBuffer); + ArrayPool.Shared.Return(buffer); + buffer = newBuffer; + } + + buffer[position++] = first ? '?' : '&'; + first = false; + + position += EncodeComponent(kvp.Key.AsSpan(), buffer.AsSpan(position)); + buffer[position++] = '='; + position += EncodeComponent(kvp.Value.AsSpan(), buffer.AsSpan(position)); + } while (enumerator.MoveNext()); + + return first ? string.Empty : new string(buffer.AsSpan(0, position)); + } + finally + { + ArrayPool.Shared.Return(buffer); + } +#else + // netstandard2.0 / net462 fallback using StringBuilder + var sb = new StringBuilder(); + var first = true; + + foreach (var kvp in parameters) + { + sb.Append(first ? '?' : '&'); + first = false; + + AppendEncoded(sb, kvp.Key); + sb.Append('='); + AppendEncoded(sb, kvp.Value); + } + + return sb.ToString(); +#endif + } + +#if NET6_0_OR_GREATER + private static int BuildCore( + ReadOnlySpan> parameters, + Span buffer + ) + { + var position = 0; + var first = true; + + foreach (var kvp in parameters) + { + buffer[position++] = first ? '?' : '&'; + first = false; + + position += EncodeComponent(kvp.Key.AsSpan(), buffer.Slice(position)); + buffer[position++] = '='; + position += EncodeComponent(kvp.Value.AsSpan(), buffer.Slice(position)); + } + + return position; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int EncodeComponent(ReadOnlySpan input, Span output) + { + if (!NeedsEncoding(input)) + { + input.CopyTo(output); + return input.Length; + } + + return EncodeSlow(input, output); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool NeedsEncoding(ReadOnlySpan value) + { + return value.ContainsAnyExcept(UnreservedChars); + } + + private static int EncodeSlow(ReadOnlySpan input, Span output) + { + var position = 0; + + foreach (var c in input) + { + if (IsUnreserved(c)) + { + output[position++] = c; + } + else if (c == ' ') + { + output[position++] = '%'; + output[position++] = '2'; + output[position++] = '0'; + } + else if (char.IsAscii(c)) + { + position += EncodeAscii((byte)c, output.Slice(position)); + } + else + { + position += EncodeUtf8(c, output.Slice(position)); + } + } + + return position; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int EncodeAscii(byte value, Span output) + { + output[0] = '%'; + output[1] = (char)UpperHexChars[value >> 4]; + output[2] = (char)UpperHexChars[value & 0xF]; + return 3; + } + + private static int EncodeUtf8(char c, Span output) + { + Span utf8Bytes = stackalloc byte[4]; + Span singleChar = stackalloc char[1] { c }; + var byteCount = global::System.Text.Encoding.UTF8.GetBytes(singleChar, utf8Bytes); + + var position = 0; + for (var i = 0; i < byteCount; i++) + { + output[position++] = '%'; + output[position++] = (char)UpperHexChars[utf8Bytes[i] >> 4]; + output[position++] = (char)UpperHexChars[utf8Bytes[i] & 0xF]; + } + + return position; + } +#else + // netstandard2.0 / net462 StringBuilder-based encoding + private static void AppendEncoded(StringBuilder sb, string value) + { + foreach (var c in value) + { + if (IsUnreserved(c)) + { + sb.Append(c); + } + else if (c == ' ') + { + sb.Append("%20"); + } + else if (c <= 127) + { + AppendPercentEncoded(sb, (byte)c); + } + else + { + var bytes = Encoding.UTF8.GetBytes(new[] { c }); + foreach (var b in bytes) + { + AppendPercentEncoded(sb, b); + } + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AppendPercentEncoded(StringBuilder sb, byte value) + { + sb.Append('%'); + sb.Append((char)UpperHexChars[value >> 4]); + sb.Append((char)UpperHexChars[value & 0xF]); + } +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsUnreserved(char c) + { +#if NET8_0_OR_GREATER + return UnreservedChars.Contains(c); +#else + return (c >= 'A' && c <= 'Z') + || (c >= 'a' && c <= 'z') + || (c >= '0' && c <= '9') + || c == '-' + || c == '_' + || c == '.' + || c == '~'; +#endif + } + + /// + /// Fluent builder for constructing query strings with support for simple parameters and deep object notation. + /// + public sealed class Builder + { + private readonly List> _params; + + /// + /// Initializes a new instance with default capacity. + /// + public Builder() + { + _params = new List>(); + } + + /// + /// Initializes a new instance with the specified initial capacity. + /// + public Builder(int capacity) + { + _params = new List>(capacity); + } + + /// + /// Adds a simple parameter. For collections, adds multiple key-value pairs (one per element). + /// + public Builder Add(string key, object? value) + { + if (value is null) + { + return this; + } + + // Handle string separately since it implements IEnumerable + if (value is string stringValue) + { + _params.Add(new KeyValuePair(key, stringValue)); + return this; + } + + // Handle collections (arrays, lists, etc.) - add each element as a separate key-value pair + if ( + value + is global::System.Collections.IEnumerable enumerable + and not global::System.Collections.IDictionary + ) + { + foreach (var item in enumerable) + { + if (item is not null) + { + _params.Add( + new KeyValuePair( + key, + ValueConvert.ToQueryStringValue(item) + ) + ); + } + } + return this; + } + + // Handle scalar values + _params.Add( + new KeyValuePair(key, ValueConvert.ToQueryStringValue(value)) + ); + return this; + } + + /// + /// Sets a parameter, removing any existing parameters with the same key before adding the new value. + /// For collections, removes all existing parameters with the key, then adds multiple key-value pairs (one per element). + /// This allows overriding parameters set earlier in the builder. + /// + public Builder Set(string key, object? value) + { + // Remove all existing parameters with this key + _params.RemoveAll(kv => kv.Key == key); + + // Add the new value(s) + return Add(key, value); + } + + /// + /// Merges additional query parameters with override semantics. + /// Groups parameters by key and calls Set() once per unique key. + /// This ensures that parameters with the same key are properly merged: + /// - If a key appears once, it's added as a single value + /// - If a key appears multiple times, all values are added as an array + /// - All parameters override any existing parameters with the same key + /// + public Builder MergeAdditional( + global::System.Collections.Generic.IEnumerable>? additionalParameters + ) + { + if (additionalParameters is null) + { + return this; + } + + // Group by key to handle multiple values for the same key correctly + var grouped = additionalParameters + .GroupBy(kv => kv.Key) + .Select(g => new global::System.Collections.Generic.KeyValuePair( + g.Key, + g.Count() == 1 ? (object)g.First().Value : g.Select(kv => kv.Value).ToArray() + )); + + foreach (var param in grouped) + { + Set(param.Key, param.Value); + } + + return this; + } + + /// + /// Adds a complex object using deep object notation with a prefix. + /// Deep object notation nests properties with brackets: prefix[key][nested]=value + /// + public Builder AddDeepObject(string prefix, object? value) + { + if (value is not null) + { + _params.AddRange(QueryStringConverter.ToDeepObject(prefix, value)); + } + return this; + } + + /// + /// Adds a complex object using exploded form notation with an optional prefix. + /// Exploded form flattens properties: prefix[key]=value (no deep nesting). + /// + public Builder AddExploded(string prefix, object? value) + { + if (value is not null) + { + _params.AddRange(QueryStringConverter.ToExplodedForm(prefix, value)); + } + return this; + } + + /// + /// Builds the final query string. + /// + public string Build() + { + return QueryStringBuilder.Build(_params); + } + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/QueryStringConverter.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/QueryStringConverter.cs new file mode 100644 index 000000000000..3f9a49404a2c --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/QueryStringConverter.cs @@ -0,0 +1,259 @@ +using global::System.Text.Json; + +namespace SeedBasicAuthOptional.Core; + +/// +/// Converts an object into a query string collection. +/// +internal static class QueryStringConverter +{ + /// + /// Converts an object into a query string collection using Deep Object notation with a prefix. + /// + /// The prefix to prepend to all keys (e.g., "session_settings"). Pass empty string for no prefix. + /// Object to form URL-encode. Can be an object, array of objects, or dictionary. + /// Throws when passing in a string or primitive value. + /// A collection of key value pairs. The keys and values are not URL encoded. + internal static IEnumerable> ToDeepObject( + string prefix, + object value + ) + { + var queryCollection = new List>(); + var json = JsonUtils.SerializeToElement(value); + JsonToDeepObject(json, prefix, queryCollection); + return queryCollection; + } + + /// + /// Converts an object into a query string collection using Deep Object notation. + /// + /// Object to form URL-encode. Can be an object, array of objects, or dictionary. + /// Throws when passing in a string or primitive value. + /// A collection of key value pairs. The keys and values are not URL encoded. + internal static IEnumerable> ToDeepObject(object value) + { + return ToDeepObject("", value); + } + + /// + /// Converts an object into a query string collection using Exploded Form notation with a prefix. + /// + /// The prefix to prepend to all keys. Pass empty string for no prefix. + /// Object to form URL-encode. You can pass in an object or dictionary, but not lists, strings, or primitives. + /// Throws when passing in a list, a string, or a primitive value. + /// A collection of key value pairs. The keys and values are not URL encoded. + internal static IEnumerable> ToExplodedForm( + string prefix, + object value + ) + { + var queryCollection = new List>(); + var json = JsonUtils.SerializeToElement(value); + AssertRootJson(json); + JsonToFormExploded(json, prefix, queryCollection); + return queryCollection; + } + + /// + /// Converts an object into a query string collection using Exploded Form notation. + /// + /// Object to form URL-encode. You can pass in an object or dictionary, but not lists, strings, or primitives. + /// Throws when passing in a list, a string, or a primitive value. + /// A collection of key value pairs. The keys and values are not URL encoded. + internal static IEnumerable> ToExplodedForm(object value) + { + return ToExplodedForm("", value); + } + + /// + /// Converts an object into a query string collection using Form notation without exploding parameters. + /// + /// Object to form URL-encode. You can pass in an object or dictionary, but not lists, strings, or primitives. + /// Throws when passing in a list, a string, or a primitive value. + /// A collection of key value pairs. The keys and values are not URL encoded. + internal static IEnumerable> ToForm(object value) + { + var queryCollection = new List>(); + var json = JsonUtils.SerializeToElement(value); + AssertRootJson(json); + JsonToForm(json, "", queryCollection); + return queryCollection; + } + + private static void AssertRootJson(JsonElement json) + { + switch (json.ValueKind) + { + case JsonValueKind.Object: + break; + case JsonValueKind.Array: + case JsonValueKind.Undefined: + case JsonValueKind.String: + case JsonValueKind.Number: + case JsonValueKind.True: + case JsonValueKind.False: + case JsonValueKind.Null: + default: + throw new global::System.Exception( + $"Only objects can be converted to query string collections. Given type is {json.ValueKind}." + ); + } + } + + private static void JsonToForm( + JsonElement element, + string prefix, + List> parameters + ) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + foreach (var property in element.EnumerateObject()) + { + var newPrefix = string.IsNullOrEmpty(prefix) + ? property.Name + : $"{prefix}[{property.Name}]"; + + JsonToForm(property.Value, newPrefix, parameters); + } + break; + case JsonValueKind.Array: + var arrayValues = element.EnumerateArray().Select(ValueToString).ToArray(); + parameters.Add( + new KeyValuePair(prefix, string.Join(",", arrayValues)) + ); + break; + case JsonValueKind.Null: + break; + case JsonValueKind.Undefined: + case JsonValueKind.String: + case JsonValueKind.Number: + case JsonValueKind.True: + case JsonValueKind.False: + default: + parameters.Add(new KeyValuePair(prefix, ValueToString(element))); + break; + } + } + + private static void JsonToFormExploded( + JsonElement element, + string prefix, + List> parameters + ) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + foreach (var property in element.EnumerateObject()) + { + var newPrefix = string.IsNullOrEmpty(prefix) + ? property.Name + : $"{prefix}[{property.Name}]"; + + JsonToFormExploded(property.Value, newPrefix, parameters); + } + + break; + case JsonValueKind.Array: + foreach (var item in element.EnumerateArray()) + { + if ( + item.ValueKind != JsonValueKind.Object + && item.ValueKind != JsonValueKind.Array + ) + { + parameters.Add( + new KeyValuePair(prefix, ValueToString(item)) + ); + } + else + { + JsonToFormExploded(item, prefix, parameters); + } + } + + break; + case JsonValueKind.Null: + break; + case JsonValueKind.Undefined: + case JsonValueKind.String: + case JsonValueKind.Number: + case JsonValueKind.True: + case JsonValueKind.False: + default: + parameters.Add(new KeyValuePair(prefix, ValueToString(element))); + break; + } + } + + private static void JsonToDeepObject( + JsonElement element, + string prefix, + List> parameters + ) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + foreach (var property in element.EnumerateObject()) + { + var newPrefix = string.IsNullOrEmpty(prefix) + ? property.Name + : $"{prefix}[{property.Name}]"; + + JsonToDeepObject(property.Value, newPrefix, parameters); + } + + break; + case JsonValueKind.Array: + var index = 0; + foreach (var item in element.EnumerateArray()) + { + var newPrefix = $"{prefix}[{index++}]"; + + if ( + item.ValueKind != JsonValueKind.Object + && item.ValueKind != JsonValueKind.Array + ) + { + parameters.Add( + new KeyValuePair(newPrefix, ValueToString(item)) + ); + } + else + { + JsonToDeepObject(item, newPrefix, parameters); + } + } + + break; + case JsonValueKind.Null: + case JsonValueKind.Undefined: + // Skip null and undefined values - don't add parameters for them + break; + case JsonValueKind.String: + case JsonValueKind.Number: + case JsonValueKind.True: + case JsonValueKind.False: + default: + parameters.Add(new KeyValuePair(prefix, ValueToString(element))); + break; + } + } + + private static string ValueToString(JsonElement element) + { + return element.ValueKind switch + { + JsonValueKind.String => element.GetString() ?? "", + JsonValueKind.Number => element.GetRawText(), + JsonValueKind.True => "true", + JsonValueKind.False => "false", + JsonValueKind.Null => "", + _ => element.GetRawText(), + }; + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/RawClient.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/RawClient.cs new file mode 100644 index 000000000000..c0c46ffc8168 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/RawClient.cs @@ -0,0 +1,344 @@ +using global::System.Net.Http; +using global::System.Net.Http.Headers; +using global::System.Text; +using SystemTask = global::System.Threading.Tasks.Task; + +namespace SeedBasicAuthOptional.Core; + +/// +/// Utility class for making raw HTTP requests to the API. +/// +internal partial class RawClient(ClientOptions clientOptions) +{ + private const int MaxRetryDelayMs = 60000; + private const double JitterFactor = 0.2; +#if NET6_0_OR_GREATER + // Use Random.Shared for thread-safe random number generation on .NET 6+ +#else + private static readonly object JitterLock = new(); + private static readonly Random JitterRandom = new(); +#endif + internal int BaseRetryDelay { get; set; } = 1000; + + /// + /// The client options applied on every request. + /// + internal readonly ClientOptions Options = clientOptions; + + internal async global::System.Threading.Tasks.Task SendRequestAsync( + global::SeedBasicAuthOptional.Core.BaseRequest request, + CancellationToken cancellationToken = default + ) + { + // Apply the request timeout. + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var timeout = request.Options?.Timeout ?? Options.Timeout; + cts.CancelAfter(timeout); + + var httpRequest = await CreateHttpRequestAsync(request).ConfigureAwait(false); + // Send the request. + return await SendWithRetriesAsync(httpRequest, request.Options, cts.Token) + .ConfigureAwait(false); + } + + internal async global::System.Threading.Tasks.Task SendRequestAsync( + HttpRequestMessage request, + IRequestOptions? options, + CancellationToken cancellationToken = default + ) + { + // Apply the request timeout. + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var timeout = options?.Timeout ?? Options.Timeout; + cts.CancelAfter(timeout); + + // Send the request. + return await SendWithRetriesAsync(request, options, cts.Token).ConfigureAwait(false); + } + + private static async global::System.Threading.Tasks.Task CloneRequestAsync( + HttpRequestMessage request + ) + { + var clonedRequest = new HttpRequestMessage(request.Method, request.RequestUri); + clonedRequest.Version = request.Version; + + if (request.Content != null) + { + switch (request.Content) + { + case MultipartContent oldMultipartFormContent: + var originalBoundary = + oldMultipartFormContent + .Headers.ContentType?.Parameters.First(p => + p.Name.Equals("boundary", StringComparison.OrdinalIgnoreCase) + ) + .Value?.Trim('"') + ?? Guid.NewGuid().ToString(); + var newMultipartContent = oldMultipartFormContent switch + { + MultipartFormDataContent => new MultipartFormDataContent(originalBoundary), + _ => new MultipartContent(), + }; + foreach (var content in oldMultipartFormContent) + { + var ms = new MemoryStream(); + await content.CopyToAsync(ms).ConfigureAwait(false); + ms.Position = 0; + var newPart = new StreamContent(ms); + foreach (var header in content.Headers) + { + newPart.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + newMultipartContent.Add(newPart); + } + + clonedRequest.Content = newMultipartContent; + break; + default: + var bodyStream = new MemoryStream(); + await request.Content.CopyToAsync(bodyStream).ConfigureAwait(false); + bodyStream.Position = 0; + var clonedContent = new StreamContent(bodyStream); + foreach (var header in request.Content.Headers) + { + clonedContent.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + clonedRequest.Content = clonedContent; + break; + } + } + + foreach (var header in request.Headers) + { + clonedRequest.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + return clonedRequest; + } + + /// + /// Sends the request with retries, unless the request content is not retryable, + /// such as stream requests and multipart form data with stream content. + /// + private async global::System.Threading.Tasks.Task SendWithRetriesAsync( + HttpRequestMessage request, + IRequestOptions? options, + CancellationToken cancellationToken + ) + { + var httpClient = options?.HttpClient ?? Options.HttpClient; + var maxRetries = options?.MaxRetries ?? Options.MaxRetries; + var response = await httpClient + .SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken) + .ConfigureAwait(false); + var isRetryableContent = IsRetryableContent(request); + + if (!isRetryableContent) + { + return new global::SeedBasicAuthOptional.Core.ApiResponse + { + StatusCode = (int)response.StatusCode, + Raw = response, + }; + } + + for (var i = 0; i < maxRetries; i++) + { + if (!ShouldRetry(response)) + { + break; + } + + var delayMs = GetRetryDelayFromHeaders(response, i); + await SystemTask.Delay(delayMs, cancellationToken).ConfigureAwait(false); + using var retryRequest = await CloneRequestAsync(request).ConfigureAwait(false); + response = await httpClient + .SendAsync( + retryRequest, + HttpCompletionOption.ResponseHeadersRead, + cancellationToken + ) + .ConfigureAwait(false); + } + + return new global::SeedBasicAuthOptional.Core.ApiResponse + { + StatusCode = (int)response.StatusCode, + Raw = response, + }; + } + + private static bool ShouldRetry(HttpResponseMessage response) + { + var statusCode = (int)response.StatusCode; + return statusCode is 408 or 429 or >= 500; + } + + private static int AddPositiveJitter(int delayMs) + { +#if NET6_0_OR_GREATER + var random = Random.Shared.NextDouble(); +#else + double random; + lock (JitterLock) + { + random = JitterRandom.NextDouble(); + } +#endif + var jitterMultiplier = 1 + random * JitterFactor; + return (int)(delayMs * jitterMultiplier); + } + + private static int AddSymmetricJitter(int delayMs) + { +#if NET6_0_OR_GREATER + var random = Random.Shared.NextDouble(); +#else + double random; + lock (JitterLock) + { + random = JitterRandom.NextDouble(); + } +#endif + var jitterMultiplier = 1 + (random - 0.5) * JitterFactor; + return (int)(delayMs * jitterMultiplier); + } + + private int GetRetryDelayFromHeaders(HttpResponseMessage response, int retryAttempt) + { + if (response.Headers.TryGetValues("Retry-After", out var retryAfterValues)) + { + var retryAfter = retryAfterValues.FirstOrDefault(); + if (!string.IsNullOrEmpty(retryAfter)) + { + if (int.TryParse(retryAfter, out var retryAfterSeconds) && retryAfterSeconds > 0) + { + return Math.Min(retryAfterSeconds * 1000, MaxRetryDelayMs); + } + + if (DateTimeOffset.TryParse(retryAfter, out var retryAfterDate)) + { + var delay = (int)(retryAfterDate - DateTimeOffset.UtcNow).TotalMilliseconds; + if (delay > 0) + { + return Math.Min(delay, MaxRetryDelayMs); + } + } + } + } + + if (response.Headers.TryGetValues("X-RateLimit-Reset", out var rateLimitResetValues)) + { + var rateLimitReset = rateLimitResetValues.FirstOrDefault(); + if ( + !string.IsNullOrEmpty(rateLimitReset) + && long.TryParse(rateLimitReset, out var resetTime) + ) + { + var resetDateTime = DateTimeOffset.FromUnixTimeSeconds(resetTime); + var delay = (int)(resetDateTime - DateTimeOffset.UtcNow).TotalMilliseconds; + if (delay > 0) + { + return AddPositiveJitter(Math.Min(delay, MaxRetryDelayMs)); + } + } + } + + var exponentialDelay = Math.Min(BaseRetryDelay * (1 << retryAttempt), MaxRetryDelayMs); + return AddSymmetricJitter(exponentialDelay); + } + + private static bool IsRetryableContent(HttpRequestMessage request) + { + return request.Content switch + { + IIsRetryableContent c => c.IsRetryable, + StreamContent => false, + MultipartContent content => !content.Any(c => c is StreamContent), + _ => true, + }; + } + + internal async global::System.Threading.Tasks.Task CreateHttpRequestAsync( + global::SeedBasicAuthOptional.Core.BaseRequest request + ) + { + var url = BuildUrl(request); + var httpRequest = new HttpRequestMessage(request.Method, url); + httpRequest.Content = request.CreateContent(); + SetHeaders(httpRequest, request.Headers); + + return httpRequest; + } + + private string BuildUrl(global::SeedBasicAuthOptional.Core.BaseRequest request) + { + var baseUrl = request.Options?.BaseUrl ?? request.BaseUrl ?? Options.BaseUrl; + + var trimmedBaseUrl = baseUrl.TrimEnd('/'); + var trimmedBasePath = request.Path.TrimStart('/'); + var url = $"{trimmedBaseUrl}/{trimmedBasePath}"; + + // Append query string if present + if (!string.IsNullOrEmpty(request.QueryString)) + { + return url + request.QueryString; + } + + return url; + } + + private void SetHeaders(HttpRequestMessage httpRequest, Dictionary? headers) + { + if (headers is null) + { + return; + } + + foreach (var kv in headers) + { + if (kv.Value is null) + { + continue; + } + + httpRequest.Headers.TryAddWithoutValidation(kv.Key, kv.Value); + } + } + + private static (Encoding encoding, string? charset, string mediaType) ParseContentTypeOrDefault( + string? contentType, + Encoding encodingFallback, + string mediaTypeFallback + ) + { + var encoding = encodingFallback; + var mediaType = mediaTypeFallback; + string? charset = null; + if (string.IsNullOrEmpty(contentType)) + { + return (encoding, charset, mediaType); + } + + if (!MediaTypeHeaderValue.TryParse(contentType, out var mediaTypeHeaderValue)) + { + return (encoding, charset, mediaType); + } + + if (!string.IsNullOrEmpty(mediaTypeHeaderValue.CharSet)) + { + charset = mediaTypeHeaderValue.CharSet; + encoding = Encoding.GetEncoding(mediaTypeHeaderValue.CharSet); + } + + if (!string.IsNullOrEmpty(mediaTypeHeaderValue.MediaType)) + { + mediaType = mediaTypeHeaderValue.MediaType; + } + + return (encoding, charset, mediaType); + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/RawResponse.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/RawResponse.cs new file mode 100644 index 000000000000..86c0a315bdf0 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/RawResponse.cs @@ -0,0 +1,24 @@ +using global::System.Net; + +namespace SeedBasicAuthOptional.Core; + +/// +/// Contains HTTP response metadata including status code, URL, and headers. +/// +public record RawResponse +{ + /// + /// The HTTP status code of the response. + /// + public required HttpStatusCode StatusCode { get; init; } + + /// + /// The request URL that generated this response. + /// + public required Uri Url { get; init; } + + /// + /// The HTTP response headers. + /// + public required Core.ResponseHeaders Headers { get; init; } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/ResponseHeaders.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/ResponseHeaders.cs new file mode 100644 index 000000000000..e2c7c2b77595 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/ResponseHeaders.cs @@ -0,0 +1,108 @@ +using global::System.Collections; +using global::System.Net.Http.Headers; + +namespace SeedBasicAuthOptional.Core; + +/// +/// Represents HTTP response headers with case-insensitive lookup. +/// +public readonly struct ResponseHeaders : IEnumerable +{ + private readonly HttpResponseHeaders? _headers; + private readonly HttpContentHeaders? _contentHeaders; + + private ResponseHeaders(HttpResponseHeaders headers, HttpContentHeaders? contentHeaders) + { + _headers = headers; + _contentHeaders = contentHeaders; + } + + /// + /// Gets the Content-Type header value, if present. + /// + public string? ContentType => _contentHeaders?.ContentType?.ToString(); + + /// + /// Gets the Content-Length header value, if present. + /// + public long? ContentLength => _contentHeaders?.ContentLength; + + /// + /// Creates a ResponseHeaders instance from an HttpResponseMessage. + /// + public static ResponseHeaders FromHttpResponseMessage(HttpResponseMessage response) + { + return new ResponseHeaders(response.Headers, response.Content?.Headers); + } + + /// + /// Tries to get a single header value. Returns the first value if multiple values exist. + /// + public bool TryGetValue(string name, out string? value) + { + if (TryGetValues(name, out var values) && values is not null) + { + value = values.FirstOrDefault(); + return true; + } + + value = null; + return false; + } + + /// + /// Tries to get all values for a header. + /// + public bool TryGetValues(string name, out IEnumerable? values) + { + if (_headers?.TryGetValues(name, out values) == true) + { + return true; + } + + if (_contentHeaders?.TryGetValues(name, out values) == true) + { + return true; + } + + values = null; + return false; + } + + /// + /// Checks if the headers contain a specific header name. + /// + public bool Contains(string name) + { + return _headers?.Contains(name) == true || _contentHeaders?.Contains(name) == true; + } + + /// + /// Gets an enumerator for all headers. + /// + public IEnumerator GetEnumerator() + { + if (_headers is not null) + { + foreach (var header in _headers) + { + yield return new HttpHeader(header.Key, string.Join(", ", header.Value)); + } + } + + if (_contentHeaders is not null) + { + foreach (var header in _contentHeaders) + { + yield return new HttpHeader(header.Key, string.Join(", ", header.Value)); + } + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} + +/// +/// Represents a single HTTP header. +/// +public readonly record struct HttpHeader(string Name, string Value); diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/StreamRequest.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/StreamRequest.cs new file mode 100644 index 000000000000..6d98922df29b --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/StreamRequest.cs @@ -0,0 +1,29 @@ +using global::System.Net.Http; +using global::System.Net.Http.Headers; + +namespace SeedBasicAuthOptional.Core; + +/// +/// The request object to be sent for streaming uploads. +/// +internal record StreamRequest : BaseRequest +{ + internal Stream? Body { get; init; } + + internal override HttpContent? CreateContent() + { + if (Body is null) + { + return null; + } + + var content = new StreamContent(Body) + { + Headers = + { + ContentType = MediaTypeHeaderValue.Parse(ContentType ?? "application/octet-stream"), + }, + }; + return content; + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/StringEnum.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/StringEnum.cs new file mode 100644 index 000000000000..3d3a3a39d207 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/StringEnum.cs @@ -0,0 +1,6 @@ +namespace SeedBasicAuthOptional.Core; + +public interface IStringEnum : IEquatable +{ + public string Value { get; } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/StringEnumExtensions.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/StringEnumExtensions.cs new file mode 100644 index 000000000000..ec99d9954684 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/StringEnumExtensions.cs @@ -0,0 +1,6 @@ +namespace SeedBasicAuthOptional.Core; + +internal static class StringEnumExtensions +{ + public static string Stringify(this IStringEnum stringEnum) => stringEnum.Value; +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/ValueConvert.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/ValueConvert.cs new file mode 100644 index 000000000000..0507e04f0e08 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/ValueConvert.cs @@ -0,0 +1,114 @@ +using global::System.Globalization; + +namespace SeedBasicAuthOptional.Core; + +/// +/// Convert values to string for path and query parameters. +/// +public static class ValueConvert +{ + internal static string ToPathParameterString(T value) => ToString(value); + + internal static string ToPathParameterString(bool v) => ToString(v); + + internal static string ToPathParameterString(int v) => ToString(v); + + internal static string ToPathParameterString(long v) => ToString(v); + + internal static string ToPathParameterString(float v) => ToString(v); + + internal static string ToPathParameterString(double v) => ToString(v); + + internal static string ToPathParameterString(decimal v) => ToString(v); + + internal static string ToPathParameterString(short v) => ToString(v); + + internal static string ToPathParameterString(ushort v) => ToString(v); + + internal static string ToPathParameterString(uint v) => ToString(v); + + internal static string ToPathParameterString(ulong v) => ToString(v); + + internal static string ToPathParameterString(string v) => ToString(v); + + internal static string ToPathParameterString(char v) => ToString(v); + + internal static string ToPathParameterString(Guid v) => ToString(v); + + internal static string ToQueryStringValue(T value) => value is null ? "" : ToString(value); + + internal static string ToQueryStringValue(bool v) => ToString(v); + + internal static string ToQueryStringValue(int v) => ToString(v); + + internal static string ToQueryStringValue(long v) => ToString(v); + + internal static string ToQueryStringValue(float v) => ToString(v); + + internal static string ToQueryStringValue(double v) => ToString(v); + + internal static string ToQueryStringValue(decimal v) => ToString(v); + + internal static string ToQueryStringValue(short v) => ToString(v); + + internal static string ToQueryStringValue(ushort v) => ToString(v); + + internal static string ToQueryStringValue(uint v) => ToString(v); + + internal static string ToQueryStringValue(ulong v) => ToString(v); + + internal static string ToQueryStringValue(string v) => v is null ? "" : v; + + internal static string ToQueryStringValue(char v) => ToString(v); + + internal static string ToQueryStringValue(Guid v) => ToString(v); + + internal static string ToString(T value) + { + return value switch + { + null => "null", + string str => str, + true => "true", + false => "false", + int i => ToString(i), + long l => ToString(l), + float f => ToString(f), + double d => ToString(d), + decimal dec => ToString(dec), + short s => ToString(s), + ushort u => ToString(u), + uint u => ToString(u), + ulong u => ToString(u), + char c => ToString(c), + Guid guid => ToString(guid), + _ => JsonUtils.SerializeRelaxedEscaping(value, value.GetType()).Trim('"'), + }; + } + + internal static string ToString(bool v) => v ? "true" : "false"; + + internal static string ToString(int v) => v.ToString(CultureInfo.InvariantCulture); + + internal static string ToString(long v) => v.ToString(CultureInfo.InvariantCulture); + + internal static string ToString(float v) => v.ToString(CultureInfo.InvariantCulture); + + internal static string ToString(double v) => v.ToString(CultureInfo.InvariantCulture); + + internal static string ToString(decimal v) => v.ToString(CultureInfo.InvariantCulture); + + internal static string ToString(short v) => v.ToString(CultureInfo.InvariantCulture); + + internal static string ToString(ushort v) => v.ToString(CultureInfo.InvariantCulture); + + internal static string ToString(uint v) => v.ToString(CultureInfo.InvariantCulture); + + internal static string ToString(ulong v) => v.ToString(CultureInfo.InvariantCulture); + + internal static string ToString(char v) => v.ToString(CultureInfo.InvariantCulture); + + internal static string ToString(string v) => v; + + internal static string ToString(Guid v) => v.ToString("D"); +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Errors/Exceptions/BadRequest.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Errors/Exceptions/BadRequest.cs new file mode 100644 index 000000000000..202ed74919a4 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Errors/Exceptions/BadRequest.cs @@ -0,0 +1,7 @@ +namespace SeedBasicAuthOptional; + +/// +/// This exception type will be thrown for any non-2XX API responses. +/// +[Serializable] +public class BadRequest(object body) : SeedBasicAuthOptionalApiException("BadRequest", 400, body); diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Errors/Exceptions/UnauthorizedRequest.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Errors/Exceptions/UnauthorizedRequest.cs new file mode 100644 index 000000000000..3cb3690f6b3a --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Errors/Exceptions/UnauthorizedRequest.cs @@ -0,0 +1,14 @@ +namespace SeedBasicAuthOptional; + +/// +/// This exception type will be thrown for any non-2XX API responses. +/// +[Serializable] +public class UnauthorizedRequest(UnauthorizedRequestErrorBody body) + : SeedBasicAuthOptionalApiException("UnauthorizedRequest", 401, body) +{ + /// + /// The body of the response that triggered the exception. + /// + public new UnauthorizedRequestErrorBody Body => body; +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Errors/Types/UnauthorizedRequestErrorBody.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Errors/Types/UnauthorizedRequestErrorBody.cs new file mode 100644 index 000000000000..038ffe868775 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Errors/Types/UnauthorizedRequestErrorBody.cs @@ -0,0 +1,28 @@ +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using SeedBasicAuthOptional.Core; + +namespace SeedBasicAuthOptional; + +[Serializable] +public record UnauthorizedRequestErrorBody : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("message")] + public required string Message { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/ISeedBasicAuthOptionalClient.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/ISeedBasicAuthOptionalClient.cs new file mode 100644 index 000000000000..cd6ea32ea6b2 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/ISeedBasicAuthOptionalClient.cs @@ -0,0 +1,6 @@ +namespace SeedBasicAuthOptional; + +public partial interface ISeedBasicAuthOptionalClient +{ + public IBasicAuthClient BasicAuth { get; } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/SeedBasicAuthOptional.Custom.props b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/SeedBasicAuthOptional.Custom.props new file mode 100644 index 000000000000..17a84cada530 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/SeedBasicAuthOptional.Custom.props @@ -0,0 +1,20 @@ + + + + diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/SeedBasicAuthOptional.csproj b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/SeedBasicAuthOptional.csproj new file mode 100644 index 000000000000..51c362c8292d --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/SeedBasicAuthOptional.csproj @@ -0,0 +1,63 @@ + + + net462;net8.0;net9.0;netstandard2.0 + enable + 12 + enable + 0.0.1 + $(Version) + $(Version) + README.md + https://github.com/basic-auth-optional/fern + true + + + + false + + + $(DefineConstants);USE_PORTABLE_DATE_ONLY + true + + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + <_Parameter1>SeedBasicAuthOptional.Test + + + + + diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/SeedBasicAuthOptionalClient.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/SeedBasicAuthOptionalClient.cs new file mode 100644 index 000000000000..3f4f6ee0f7d2 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/SeedBasicAuthOptionalClient.cs @@ -0,0 +1,40 @@ +using SeedBasicAuthOptional.Core; + +namespace SeedBasicAuthOptional; + +public partial class SeedBasicAuthOptionalClient : ISeedBasicAuthOptionalClient +{ + private readonly RawClient _client; + + public SeedBasicAuthOptionalClient( + string? username = null, + string? password = null, + ClientOptions? clientOptions = null + ) + { + clientOptions ??= new ClientOptions(); + var platformHeaders = new Headers( + new Dictionary() + { + { "X-Fern-Language", "C#" }, + { "X-Fern-SDK-Name", "SeedBasicAuthOptional" }, + { "X-Fern-SDK-Version", Version.Current }, + { "User-Agent", "Fernbasic-auth-optional/0.0.1" }, + } + ); + foreach (var header in platformHeaders) + { + if (!clientOptions.Headers.ContainsKey(header.Key)) + { + clientOptions.Headers[header.Key] = header.Value; + } + } + var clientOptionsWithAuth = clientOptions.Clone(); + clientOptionsWithAuth.Headers["Authorization"] = + $"Basic {Convert.ToBase64String(global::System.Text.Encoding.UTF8.GetBytes($"{username ?? ""}:{password ?? ""}"))}"; + _client = new RawClient(clientOptionsWithAuth); + BasicAuth = new BasicAuthClient(_client); + } + + public IBasicAuthClient BasicAuth { get; } +} diff --git a/test-definitions/fern/apis/basic-auth-optional/definition/api.yml b/test-definitions/fern/apis/basic-auth-optional/definition/api.yml new file mode 100644 index 000000000000..8b1d72b0b769 --- /dev/null +++ b/test-definitions/fern/apis/basic-auth-optional/definition/api.yml @@ -0,0 +1,12 @@ +name: basic-auth-optional +auth: Basic +auth-schemes: + Basic: + scheme: basic + username: + name: username + password: + name: password + omit: true +error-discrimination: + strategy: status-code diff --git a/test-definitions/fern/apis/basic-auth-optional/definition/basic-auth.yml b/test-definitions/fern/apis/basic-auth-optional/definition/basic-auth.yml new file mode 100644 index 000000000000..6a21fd56c9f9 --- /dev/null +++ b/test-definitions/fern/apis/basic-auth-optional/definition/basic-auth.yml @@ -0,0 +1,39 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json + +imports: + errors: ./errors.yml + +service: + auth: false + base-path: "" + endpoints: + getWithBasicAuth: + auth: true + docs: GET request with basic auth scheme + path: /basic-auth + method: GET + response: + boolean + examples: + - response: + body: true + errors: + - errors.UnauthorizedRequest + + postWithBasicAuth: + auth: true + docs: POST request with basic auth scheme + path: /basic-auth + method: POST + request: + name: PostWithBasicAuth + body: unknown + response: boolean + examples: + - request: + key: "value" + response: + body: true + errors: + - errors.UnauthorizedRequest + - errors.BadRequest diff --git a/test-definitions/fern/apis/basic-auth-optional/definition/errors.yml b/test-definitions/fern/apis/basic-auth-optional/definition/errors.yml new file mode 100644 index 000000000000..cdd6a9667031 --- /dev/null +++ b/test-definitions/fern/apis/basic-auth-optional/definition/errors.yml @@ -0,0 +1,11 @@ +errors: + UnauthorizedRequest: + status-code: 401 + type: UnauthorizedRequestErrorBody + BadRequest: + status-code: 400 + +types: + UnauthorizedRequestErrorBody: + properties: + message: string diff --git a/test-definitions/fern/apis/basic-auth-optional/generators.yml b/test-definitions/fern/apis/basic-auth-optional/generators.yml new file mode 100644 index 000000000000..b30d6ed97cd2 --- /dev/null +++ b/test-definitions/fern/apis/basic-auth-optional/generators.yml @@ -0,0 +1,22 @@ +# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json +groups: + php-sdk: + generators: + - name: fernapi/fern-php-sdk + version: latest + ir-version: v61 + github: + token: ${GITHUB_TOKEN} + mode: push + uri: fern-api/php-sdk-tests + branch: basic-auth-optional + go-sdk: + generators: + - name: fernapi/fern-go-sdk + version: latest + ir-version: v61 + github: + token: ${GITHUB_TOKEN} + mode: push + uri: fern-api/go-sdk-tests + branch: basic-auth-optional From 99467fe67c7fd08caee577b297f60aa7d9907081 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:52:17 +0000 Subject: [PATCH 02/18] fix(csharp-sdk): use per-field omit checks and constructor optionality instead of coarse eitherOmitted flag Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../src/root-client/RootClientGenerator.ts | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/generators/csharp/sdk/src/root-client/RootClientGenerator.ts b/generators/csharp/sdk/src/root-client/RootClientGenerator.ts index 6e9590ba7846..97a3a71f7305 100644 --- a/generators/csharp/sdk/src/root-client/RootClientGenerator.ts +++ b/generators/csharp/sdk/src/root-client/RootClientGenerator.ts @@ -466,24 +466,29 @@ export class RootClientGenerator extends FileGenerator 1) { const controlFlowKeyword = i === 0 ? "if" : "else if"; innerWriter.controlFlow(controlFlowKeyword, this.csharp.codeblock(condition)); } - if (eitherOmitted) { - innerWriter.writeTextStatement( - `clientOptionsWithAuth.Headers["Authorization"] = $"Basic {Convert.ToBase64String(global::System.Text.Encoding.UTF8.GetBytes($"{${usernameAccess} ?? ""}:{${passwordAccess} ?? ""}"))}"` - ); - } else { - innerWriter.writeTextStatement( - `clientOptionsWithAuth.Headers["Authorization"] = $"Basic {Convert.ToBase64String(global::System.Text.Encoding.UTF8.GetBytes($"{${usernameAccess}}:{${passwordAccess}}"))}"` - ); - } + // Per-field null coalescing: only omittable fields get ?? "" fallback + const usernameExpr = usernameOmitted ? `${usernameAccess} ?? ""` : `${usernameAccess}`; + const passwordExpr = passwordOmitted ? `${passwordAccess} ?? ""` : `${passwordAccess}`; + innerWriter.writeTextStatement( + `clientOptionsWithAuth.Headers["Authorization"] = $"Basic {Convert.ToBase64String(global::System.Text.Encoding.UTF8.GetBytes($"{${usernameExpr}}:{${passwordExpr}}"))}"` + ); if (isAuthOptional || basicSchemes.length > 1) { innerWriter.endControlFlow(); } @@ -809,11 +814,13 @@ export class RootClientGenerator extends FileGenerator Date: Wed, 1 Apr 2026 16:15:22 +0000 Subject: [PATCH 03/18] fix(csharp-sdk): regenerate seed output for basic-auth-optional after per-field omit fix Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../src/SeedBasicAuthOptional/SeedBasicAuthOptionalClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/SeedBasicAuthOptionalClient.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/SeedBasicAuthOptionalClient.cs index 3f4f6ee0f7d2..faa7f44d8808 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/SeedBasicAuthOptionalClient.cs +++ b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/SeedBasicAuthOptionalClient.cs @@ -31,7 +31,7 @@ public SeedBasicAuthOptionalClient( } var clientOptionsWithAuth = clientOptions.Clone(); clientOptionsWithAuth.Headers["Authorization"] = - $"Basic {Convert.ToBase64String(global::System.Text.Encoding.UTF8.GetBytes($"{username ?? ""}:{password ?? ""}"))}"; + $"Basic {Convert.ToBase64String(global::System.Text.Encoding.UTF8.GetBytes($"{username}:{password ?? ""}"))}"; _client = new RawClient(clientOptionsWithAuth); BasicAuth = new BasicAuthClient(_client); } From a405301cc88c4d7795759e61e09b2509b9f0721b Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Wed, 1 Apr 2026 18:36:02 +0000 Subject: [PATCH 04/18] fix(csharp-sdk): update createdAt date to 2026-04-01 for v2.55.4 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- generators/csharp/sdk/versions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generators/csharp/sdk/versions.yml b/generators/csharp/sdk/versions.yml index eee56caa7df5..e48818fc8124 100644 --- a/generators/csharp/sdk/versions.yml +++ b/generators/csharp/sdk/versions.yml @@ -8,7 +8,7 @@ encodes `:password`). When neither is provided, the Authorization header is omitted entirely. type: feat - createdAt: "2026-03-31" + createdAt: "2026-04-01" irVersion: 65 - version: 2.55.3 From b41fd85a4394c40b531fd7eec482bc4980c3e594 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Thu, 2 Apr 2026 00:16:12 +0000 Subject: [PATCH 05/18] fix(csharp-sdk): remove omitted fields entirely from constructor params, use empty string internally Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../src/root-client/RootClientGenerator.ts | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/generators/csharp/sdk/src/root-client/RootClientGenerator.ts b/generators/csharp/sdk/src/root-client/RootClientGenerator.ts index 97a3a71f7305..ffd5bcb4c561 100644 --- a/generators/csharp/sdk/src/root-client/RootClientGenerator.ts +++ b/generators/csharp/sdk/src/root-client/RootClientGenerator.ts @@ -468,24 +468,24 @@ export class RootClientGenerator extends FileGenerator 1) { const controlFlowKeyword = i === 0 ? "if" : "else if"; innerWriter.controlFlow(controlFlowKeyword, this.csharp.codeblock(condition)); } - // Per-field null coalescing: only omittable fields get ?? "" fallback - const usernameExpr = usernameOmitted ? `${usernameAccess} ?? ""` : `${usernameAccess}`; - const passwordExpr = passwordOmitted ? `${passwordAccess} ?? ""` : `${passwordAccess}`; + // Omitted fields use empty string directly + const usernameExpr = usernameOmitted ? `""` : `${usernameAccess}`; + const passwordExpr = passwordOmitted ? `""` : `${passwordAccess}`; innerWriter.writeTextStatement( `clientOptionsWithAuth.Headers["Authorization"] = $"Basic {Convert.ToBase64String(global::System.Text.Encoding.UTF8.GetBytes($"{${usernameExpr}}:{${passwordExpr}}"))}"` ); @@ -814,13 +814,15 @@ export class RootClientGenerator extends FileGenerator Date: Thu, 2 Apr 2026 01:16:43 +0000 Subject: [PATCH 06/18] ci: retrigger CI after flaky csharp-sdk cancellation Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> From abe481bc8c53603433370a4bfe1a55c5b3cc1b03 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Thu, 2 Apr 2026 01:49:34 +0000 Subject: [PATCH 07/18] ci: retrigger CI for flaky csharp-sdk seed-test-results cancellation Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> From 61e689909809b683ad249a9ed130462fbbe0969d Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Thu, 2 Apr 2026 02:33:10 +0000 Subject: [PATCH 08/18] ci: retrigger CI for flaky csharp-sdk seed-test-results cancellation (attempt 3) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> From 86bdc62482c278572fb3947aa73ff439b118e3ff Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Thu, 2 Apr 2026 02:59:22 +0000 Subject: [PATCH 09/18] ci: retrigger CI for flaky csharp-sdk seed-test-results cancellation (attempt 4) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> From a4edff79977c9032e96d9482175589c2db69d091 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Thu, 2 Apr 2026 17:23:10 +0000 Subject: [PATCH 10/18] fix(csharp-sdk): skip auth header when both fields omitted and auth is non-mandatory Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- generators/csharp/sdk/src/root-client/RootClientGenerator.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/generators/csharp/sdk/src/root-client/RootClientGenerator.ts b/generators/csharp/sdk/src/root-client/RootClientGenerator.ts index ffd5bcb4c561..0e18671a145e 100644 --- a/generators/csharp/sdk/src/root-client/RootClientGenerator.ts +++ b/generators/csharp/sdk/src/root-client/RootClientGenerator.ts @@ -477,7 +477,8 @@ export class RootClientGenerator extends FileGenerator 1) { const controlFlowKeyword = i === 0 ? "if" : "else if"; From 6867963e0cf9b89ff6883ec3ba9319bdf0b0f063 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Fri, 3 Apr 2026 02:18:20 +0000 Subject: [PATCH 11/18] fix(csharp-sdk): use 'omit' instead of 'optional' in versions.yml changelog and code comment Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../csharp/sdk/src/root-client/RootClientGenerator.ts | 2 +- generators/csharp/sdk/versions.yml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/generators/csharp/sdk/src/root-client/RootClientGenerator.ts b/generators/csharp/sdk/src/root-client/RootClientGenerator.ts index 0d219276bf31..9943076755eb 100644 --- a/generators/csharp/sdk/src/root-client/RootClientGenerator.ts +++ b/generators/csharp/sdk/src/root-client/RootClientGenerator.ts @@ -478,7 +478,7 @@ export class RootClientGenerator extends FileGenerator 1) { diff --git a/generators/csharp/sdk/versions.yml b/generators/csharp/sdk/versions.yml index cd227a9266ac..a4e0bc950ad3 100644 --- a/generators/csharp/sdk/versions.yml +++ b/generators/csharp/sdk/versions.yml @@ -2,11 +2,11 @@ - version: 2.56.6 changelogEntry: - summary: | - Support optional username and password in basic auth. The SDK now accepts - username-only, password-only, or both credentials. Missing fields are treated - as empty strings (e.g., username-only encodes `username:`, password-only - encodes `:password`). When neither is provided, the Authorization header is - omitted entirely. + Support omitting username or password from basic auth when configured via + `usernameOmit` or `passwordOmit` in the IR. Omitted fields are removed from + the SDK's public API and treated as empty strings internally (e.g., omitting + password encodes `username:`, omitting username encodes `:password`). When + both are omitted, the Authorization header is skipped entirely. type: feat createdAt: "2026-04-03" irVersion: 65 From 90f03d8ea17e7d4ab6ff8a8a8c43e710558efce5 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Fri, 3 Apr 2026 15:37:22 +0000 Subject: [PATCH 12/18] refactor: rename basic-auth-optional fixture to basic-auth-pw-omitted Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- ...pe_errors_UnauthorizedRequestErrorBody.json | 0 .../SeedBasicAuthOptional.slnx | 4 ---- .../Errors/Exceptions/BadRequest.cs | 7 ------- .../ISeedBasicAuthOptionalClient.cs | 6 ------ .../.editorconfig | 0 .../.fern/metadata.json | 0 .../.github/workflows/ci.yml | 0 .../.gitignore | 0 .../README.md | 14 +++++++------- .../SeedBasicAuthPwOmitted.slnx | 4 ++++ .../reference.md | 4 ++-- .../snippet.json | 4 ++-- .../src/SeedApi.DynamicSnippets/Example0.cs | 4 ++-- .../src/SeedApi.DynamicSnippets/Example1.cs | 4 ++-- .../src/SeedApi.DynamicSnippets/Example2.cs | 4 ++-- .../src/SeedApi.DynamicSnippets/Example3.cs | 4 ++-- .../src/SeedApi.DynamicSnippets/Example4.cs | 4 ++-- .../src/SeedApi.DynamicSnippets/Example5.cs | 4 ++-- .../src/SeedApi.DynamicSnippets/Example6.cs | 4 ++-- .../SeedApi.DynamicSnippets.csproj | 0 .../Core/HeadersBuilderTests.cs | 4 ++-- .../Core/Json/AdditionalPropertiesTests.cs | 4 ++-- .../Core/Json/DateOnlyJsonTests.cs | 4 ++-- .../Core/Json/DateTimeJsonTests.cs | 4 ++-- .../Core/Json/JsonAccessAttributeTests.cs | 4 ++-- .../Core/QueryStringBuilderTests.cs | 4 ++-- .../Core/QueryStringConverterTests.cs | 4 ++-- .../Core/RawClientTests/MultipartFormTests.cs | 8 ++++---- .../Core/RawClientTests/QueryParameterTests.cs | 4 ++-- .../Core/RawClientTests/RetriesTests.cs | 10 +++++----- .../Core/WithRawResponseTests.cs | 6 +++--- .../SeedBasicAuthPwOmitted.Test.Custom.props} | 0 .../SeedBasicAuthPwOmitted.Test.csproj} | 6 +++--- .../SeedBasicAuthOptional.Test/TestClient.cs | 2 +- .../Unit/MockServer/BaseMockServerTest.cs | 8 ++++---- .../BasicAuth/GetWithBasicAuthTest.cs | 6 +++--- .../BasicAuth/PostWithBasicAuthTest.cs | 6 +++--- .../Utils/AdditionalPropertiesComparer.cs | 4 ++-- .../Utils/JsonAssert.cs | 4 ++-- .../Utils/JsonElementComparer.cs | 0 .../Utils/NUnitExtensions.cs | 0 .../Utils/OneOfComparer.cs | 0 .../Utils/OptionalComparer.cs | 2 +- .../Utils/ReadOnlyMemoryComparer.cs | 0 .../BasicAuth/BasicAuthClient.cs | 16 ++++++++-------- .../BasicAuth/IBasicAuthClient.cs | 2 +- .../SeedBasicAuthOptional/Core/ApiResponse.cs | 2 +- .../SeedBasicAuthOptional/Core/BaseRequest.cs | 2 +- .../Core/CollectionItemSerializer.cs | 2 +- .../SeedBasicAuthOptional/Core/Constants.cs | 2 +- .../Core/DateOnlyConverter.cs | 2 +- .../Core/DateTimeSerializer.cs | 2 +- .../SeedBasicAuthOptional/Core/EmptyRequest.cs | 2 +- .../Core/EncodingCache.cs | 2 +- .../SeedBasicAuthOptional/Core/Extensions.cs | 2 +- .../Core/FormUrlEncoder.cs | 2 +- .../SeedBasicAuthOptional/Core/HeaderValue.cs | 2 +- .../src/SeedBasicAuthOptional/Core/Headers.cs | 2 +- .../Core/HeadersBuilder.cs | 2 +- .../Core/HttpContentExtensions.cs | 2 +- .../Core/HttpMethodExtensions.cs | 2 +- .../Core/IIsRetryableContent.cs | 2 +- .../Core/IRequestOptions.cs | 2 +- .../Core/JsonAccessAttribute.cs | 2 +- .../Core/JsonConfiguration.cs | 2 +- .../SeedBasicAuthOptional/Core/JsonRequest.cs | 2 +- .../Core/MultipartFormRequest.cs | 2 +- .../Core/NullableAttribute.cs | 2 +- .../Core/OneOfSerializer.cs | 2 +- .../src/SeedBasicAuthOptional/Core/Optional.cs | 2 +- .../Core/OptionalAttribute.cs | 2 +- .../Core/Public/AdditionalProperties.cs | 4 ++-- .../Core/Public/ClientOptions.cs | 4 ++-- .../Core/Public/FileParameter.cs | 2 +- .../Core/Public/RawResponse.cs | 2 +- .../Core/Public/RequestOptions.cs | 4 ++-- .../SeedBasicAuthPwOmittedApiException.cs} | 6 +++--- .../Public/SeedBasicAuthPwOmittedException.cs} | 4 ++-- .../Core/Public/Version.cs | 2 +- .../Core/Public/WithRawResponse.cs | 2 +- .../Core/Public/WithRawResponseTask.cs | 2 +- .../Core/QueryStringBuilder.cs | 2 +- .../Core/QueryStringConverter.cs | 2 +- .../SeedBasicAuthOptional/Core/RawClient.cs | 18 +++++++++--------- .../SeedBasicAuthOptional/Core/RawResponse.cs | 2 +- .../Core/ResponseHeaders.cs | 2 +- .../Core/StreamRequest.cs | 2 +- .../SeedBasicAuthOptional/Core/StringEnum.cs | 2 +- .../Core/StringEnumExtensions.cs | 2 +- .../SeedBasicAuthOptional/Core/ValueConvert.cs | 2 +- .../Errors/Exceptions/BadRequest.cs | 7 +++++++ .../Errors/Exceptions/UnauthorizedRequest.cs | 4 ++-- .../Types/UnauthorizedRequestErrorBody.cs | 4 ++-- .../ISeedBasicAuthPwOmittedClient.cs | 6 ++++++ .../SeedBasicAuthPwOmitted.Custom.props} | 0 .../SeedBasicAuthPwOmitted.csproj} | 8 ++++---- .../SeedBasicAuthPwOmittedClient.cs} | 12 ++++++------ .../definition/api.yml | 2 +- .../definition/basic-auth.yml | 0 .../definition/errors.yml | 0 .../generators.yml | 4 ++-- 101 files changed, 173 insertions(+), 173 deletions(-) rename packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/{basic-auth-optional => basic-auth-pw-omitted}/type_errors_UnauthorizedRequestErrorBody.json (100%) delete mode 100644 seed/csharp-sdk/basic-auth-optional/SeedBasicAuthOptional.slnx delete mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Errors/Exceptions/BadRequest.cs delete mode 100644 seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/ISeedBasicAuthOptionalClient.cs rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/.editorconfig (100%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/.fern/metadata.json (100%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/.github/workflows/ci.yml (100%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/.gitignore (100%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/README.md (92%) create mode 100644 seed/csharp-sdk/basic-auth-pw-omitted/SeedBasicAuthPwOmitted.slnx rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/reference.md (76%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/snippet.json (60%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedApi.DynamicSnippets/Example0.cs (79%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedApi.DynamicSnippets/Example1.cs (79%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedApi.DynamicSnippets/Example2.cs (79%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedApi.DynamicSnippets/Example3.cs (84%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedApi.DynamicSnippets/Example4.cs (84%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedApi.DynamicSnippets/Example5.cs (84%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedApi.DynamicSnippets/Example6.cs (84%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedApi.DynamicSnippets/SeedApi.DynamicSnippets.csproj (100%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional.Test/Core/HeadersBuilderTests.cs (99%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional.Test/Core/Json/AdditionalPropertiesTests.cs (99%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional.Test/Core/Json/DateOnlyJsonTests.cs (97%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional.Test/Core/Json/DateTimeJsonTests.cs (98%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional.Test/Core/Json/JsonAccessAttributeTests.cs (98%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional.Test/Core/QueryStringBuilderTests.cs (99%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional.Test/Core/QueryStringConverterTests.cs (98%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional.Test/Core/RawClientTests/MultipartFormTests.cs (99%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional.Test/Core/RawClientTests/QueryParameterTests.cs (97%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional.Test/Core/RawClientTests/RetriesTests.cs (97%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional.Test/Core/WithRawResponseTests.cs (98%) rename seed/csharp-sdk/{basic-auth-optional/src/SeedBasicAuthOptional.Test/SeedBasicAuthOptional.Test.Custom.props => basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/SeedBasicAuthPwOmitted.Test.Custom.props} (100%) rename seed/csharp-sdk/{basic-auth-optional/src/SeedBasicAuthOptional.Test/SeedBasicAuthOptional.Test.csproj => basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/SeedBasicAuthPwOmitted.Test.csproj} (87%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional.Test/TestClient.cs (62%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional.Test/Unit/MockServer/BaseMockServerTest.cs (79%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional.Test/Unit/MockServer/BasicAuth/GetWithBasicAuthTest.cs (89%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional.Test/Unit/MockServer/BasicAuth/PostWithBasicAuthTest.cs (92%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional.Test/Utils/AdditionalPropertiesComparer.cs (99%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional.Test/Utils/JsonAssert.cs (92%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional.Test/Utils/JsonElementComparer.cs (100%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional.Test/Utils/NUnitExtensions.cs (100%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional.Test/Utils/OneOfComparer.cs (100%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional.Test/Utils/OptionalComparer.cs (99%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional.Test/Utils/ReadOnlyMemoryComparer.cs (100%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/BasicAuth/BasicAuthClient.cs (93%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/BasicAuth/IBasicAuthClient.cs (94%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/ApiResponse.cs (86%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/BaseRequest.cs (97%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/CollectionItemSerializer.cs (98%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/Constants.cs (81%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/DateOnlyConverter.cs (99%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/DateTimeSerializer.cs (96%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/EmptyRequest.cs (85%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/EncodingCache.cs (85%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/Extensions.cs (98%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/FormUrlEncoder.cs (97%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/HeaderValue.cs (97%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/Headers.cs (95%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/HeadersBuilder.cs (99%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/HttpContentExtensions.cs (93%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/HttpMethodExtensions.cs (78%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/IIsRetryableContent.cs (66%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/IRequestOptions.cs (97%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/JsonAccessAttribute.cs (89%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/JsonConfiguration.cs (99%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/JsonRequest.cs (95%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/MultipartFormRequest.cs (99%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/NullableAttribute.cs (95%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/OneOfSerializer.cs (99%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/Optional.cs (99%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/OptionalAttribute.cs (95%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/Public/AdditionalProperties.cs (99%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/Public/ClientOptions.cs (96%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/Public/FileParameter.cs (97%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/Public/RawResponse.cs (94%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/Public/RequestOptions.cs (96%) rename seed/csharp-sdk/{basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/SeedBasicAuthOptionalApiException.cs => basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Public/SeedBasicAuthPwOmittedApiException.cs} (76%) rename seed/csharp-sdk/{basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/SeedBasicAuthOptionalException.cs => basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Public/SeedBasicAuthPwOmittedException.cs} (51%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/Public/Version.cs (71%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/Public/WithRawResponse.cs (93%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/Public/WithRawResponseTask.cs (99%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/QueryStringBuilder.cs (99%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/QueryStringConverter.cs (99%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/RawClient.cs (95%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/RawResponse.cs (93%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/ResponseHeaders.cs (98%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/StreamRequest.cs (94%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/StringEnum.cs (69%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/StringEnumExtensions.cs (77%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Core/ValueConvert.cs (99%) create mode 100644 seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Errors/Exceptions/BadRequest.cs rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Errors/Exceptions/UnauthorizedRequest.cs (75%) rename seed/csharp-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/SeedBasicAuthOptional/Errors/Types/UnauthorizedRequestErrorBody.cs (91%) create mode 100644 seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/ISeedBasicAuthPwOmittedClient.cs rename seed/csharp-sdk/{basic-auth-optional/src/SeedBasicAuthOptional/SeedBasicAuthOptional.Custom.props => basic-auth-pw-omitted/src/SeedBasicAuthOptional/SeedBasicAuthPwOmitted.Custom.props} (100%) rename seed/csharp-sdk/{basic-auth-optional/src/SeedBasicAuthOptional/SeedBasicAuthOptional.csproj => basic-auth-pw-omitted/src/SeedBasicAuthOptional/SeedBasicAuthPwOmitted.csproj} (90%) rename seed/csharp-sdk/{basic-auth-optional/src/SeedBasicAuthOptional/SeedBasicAuthOptionalClient.cs => basic-auth-pw-omitted/src/SeedBasicAuthOptional/SeedBasicAuthPwOmittedClient.cs} (76%) rename test-definitions/fern/apis/{basic-auth-optional => basic-auth-pw-omitted}/definition/api.yml (86%) rename test-definitions/fern/apis/{basic-auth-optional => basic-auth-pw-omitted}/definition/basic-auth.yml (100%) rename test-definitions/fern/apis/{basic-auth-optional => basic-auth-pw-omitted}/definition/errors.yml (100%) rename test-definitions/fern/apis/{basic-auth-optional => basic-auth-pw-omitted}/generators.yml (86%) diff --git a/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/basic-auth-optional/type_errors_UnauthorizedRequestErrorBody.json b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/basic-auth-pw-omitted/type_errors_UnauthorizedRequestErrorBody.json similarity index 100% rename from packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/basic-auth-optional/type_errors_UnauthorizedRequestErrorBody.json rename to packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/basic-auth-pw-omitted/type_errors_UnauthorizedRequestErrorBody.json diff --git a/seed/csharp-sdk/basic-auth-optional/SeedBasicAuthOptional.slnx b/seed/csharp-sdk/basic-auth-optional/SeedBasicAuthOptional.slnx deleted file mode 100644 index 9870a035ef0a..000000000000 --- a/seed/csharp-sdk/basic-auth-optional/SeedBasicAuthOptional.slnx +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Errors/Exceptions/BadRequest.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Errors/Exceptions/BadRequest.cs deleted file mode 100644 index 202ed74919a4..000000000000 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Errors/Exceptions/BadRequest.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace SeedBasicAuthOptional; - -/// -/// This exception type will be thrown for any non-2XX API responses. -/// -[Serializable] -public class BadRequest(object body) : SeedBasicAuthOptionalApiException("BadRequest", 400, body); diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/ISeedBasicAuthOptionalClient.cs b/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/ISeedBasicAuthOptionalClient.cs deleted file mode 100644 index cd6ea32ea6b2..000000000000 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/ISeedBasicAuthOptionalClient.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace SeedBasicAuthOptional; - -public partial interface ISeedBasicAuthOptionalClient -{ - public IBasicAuthClient BasicAuth { get; } -} diff --git a/seed/csharp-sdk/basic-auth-optional/.editorconfig b/seed/csharp-sdk/basic-auth-pw-omitted/.editorconfig similarity index 100% rename from seed/csharp-sdk/basic-auth-optional/.editorconfig rename to seed/csharp-sdk/basic-auth-pw-omitted/.editorconfig diff --git a/seed/csharp-sdk/basic-auth-optional/.fern/metadata.json b/seed/csharp-sdk/basic-auth-pw-omitted/.fern/metadata.json similarity index 100% rename from seed/csharp-sdk/basic-auth-optional/.fern/metadata.json rename to seed/csharp-sdk/basic-auth-pw-omitted/.fern/metadata.json diff --git a/seed/csharp-sdk/basic-auth-optional/.github/workflows/ci.yml b/seed/csharp-sdk/basic-auth-pw-omitted/.github/workflows/ci.yml similarity index 100% rename from seed/csharp-sdk/basic-auth-optional/.github/workflows/ci.yml rename to seed/csharp-sdk/basic-auth-pw-omitted/.github/workflows/ci.yml diff --git a/seed/csharp-sdk/basic-auth-optional/.gitignore b/seed/csharp-sdk/basic-auth-pw-omitted/.gitignore similarity index 100% rename from seed/csharp-sdk/basic-auth-optional/.gitignore rename to seed/csharp-sdk/basic-auth-pw-omitted/.gitignore diff --git a/seed/csharp-sdk/basic-auth-optional/README.md b/seed/csharp-sdk/basic-auth-pw-omitted/README.md similarity index 92% rename from seed/csharp-sdk/basic-auth-optional/README.md rename to seed/csharp-sdk/basic-auth-pw-omitted/README.md index 68e678269f42..416d3d0f71fe 100644 --- a/seed/csharp-sdk/basic-auth-optional/README.md +++ b/seed/csharp-sdk/basic-auth-pw-omitted/README.md @@ -1,7 +1,7 @@ # Seed C# Library [![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=Seed%2FC%23) -[![nuget shield](https://img.shields.io/nuget/v/Fernbasic-auth-optional)](https://nuget.org/packages/Fernbasic-auth-optional) +[![nuget shield](https://img.shields.io/nuget/v/Fernbasic-auth-pw-omitted)](https://nuget.org/packages/Fernbasic-auth-pw-omitted) The Seed C# library provides convenient access to the Seed APIs from C#. @@ -27,7 +27,7 @@ This SDK requires: ## Installation ```sh -dotnet add package Fernbasic-auth-optional +dotnet add package Fernbasic-auth-pw-omitted ``` ## Reference @@ -39,9 +39,9 @@ A full reference for this library is available [here](./reference.md). Instantiate and use the client with the following: ```csharp -using SeedBasicAuthOptional; +using SeedBasicAuthPwOmitted; -var client = new SeedBasicAuthOptionalClient("USERNAME", "PASSWORD"); +var client = new SeedBasicAuthPwOmittedClient("USERNAME", "PASSWORD"); await client.BasicAuth.PostWithBasicAuthAsync( new Dictionary() { { "key", "value" } } ); @@ -53,11 +53,11 @@ When the API returns a non-success status code (4xx or 5xx response), a subclass will be thrown. ```csharp -using SeedBasicAuthOptional; +using SeedBasicAuthPwOmitted; try { var response = await client.BasicAuth.PostWithBasicAuthAsync(...); -} catch (SeedBasicAuthOptionalApiException e) { +} catch (SeedBasicAuthPwOmittedApiException e) { System.Console.WriteLine(e.Body); System.Console.WriteLine(e.StatusCode); } @@ -106,7 +106,7 @@ var response = await client.BasicAuth.PostWithBasicAuthAsync( Access raw HTTP response data (status code, headers, URL) alongside parsed response data using the `.WithRawResponse()` method. ```csharp -using SeedBasicAuthOptional; +using SeedBasicAuthPwOmitted; // Access raw response data (status code, headers, etc.) alongside the parsed response var result = await client.BasicAuth.PostWithBasicAuthAsync(...).WithRawResponse(); diff --git a/seed/csharp-sdk/basic-auth-pw-omitted/SeedBasicAuthPwOmitted.slnx b/seed/csharp-sdk/basic-auth-pw-omitted/SeedBasicAuthPwOmitted.slnx new file mode 100644 index 000000000000..12d718a3b965 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-pw-omitted/SeedBasicAuthPwOmitted.slnx @@ -0,0 +1,4 @@ + + + + diff --git a/seed/csharp-sdk/basic-auth-optional/reference.md b/seed/csharp-sdk/basic-auth-pw-omitted/reference.md similarity index 76% rename from seed/csharp-sdk/basic-auth-optional/reference.md rename to seed/csharp-sdk/basic-auth-pw-omitted/reference.md index 3109a2c4b705..86a776673782 100644 --- a/seed/csharp-sdk/basic-auth-optional/reference.md +++ b/seed/csharp-sdk/basic-auth-pw-omitted/reference.md @@ -1,6 +1,6 @@ # Reference ## BasicAuth -
client.BasicAuth.GetWithBasicAuthAsync() -> WithRawResponseTask<bool> +
client.BasicAuth.GetWithBasicAuthAsync() -> WithRawResponseTask<bool>
@@ -39,7 +39,7 @@ await client.BasicAuth.GetWithBasicAuthAsync();
-
client.BasicAuth.PostWithBasicAuthAsync(object { ... }) -> WithRawResponseTask<bool> +
client.BasicAuth.PostWithBasicAuthAsync(object { ... }) -> WithRawResponseTask<bool>
diff --git a/seed/csharp-sdk/basic-auth-optional/snippet.json b/seed/csharp-sdk/basic-auth-pw-omitted/snippet.json similarity index 60% rename from seed/csharp-sdk/basic-auth-optional/snippet.json rename to seed/csharp-sdk/basic-auth-pw-omitted/snippet.json index 3c004d73f893..d34d7ff2de39 100644 --- a/seed/csharp-sdk/basic-auth-optional/snippet.json +++ b/seed/csharp-sdk/basic-auth-pw-omitted/snippet.json @@ -10,7 +10,7 @@ }, "snippet": { "type": "csharp", - "client": "using SeedBasicAuthOptional;\n\nvar client = new SeedBasicAuthOptionalClient(\"USERNAME\", \"PASSWORD\");\nawait client.BasicAuth.GetWithBasicAuthAsync();\n" + "client": "using SeedBasicAuthPwOmitted;\n\nvar client = new SeedBasicAuthPwOmittedClient(\"USERNAME\", \"PASSWORD\");\nawait client.BasicAuth.GetWithBasicAuthAsync();\n" } }, { @@ -22,7 +22,7 @@ }, "snippet": { "type": "csharp", - "client": "using SeedBasicAuthOptional;\n\nvar client = new SeedBasicAuthOptionalClient(\"USERNAME\", \"PASSWORD\");\nawait client.BasicAuth.PostWithBasicAuthAsync(\n new Dictionary() { { \"key\", \"value\" } }\n);\n" + "client": "using SeedBasicAuthPwOmitted;\n\nvar client = new SeedBasicAuthPwOmittedClient(\"USERNAME\", \"PASSWORD\");\nawait client.BasicAuth.PostWithBasicAuthAsync(\n new Dictionary() { { \"key\", \"value\" } }\n);\n" } } ] diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example0.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedApi.DynamicSnippets/Example0.cs similarity index 79% rename from seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example0.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedApi.DynamicSnippets/Example0.cs index 0a6d097846c7..0386a3aae32d 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example0.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedApi.DynamicSnippets/Example0.cs @@ -1,11 +1,11 @@ -using SeedBasicAuthOptional; +using SeedBasicAuthPwOmitted; namespace Usage; public class Example0 { public async Task Do() { - var client = new SeedBasicAuthOptionalClient( + var client = new SeedBasicAuthPwOmittedClient( username: "", password: "", clientOptions: new ClientOptions { diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example1.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedApi.DynamicSnippets/Example1.cs similarity index 79% rename from seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example1.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedApi.DynamicSnippets/Example1.cs index 2b220847e903..1350c4c8bf5f 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example1.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedApi.DynamicSnippets/Example1.cs @@ -1,11 +1,11 @@ -using SeedBasicAuthOptional; +using SeedBasicAuthPwOmitted; namespace Usage; public class Example1 { public async Task Do() { - var client = new SeedBasicAuthOptionalClient( + var client = new SeedBasicAuthPwOmittedClient( username: "", password: "", clientOptions: new ClientOptions { diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example2.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedApi.DynamicSnippets/Example2.cs similarity index 79% rename from seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example2.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedApi.DynamicSnippets/Example2.cs index de30b520c866..18153656c351 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example2.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedApi.DynamicSnippets/Example2.cs @@ -1,11 +1,11 @@ -using SeedBasicAuthOptional; +using SeedBasicAuthPwOmitted; namespace Usage; public class Example2 { public async Task Do() { - var client = new SeedBasicAuthOptionalClient( + var client = new SeedBasicAuthPwOmittedClient( username: "", password: "", clientOptions: new ClientOptions { diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example3.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedApi.DynamicSnippets/Example3.cs similarity index 84% rename from seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example3.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedApi.DynamicSnippets/Example3.cs index 1994ca07d935..f87705f66997 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example3.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedApi.DynamicSnippets/Example3.cs @@ -1,11 +1,11 @@ -using SeedBasicAuthOptional; +using SeedBasicAuthPwOmitted; namespace Usage; public class Example3 { public async Task Do() { - var client = new SeedBasicAuthOptionalClient( + var client = new SeedBasicAuthPwOmittedClient( username: "", password: "", clientOptions: new ClientOptions { diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example4.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedApi.DynamicSnippets/Example4.cs similarity index 84% rename from seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example4.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedApi.DynamicSnippets/Example4.cs index 2b5d846975d2..6830ffefd31d 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example4.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedApi.DynamicSnippets/Example4.cs @@ -1,11 +1,11 @@ -using SeedBasicAuthOptional; +using SeedBasicAuthPwOmitted; namespace Usage; public class Example4 { public async Task Do() { - var client = new SeedBasicAuthOptionalClient( + var client = new SeedBasicAuthPwOmittedClient( username: "", password: "", clientOptions: new ClientOptions { diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example5.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedApi.DynamicSnippets/Example5.cs similarity index 84% rename from seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example5.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedApi.DynamicSnippets/Example5.cs index fecaff239a15..e72d56e4a6ae 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example5.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedApi.DynamicSnippets/Example5.cs @@ -1,11 +1,11 @@ -using SeedBasicAuthOptional; +using SeedBasicAuthPwOmitted; namespace Usage; public class Example5 { public async Task Do() { - var client = new SeedBasicAuthOptionalClient( + var client = new SeedBasicAuthPwOmittedClient( username: "", password: "", clientOptions: new ClientOptions { diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example6.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedApi.DynamicSnippets/Example6.cs similarity index 84% rename from seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example6.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedApi.DynamicSnippets/Example6.cs index 52319b55bf66..575b1b09bc67 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/Example6.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedApi.DynamicSnippets/Example6.cs @@ -1,11 +1,11 @@ -using SeedBasicAuthOptional; +using SeedBasicAuthPwOmitted; namespace Usage; public class Example6 { public async Task Do() { - var client = new SeedBasicAuthOptionalClient( + var client = new SeedBasicAuthPwOmittedClient( username: "", password: "", clientOptions: new ClientOptions { diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/SeedApi.DynamicSnippets.csproj b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedApi.DynamicSnippets/SeedApi.DynamicSnippets.csproj similarity index 100% rename from seed/csharp-sdk/basic-auth-optional/src/SeedApi.DynamicSnippets/SeedApi.DynamicSnippets.csproj rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedApi.DynamicSnippets/SeedApi.DynamicSnippets.csproj diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/HeadersBuilderTests.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Core/HeadersBuilderTests.cs similarity index 99% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/HeadersBuilderTests.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Core/HeadersBuilderTests.cs index bc0e7759e1f1..10ec331c4196 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/HeadersBuilderTests.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Core/HeadersBuilderTests.cs @@ -1,7 +1,7 @@ using NUnit.Framework; -using SeedBasicAuthOptional.Core; +using SeedBasicAuthPwOmitted.Core; -namespace SeedBasicAuthOptional.Test.Core; +namespace SeedBasicAuthPwOmitted.Test.Core; [TestFixture] public class HeadersBuilderTests diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/Json/AdditionalPropertiesTests.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Core/Json/AdditionalPropertiesTests.cs similarity index 99% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/Json/AdditionalPropertiesTests.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Core/Json/AdditionalPropertiesTests.cs index fbc8b32bd84e..ac2e910f2bfd 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/Json/AdditionalPropertiesTests.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Core/Json/AdditionalPropertiesTests.cs @@ -1,9 +1,9 @@ using global::System.Text.Json; using global::System.Text.Json.Serialization; using NUnit.Framework; -using SeedBasicAuthOptional.Core; +using SeedBasicAuthPwOmitted.Core; -namespace SeedBasicAuthOptional.Test.Core.Json; +namespace SeedBasicAuthPwOmitted.Test.Core.Json; [TestFixture] public class AdditionalPropertiesTests diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/Json/DateOnlyJsonTests.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Core/Json/DateOnlyJsonTests.cs similarity index 97% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/Json/DateOnlyJsonTests.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Core/Json/DateOnlyJsonTests.cs index df03e0d70b29..e5bb333b9782 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/Json/DateOnlyJsonTests.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Core/Json/DateOnlyJsonTests.cs @@ -1,7 +1,7 @@ using NUnit.Framework; -using SeedBasicAuthOptional.Core; +using SeedBasicAuthPwOmitted.Core; -namespace SeedBasicAuthOptional.Test.Core.Json; +namespace SeedBasicAuthPwOmitted.Test.Core.Json; [TestFixture] public class DateOnlyJsonTests diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/Json/DateTimeJsonTests.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Core/Json/DateTimeJsonTests.cs similarity index 98% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/Json/DateTimeJsonTests.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Core/Json/DateTimeJsonTests.cs index 6807d966d800..11a18f440632 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/Json/DateTimeJsonTests.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Core/Json/DateTimeJsonTests.cs @@ -1,7 +1,7 @@ using NUnit.Framework; -using SeedBasicAuthOptional.Core; +using SeedBasicAuthPwOmitted.Core; -namespace SeedBasicAuthOptional.Test.Core.Json; +namespace SeedBasicAuthPwOmitted.Test.Core.Json; [TestFixture] public class DateTimeJsonTests diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Core/Json/JsonAccessAttributeTests.cs similarity index 98% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/Json/JsonAccessAttributeTests.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Core/Json/JsonAccessAttributeTests.cs index 3d965c92fe55..1a0d3ba22d14 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/Json/JsonAccessAttributeTests.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Core/Json/JsonAccessAttributeTests.cs @@ -1,8 +1,8 @@ using global::System.Text.Json.Serialization; using NUnit.Framework; -using SeedBasicAuthOptional.Core; +using SeedBasicAuthPwOmitted.Core; -namespace SeedBasicAuthOptional.Test.Core.Json; +namespace SeedBasicAuthPwOmitted.Test.Core.Json; [TestFixture] public class JsonAccessAttributeTests diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/QueryStringBuilderTests.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Core/QueryStringBuilderTests.cs similarity index 99% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/QueryStringBuilderTests.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Core/QueryStringBuilderTests.cs index 2a84be60aa64..cd08233246d4 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/QueryStringBuilderTests.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Core/QueryStringBuilderTests.cs @@ -1,7 +1,7 @@ using NUnit.Framework; -using SeedBasicAuthOptional.Core; +using SeedBasicAuthPwOmitted.Core; -namespace SeedBasicAuthOptional.Test.Core; +namespace SeedBasicAuthPwOmitted.Test.Core; [TestFixture] public class QueryStringBuilderTests diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/QueryStringConverterTests.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Core/QueryStringConverterTests.cs similarity index 98% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/QueryStringConverterTests.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Core/QueryStringConverterTests.cs index 9c137198ad52..437cdd40291a 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/QueryStringConverterTests.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Core/QueryStringConverterTests.cs @@ -1,7 +1,7 @@ using NUnit.Framework; -using SeedBasicAuthOptional.Core; +using SeedBasicAuthPwOmitted.Core; -namespace SeedBasicAuthOptional.Test.Core; +namespace SeedBasicAuthPwOmitted.Test.Core; [TestFixture] public class QueryStringConverterTests diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/RawClientTests/MultipartFormTests.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Core/RawClientTests/MultipartFormTests.cs similarity index 99% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/RawClientTests/MultipartFormTests.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Core/RawClientTests/MultipartFormTests.cs index e65385d0e14d..c880e4713819 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/RawClientTests/MultipartFormTests.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Core/RawClientTests/MultipartFormTests.cs @@ -2,10 +2,10 @@ using global::System.Text; using global::System.Text.Json.Serialization; using NUnit.Framework; -using SeedBasicAuthOptional.Core; +using SeedBasicAuthPwOmitted.Core; using SystemTask = global::System.Threading.Tasks.Task; -namespace SeedBasicAuthOptional.Test.Core.RawClientTests; +namespace SeedBasicAuthPwOmitted.Test.Core.RawClientTests; [TestFixture] [Parallelizable(ParallelScope.Self)] @@ -1056,9 +1056,9 @@ private static string GetBoundary(MultipartFormDataContent content) ?? throw new global::System.Exception("Boundary not found"); } - private static SeedBasicAuthOptional.Core.MultipartFormRequest CreateMultipartFormRequest() + private static SeedBasicAuthPwOmitted.Core.MultipartFormRequest CreateMultipartFormRequest() { - return new SeedBasicAuthOptional.Core.MultipartFormRequest + return new SeedBasicAuthPwOmitted.Core.MultipartFormRequest { BaseUrl = "https://localhost", Method = HttpMethod.Post, diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/RawClientTests/QueryParameterTests.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Core/RawClientTests/QueryParameterTests.cs similarity index 97% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/RawClientTests/QueryParameterTests.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Core/RawClientTests/QueryParameterTests.cs index 7362c5e111d5..61d14069aca9 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/RawClientTests/QueryParameterTests.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Core/RawClientTests/QueryParameterTests.cs @@ -1,7 +1,7 @@ using NUnit.Framework; -using SeedBasicAuthOptional.Core; +using SeedBasicAuthPwOmitted.Core; -namespace SeedBasicAuthOptional.Test.Core.RawClientTests; +namespace SeedBasicAuthPwOmitted.Test.Core.RawClientTests; [TestFixture] [Parallelizable(ParallelScope.Self)] diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Core/RawClientTests/RetriesTests.cs similarity index 97% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/RawClientTests/RetriesTests.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Core/RawClientTests/RetriesTests.cs index bd03b5d5b477..5b2e42e89441 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Core/RawClientTests/RetriesTests.cs @@ -1,13 +1,13 @@ using global::System.Net.Http; using global::System.Text.Json; using NUnit.Framework; -using SeedBasicAuthOptional.Core; +using SeedBasicAuthPwOmitted.Core; using WireMock.Server; using SystemTask = global::System.Threading.Tasks.Task; using WireMockRequest = WireMock.RequestBuilders.Request; using WireMockResponse = WireMock.ResponseBuilders.Response; -namespace SeedBasicAuthOptional.Test.Core.RawClientTests; +namespace SeedBasicAuthPwOmitted.Test.Core.RawClientTests; [TestFixture] [Parallelizable(ParallelScope.Self)] @@ -146,7 +146,7 @@ public async SystemTask SendRequestAsync_ShouldNotRetry_WithMultiPartFormRequest .WillSetStateTo("Server Error") .RespondWith(WireMockResponse.Create().WithStatusCode(429).WithBody("Failure")); - var request = new SeedBasicAuthOptional.Core.MultipartFormRequest + var request = new SeedBasicAuthPwOmitted.Core.MultipartFormRequest { BaseUrl = _baseUrl, Method = HttpMethod.Post, @@ -187,7 +187,7 @@ public async SystemTask SendRequestAsync_ShouldRetry_WithMultiPartFormRequest_Wi .WhenStateIs("Success") .RespondWith(WireMockResponse.Create().WithStatusCode(200).WithBody("Success")); - var request = new SeedBasicAuthOptional.Core.MultipartFormRequest + var request = new SeedBasicAuthPwOmitted.Core.MultipartFormRequest { BaseUrl = _baseUrl, Method = HttpMethod.Post, @@ -373,7 +373,7 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() .WhenStateIs("Success") .RespondWith(WireMockResponse.Create().WithStatusCode(200).WithBody("Success")); - var request = new SeedBasicAuthOptional.Core.MultipartFormRequest + var request = new SeedBasicAuthPwOmitted.Core.MultipartFormRequest { BaseUrl = _baseUrl, Method = HttpMethod.Post, diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/WithRawResponseTests.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Core/WithRawResponseTests.cs similarity index 98% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/WithRawResponseTests.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Core/WithRawResponseTests.cs index 07deaaab2932..5478a7d48fca 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Core/WithRawResponseTests.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Core/WithRawResponseTests.cs @@ -1,10 +1,10 @@ using global::System.Net; using global::System.Net.Http.Headers; using NUnit.Framework; -using SeedBasicAuthOptional; -using SeedBasicAuthOptional.Core; +using SeedBasicAuthPwOmitted; +using SeedBasicAuthPwOmitted.Core; -namespace SeedBasicAuthOptional.Test.Core; +namespace SeedBasicAuthPwOmitted.Test.Core; [TestFixture] public class WithRawResponseTests diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/SeedBasicAuthOptional.Test.Custom.props b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/SeedBasicAuthPwOmitted.Test.Custom.props similarity index 100% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/SeedBasicAuthOptional.Test.Custom.props rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/SeedBasicAuthPwOmitted.Test.Custom.props diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/SeedBasicAuthOptional.Test.csproj b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/SeedBasicAuthPwOmitted.Test.csproj similarity index 87% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/SeedBasicAuthOptional.Test.csproj rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/SeedBasicAuthPwOmitted.Test.csproj index 2ffd45f0bd14..611c765081de 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/SeedBasicAuthOptional.Test.csproj +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/SeedBasicAuthPwOmitted.Test.csproj @@ -29,11 +29,11 @@ - + diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/TestClient.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/TestClient.cs similarity index 62% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/TestClient.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/TestClient.cs index 378fc6e838e2..6b1e168cac41 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/TestClient.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/TestClient.cs @@ -1,6 +1,6 @@ using NUnit.Framework; -namespace SeedBasicAuthOptional.Test; +namespace SeedBasicAuthPwOmitted.Test; [TestFixture] public class TestClient; diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Unit/MockServer/BaseMockServerTest.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Unit/MockServer/BaseMockServerTest.cs similarity index 79% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Unit/MockServer/BaseMockServerTest.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Unit/MockServer/BaseMockServerTest.cs index c52d8138a3d3..5e326bfd0150 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Unit/MockServer/BaseMockServerTest.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Unit/MockServer/BaseMockServerTest.cs @@ -1,16 +1,16 @@ using NUnit.Framework; -using SeedBasicAuthOptional; +using SeedBasicAuthPwOmitted; using WireMock.Logging; using WireMock.Server; using WireMock.Settings; -namespace SeedBasicAuthOptional.Test.Unit.MockServer; +namespace SeedBasicAuthPwOmitted.Test.Unit.MockServer; public class BaseMockServerTest { protected WireMockServer Server { get; set; } = null!; - protected SeedBasicAuthOptionalClient Client { get; set; } = null!; + protected SeedBasicAuthPwOmittedClient Client { get; set; } = null!; protected RequestOptions RequestOptions { get; set; } = new(); @@ -23,7 +23,7 @@ public void GlobalSetup() ); // Initialize the Client - Client = new SeedBasicAuthOptionalClient( + Client = new SeedBasicAuthPwOmittedClient( "USERNAME", "PASSWORD", clientOptions: new ClientOptions { BaseUrl = Server.Urls[0], MaxRetries = 0 } diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Unit/MockServer/BasicAuth/GetWithBasicAuthTest.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Unit/MockServer/BasicAuth/GetWithBasicAuthTest.cs similarity index 89% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Unit/MockServer/BasicAuth/GetWithBasicAuthTest.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Unit/MockServer/BasicAuth/GetWithBasicAuthTest.cs index 15a25ad49511..cdf3435dcb52 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Unit/MockServer/BasicAuth/GetWithBasicAuthTest.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Unit/MockServer/BasicAuth/GetWithBasicAuthTest.cs @@ -1,8 +1,8 @@ using NUnit.Framework; -using SeedBasicAuthOptional.Test.Unit.MockServer; -using SeedBasicAuthOptional.Test.Utils; +using SeedBasicAuthPwOmitted.Test.Unit.MockServer; +using SeedBasicAuthPwOmitted.Test.Utils; -namespace SeedBasicAuthOptional.Test.Unit.MockServer.BasicAuth; +namespace SeedBasicAuthPwOmitted.Test.Unit.MockServer.BasicAuth; [TestFixture] [Parallelizable(ParallelScope.Self)] diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Unit/MockServer/BasicAuth/PostWithBasicAuthTest.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Unit/MockServer/BasicAuth/PostWithBasicAuthTest.cs similarity index 92% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Unit/MockServer/BasicAuth/PostWithBasicAuthTest.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Unit/MockServer/BasicAuth/PostWithBasicAuthTest.cs index 0eb28eb4a585..76b2b3112bf9 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Unit/MockServer/BasicAuth/PostWithBasicAuthTest.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Unit/MockServer/BasicAuth/PostWithBasicAuthTest.cs @@ -1,8 +1,8 @@ using NUnit.Framework; -using SeedBasicAuthOptional.Test.Unit.MockServer; -using SeedBasicAuthOptional.Test.Utils; +using SeedBasicAuthPwOmitted.Test.Unit.MockServer; +using SeedBasicAuthPwOmitted.Test.Utils; -namespace SeedBasicAuthOptional.Test.Unit.MockServer.BasicAuth; +namespace SeedBasicAuthPwOmitted.Test.Unit.MockServer.BasicAuth; [TestFixture] [Parallelizable(ParallelScope.Self)] diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/AdditionalPropertiesComparer.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Utils/AdditionalPropertiesComparer.cs similarity index 99% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/AdditionalPropertiesComparer.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Utils/AdditionalPropertiesComparer.cs index 1c5f0cf63e64..bad8b90124f3 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/AdditionalPropertiesComparer.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Utils/AdditionalPropertiesComparer.cs @@ -1,7 +1,7 @@ using global::System.Text.Json; using NUnit.Framework.Constraints; -using SeedBasicAuthOptional; -using SeedBasicAuthOptional.Core; +using SeedBasicAuthPwOmitted; +using SeedBasicAuthPwOmitted.Core; namespace NUnit.Framework; diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/JsonAssert.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Utils/JsonAssert.cs similarity index 92% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/JsonAssert.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Utils/JsonAssert.cs index cccd122c8ed1..fe198d27b671 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/JsonAssert.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Utils/JsonAssert.cs @@ -1,8 +1,8 @@ using global::System.Text.Json; using NUnit.Framework; -using SeedBasicAuthOptional.Core; +using SeedBasicAuthPwOmitted.Core; -namespace SeedBasicAuthOptional.Test.Utils; +namespace SeedBasicAuthPwOmitted.Test.Utils; internal static class JsonAssert { diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/JsonElementComparer.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Utils/JsonElementComparer.cs similarity index 100% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/JsonElementComparer.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Utils/JsonElementComparer.cs diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/NUnitExtensions.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Utils/NUnitExtensions.cs similarity index 100% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/NUnitExtensions.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Utils/NUnitExtensions.cs diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/OneOfComparer.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Utils/OneOfComparer.cs similarity index 100% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/OneOfComparer.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Utils/OneOfComparer.cs diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/OptionalComparer.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Utils/OptionalComparer.cs similarity index 99% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/OptionalComparer.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Utils/OptionalComparer.cs index 1cac67cf25d2..740e356b0246 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/OptionalComparer.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Utils/OptionalComparer.cs @@ -1,6 +1,6 @@ using NUnit.Framework.Constraints; using OneOf; -using SeedBasicAuthOptional.Core; +using SeedBasicAuthPwOmitted.Core; namespace NUnit.Framework; diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/ReadOnlyMemoryComparer.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Utils/ReadOnlyMemoryComparer.cs similarity index 100% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional.Test/Utils/ReadOnlyMemoryComparer.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional.Test/Utils/ReadOnlyMemoryComparer.cs diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/BasicAuth/BasicAuthClient.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/BasicAuth/BasicAuthClient.cs similarity index 93% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/BasicAuth/BasicAuthClient.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/BasicAuth/BasicAuthClient.cs index d78685d4ec41..458fa59bb599 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/BasicAuth/BasicAuthClient.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/BasicAuth/BasicAuthClient.cs @@ -1,7 +1,7 @@ using global::System.Text.Json; -using SeedBasicAuthOptional.Core; +using SeedBasicAuthPwOmitted.Core; -namespace SeedBasicAuthOptional; +namespace SeedBasicAuthPwOmitted; public partial class BasicAuthClient : IBasicAuthClient { @@ -17,7 +17,7 @@ private async Task> GetWithBasicAuthAsyncCore( CancellationToken cancellationToken = default ) { - var _headers = await new SeedBasicAuthOptional.Core.HeadersBuilder.Builder() + var _headers = await new SeedBasicAuthPwOmitted.Core.HeadersBuilder.Builder() .Add(_client.Options.Headers) .Add(_client.Options.AdditionalHeaders) .Add(options?.AdditionalHeaders) @@ -56,7 +56,7 @@ private async Task> GetWithBasicAuthAsyncCore( } catch (JsonException e) { - throw new SeedBasicAuthOptionalApiException( + throw new SeedBasicAuthPwOmittedApiException( "Failed to deserialize response", response.StatusCode, responseBody, @@ -82,7 +82,7 @@ private async Task> GetWithBasicAuthAsyncCore( { // unable to map error response, throwing generic error } - throw new SeedBasicAuthOptionalApiException( + throw new SeedBasicAuthPwOmittedApiException( $"Error with status code {response.StatusCode}", response.StatusCode, responseBody @@ -96,7 +96,7 @@ private async Task> PostWithBasicAuthAsyncCore( CancellationToken cancellationToken = default ) { - var _headers = await new SeedBasicAuthOptional.Core.HeadersBuilder.Builder() + var _headers = await new SeedBasicAuthPwOmitted.Core.HeadersBuilder.Builder() .Add(_client.Options.Headers) .Add(_client.Options.AdditionalHeaders) .Add(options?.AdditionalHeaders) @@ -136,7 +136,7 @@ private async Task> PostWithBasicAuthAsyncCore( } catch (JsonException e) { - throw new SeedBasicAuthOptionalApiException( + throw new SeedBasicAuthPwOmittedApiException( "Failed to deserialize response", response.StatusCode, responseBody, @@ -164,7 +164,7 @@ private async Task> PostWithBasicAuthAsyncCore( { // unable to map error response, throwing generic error } - throw new SeedBasicAuthOptionalApiException( + throw new SeedBasicAuthPwOmittedApiException( $"Error with status code {response.StatusCode}", response.StatusCode, responseBody diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/BasicAuth/IBasicAuthClient.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/BasicAuth/IBasicAuthClient.cs similarity index 94% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/BasicAuth/IBasicAuthClient.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/BasicAuth/IBasicAuthClient.cs index aca0fbc2578b..3c2bd48c4181 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/BasicAuth/IBasicAuthClient.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/BasicAuth/IBasicAuthClient.cs @@ -1,4 +1,4 @@ -namespace SeedBasicAuthOptional; +namespace SeedBasicAuthPwOmitted; public partial interface IBasicAuthClient { diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/ApiResponse.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/ApiResponse.cs similarity index 86% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/ApiResponse.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/ApiResponse.cs index f033f46040d8..401d24be80b7 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/ApiResponse.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/ApiResponse.cs @@ -1,6 +1,6 @@ using global::System.Net.Http; -namespace SeedBasicAuthOptional.Core; +namespace SeedBasicAuthPwOmitted.Core; /// /// The response object returned from the API. diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/BaseRequest.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/BaseRequest.cs similarity index 97% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/BaseRequest.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/BaseRequest.cs index ad45d88fcc0e..5d2245a89fef 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/BaseRequest.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/BaseRequest.cs @@ -2,7 +2,7 @@ using global::System.Net.Http.Headers; using global::System.Text; -namespace SeedBasicAuthOptional.Core; +namespace SeedBasicAuthPwOmitted.Core; internal abstract record BaseRequest { diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/CollectionItemSerializer.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/CollectionItemSerializer.cs similarity index 98% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/CollectionItemSerializer.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/CollectionItemSerializer.cs index 730f1e27b265..278898c726a3 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/CollectionItemSerializer.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/CollectionItemSerializer.cs @@ -1,7 +1,7 @@ using global::System.Text.Json; using global::System.Text.Json.Serialization; -namespace SeedBasicAuthOptional.Core; +namespace SeedBasicAuthPwOmitted.Core; /// /// Json collection converter. diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Constants.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Constants.cs similarity index 81% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Constants.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Constants.cs index b7f86c54a04c..a0f880defe14 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Constants.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Constants.cs @@ -1,4 +1,4 @@ -namespace SeedBasicAuthOptional.Core; +namespace SeedBasicAuthPwOmitted.Core; internal static class Constants { diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/DateOnlyConverter.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/DateOnlyConverter.cs similarity index 99% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/DateOnlyConverter.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/DateOnlyConverter.cs index cb4e399cca83..66fa79284324 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/DateOnlyConverter.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/DateOnlyConverter.cs @@ -15,7 +15,7 @@ // ReSharper disable SuggestVarOrType_SimpleTypes // ReSharper disable SuggestVarOrType_BuiltInTypes -namespace SeedBasicAuthOptional.Core +namespace SeedBasicAuthPwOmitted.Core { /// /// Custom converter for handling the data type with the System.Text.Json library. diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/DateTimeSerializer.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/DateTimeSerializer.cs similarity index 96% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/DateTimeSerializer.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/DateTimeSerializer.cs index 27b62074d137..3313d5d686dd 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/DateTimeSerializer.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/DateTimeSerializer.cs @@ -2,7 +2,7 @@ using global::System.Text.Json; using global::System.Text.Json.Serialization; -namespace SeedBasicAuthOptional.Core; +namespace SeedBasicAuthPwOmitted.Core; internal class DateTimeSerializer : JsonConverter { diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/EmptyRequest.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/EmptyRequest.cs similarity index 85% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/EmptyRequest.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/EmptyRequest.cs index 384327b783ab..af5b70cca86a 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/EmptyRequest.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/EmptyRequest.cs @@ -1,6 +1,6 @@ using global::System.Net.Http; -namespace SeedBasicAuthOptional.Core; +namespace SeedBasicAuthPwOmitted.Core; /// /// The request object to send without a request body. diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/EncodingCache.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/EncodingCache.cs similarity index 85% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/EncodingCache.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/EncodingCache.cs index ae6139d66855..9172f318e869 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/EncodingCache.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/EncodingCache.cs @@ -1,6 +1,6 @@ using global::System.Text; -namespace SeedBasicAuthOptional.Core; +namespace SeedBasicAuthPwOmitted.Core; internal static class EncodingCache { diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Extensions.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Extensions.cs similarity index 98% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Extensions.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Extensions.cs index 11f9d385c5ef..8361264a88b1 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Extensions.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Extensions.cs @@ -1,7 +1,7 @@ using global::System.Diagnostics.CodeAnalysis; using global::System.Runtime.Serialization; -namespace SeedBasicAuthOptional.Core; +namespace SeedBasicAuthPwOmitted.Core; internal static class Extensions { diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/FormUrlEncoder.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/FormUrlEncoder.cs similarity index 97% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/FormUrlEncoder.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/FormUrlEncoder.cs index 954003fc2dc1..649d931e53d9 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/FormUrlEncoder.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/FormUrlEncoder.cs @@ -1,6 +1,6 @@ using global::System.Net.Http; -namespace SeedBasicAuthOptional.Core; +namespace SeedBasicAuthPwOmitted.Core; /// /// Encodes an object into a form URL-encoded content. diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/HeaderValue.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/HeaderValue.cs similarity index 97% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/HeaderValue.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/HeaderValue.cs index 35d9400e7864..0dae4aa97d82 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/HeaderValue.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/HeaderValue.cs @@ -1,4 +1,4 @@ -namespace SeedBasicAuthOptional.Core; +namespace SeedBasicAuthPwOmitted.Core; internal sealed class HeaderValue { diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Headers.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Headers.cs similarity index 95% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Headers.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Headers.cs index 878c9821ffc7..73a892a31e02 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Headers.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Headers.cs @@ -1,4 +1,4 @@ -namespace SeedBasicAuthOptional.Core; +namespace SeedBasicAuthPwOmitted.Core; /// /// Represents the headers sent with the request. diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/HeadersBuilder.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/HeadersBuilder.cs similarity index 99% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/HeadersBuilder.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/HeadersBuilder.cs index b4e0dbee737e..82432ee9a0c3 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/HeadersBuilder.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/HeadersBuilder.cs @@ -1,4 +1,4 @@ -namespace SeedBasicAuthOptional.Core; +namespace SeedBasicAuthPwOmitted.Core; /// /// Fluent builder for constructing HTTP headers with support for merging from multiple sources. diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/HttpContentExtensions.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/HttpContentExtensions.cs similarity index 93% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/HttpContentExtensions.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/HttpContentExtensions.cs index 65f5ce6554f6..690bc54dc7d0 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/HttpContentExtensions.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/HttpContentExtensions.cs @@ -1,5 +1,5 @@ #if !NET5_0_OR_GREATER -namespace SeedBasicAuthOptional.Core; +namespace SeedBasicAuthPwOmitted.Core; /// /// Polyfill extension providing a ReadAsStringAsync(CancellationToken) overload diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/HttpMethodExtensions.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/HttpMethodExtensions.cs similarity index 78% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/HttpMethodExtensions.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/HttpMethodExtensions.cs index ad77851023d7..45dadd2a8e34 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/HttpMethodExtensions.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/HttpMethodExtensions.cs @@ -1,6 +1,6 @@ using global::System.Net.Http; -namespace SeedBasicAuthOptional.Core; +namespace SeedBasicAuthPwOmitted.Core; internal static class HttpMethodExtensions { diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/IIsRetryableContent.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/IIsRetryableContent.cs similarity index 66% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/IIsRetryableContent.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/IIsRetryableContent.cs index e64c9b5a5c89..e078be02f2be 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/IIsRetryableContent.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/IIsRetryableContent.cs @@ -1,4 +1,4 @@ -namespace SeedBasicAuthOptional.Core; +namespace SeedBasicAuthPwOmitted.Core; public interface IIsRetryableContent { diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/IRequestOptions.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/IRequestOptions.cs similarity index 97% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/IRequestOptions.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/IRequestOptions.cs index 0386a81482b4..22969e9d2200 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/IRequestOptions.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/IRequestOptions.cs @@ -1,4 +1,4 @@ -namespace SeedBasicAuthOptional.Core; +namespace SeedBasicAuthPwOmitted.Core; internal interface IRequestOptions { diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/JsonAccessAttribute.cs similarity index 89% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/JsonAccessAttribute.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/JsonAccessAttribute.cs index bef7f86c5d27..75390a6e0b17 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/JsonAccessAttribute.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/JsonAccessAttribute.cs @@ -1,4 +1,4 @@ -namespace SeedBasicAuthOptional.Core; +namespace SeedBasicAuthPwOmitted.Core; [global::System.AttributeUsage( global::System.AttributeTargets.Property | global::System.AttributeTargets.Field diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/JsonConfiguration.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/JsonConfiguration.cs similarity index 99% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/JsonConfiguration.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/JsonConfiguration.cs index c984380d74b2..bd877db21c96 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/JsonConfiguration.cs @@ -5,7 +5,7 @@ using global::System.Text.Json.Serialization; using global::System.Text.Json.Serialization.Metadata; -namespace SeedBasicAuthOptional.Core; +namespace SeedBasicAuthPwOmitted.Core; internal static partial class JsonOptions { diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/JsonRequest.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/JsonRequest.cs similarity index 95% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/JsonRequest.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/JsonRequest.cs index 900a41b8e024..ca236502d112 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/JsonRequest.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/JsonRequest.cs @@ -1,6 +1,6 @@ using global::System.Net.Http; -namespace SeedBasicAuthOptional.Core; +namespace SeedBasicAuthPwOmitted.Core; /// /// The request object to be sent for JSON APIs. diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/MultipartFormRequest.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/MultipartFormRequest.cs similarity index 99% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/MultipartFormRequest.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/MultipartFormRequest.cs index 3f61e753b056..82f46dc878dc 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/MultipartFormRequest.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/MultipartFormRequest.cs @@ -1,7 +1,7 @@ using global::System.Net.Http; using global::System.Net.Http.Headers; -namespace SeedBasicAuthOptional.Core; +namespace SeedBasicAuthPwOmitted.Core; /// /// The request object to be sent for multipart form data. diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/NullableAttribute.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/NullableAttribute.cs similarity index 95% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/NullableAttribute.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/NullableAttribute.cs index c29115cb9074..9355ac14b0df 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/NullableAttribute.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/NullableAttribute.cs @@ -1,4 +1,4 @@ -namespace SeedBasicAuthOptional.Core; +namespace SeedBasicAuthPwOmitted.Core; /// /// Marks a property as nullable in the OpenAPI specification. diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/OneOfSerializer.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/OneOfSerializer.cs similarity index 99% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/OneOfSerializer.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/OneOfSerializer.cs index 9aacfd4e5fa4..3c7bdde4ec0d 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/OneOfSerializer.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/OneOfSerializer.cs @@ -3,7 +3,7 @@ using global::System.Text.Json.Serialization; using OneOf; -namespace SeedBasicAuthOptional.Core; +namespace SeedBasicAuthPwOmitted.Core; internal class OneOfSerializer : JsonConverter { diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Optional.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Optional.cs similarity index 99% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Optional.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Optional.cs index 0dac756de991..f165b4166582 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Optional.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Optional.cs @@ -1,7 +1,7 @@ using global::System.Text.Json; using global::System.Text.Json.Serialization; -namespace SeedBasicAuthOptional.Core; +namespace SeedBasicAuthPwOmitted.Core; /// /// Non-generic interface for Optional types to enable reflection-free checks. diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/OptionalAttribute.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/OptionalAttribute.cs similarity index 95% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/OptionalAttribute.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/OptionalAttribute.cs index 2867248f37dc..e5184234617c 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/OptionalAttribute.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/OptionalAttribute.cs @@ -1,4 +1,4 @@ -namespace SeedBasicAuthOptional.Core; +namespace SeedBasicAuthPwOmitted.Core; /// /// Marks a property as optional in the OpenAPI specification. diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/AdditionalProperties.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Public/AdditionalProperties.cs similarity index 99% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/AdditionalProperties.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Public/AdditionalProperties.cs index cfa2218695c6..fce5fac9f5d9 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/AdditionalProperties.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Public/AdditionalProperties.cs @@ -2,9 +2,9 @@ using global::System.Collections.ObjectModel; using global::System.Text.Json; using global::System.Text.Json.Nodes; -using SeedBasicAuthOptional.Core; +using SeedBasicAuthPwOmitted.Core; -namespace SeedBasicAuthOptional; +namespace SeedBasicAuthPwOmitted; public record ReadOnlyAdditionalProperties : ReadOnlyAdditionalProperties { diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/ClientOptions.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Public/ClientOptions.cs similarity index 96% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/ClientOptions.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Public/ClientOptions.cs index 095b5e14bc8c..696462b1a8d2 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/ClientOptions.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Public/ClientOptions.cs @@ -1,6 +1,6 @@ -using SeedBasicAuthOptional.Core; +using SeedBasicAuthPwOmitted.Core; -namespace SeedBasicAuthOptional; +namespace SeedBasicAuthPwOmitted; [Serializable] public partial class ClientOptions diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/FileParameter.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Public/FileParameter.cs similarity index 97% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/FileParameter.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Public/FileParameter.cs index 59afdfed2d7d..2fcf5dd348cc 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/FileParameter.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Public/FileParameter.cs @@ -1,4 +1,4 @@ -namespace SeedBasicAuthOptional; +namespace SeedBasicAuthPwOmitted; /// /// File parameter for uploading files. diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/RawResponse.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Public/RawResponse.cs similarity index 94% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/RawResponse.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Public/RawResponse.cs index 0bfb9f0a87e1..ae3fa8f7ad57 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/RawResponse.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Public/RawResponse.cs @@ -1,6 +1,6 @@ using global::System.Net; -namespace SeedBasicAuthOptional; +namespace SeedBasicAuthPwOmitted; /// /// Contains HTTP response metadata including status code, URL, and headers. diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/RequestOptions.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Public/RequestOptions.cs similarity index 96% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/RequestOptions.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Public/RequestOptions.cs index b31eb653991f..8e77b56b77f1 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/RequestOptions.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Public/RequestOptions.cs @@ -1,6 +1,6 @@ -using SeedBasicAuthOptional.Core; +using SeedBasicAuthPwOmitted.Core; -namespace SeedBasicAuthOptional; +namespace SeedBasicAuthPwOmitted; [Serializable] public partial class RequestOptions : IRequestOptions diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/SeedBasicAuthOptionalApiException.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Public/SeedBasicAuthPwOmittedApiException.cs similarity index 76% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/SeedBasicAuthOptionalApiException.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Public/SeedBasicAuthPwOmittedApiException.cs index 776d063187d7..f9af083d7577 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/SeedBasicAuthOptionalApiException.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Public/SeedBasicAuthPwOmittedApiException.cs @@ -1,14 +1,14 @@ -namespace SeedBasicAuthOptional; +namespace SeedBasicAuthPwOmitted; /// /// This exception type will be thrown for any non-2XX API responses. /// -public class SeedBasicAuthOptionalApiException( +public class SeedBasicAuthPwOmittedApiException( string message, int statusCode, object body, Exception? innerException = null -) : SeedBasicAuthOptionalException(message, innerException) +) : SeedBasicAuthPwOmittedException(message, innerException) { /// /// The error code of the response that triggered the exception. diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/SeedBasicAuthOptionalException.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Public/SeedBasicAuthPwOmittedException.cs similarity index 51% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/SeedBasicAuthOptionalException.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Public/SeedBasicAuthPwOmittedException.cs index bf72810d1158..ca7ef9aee927 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/SeedBasicAuthOptionalException.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Public/SeedBasicAuthPwOmittedException.cs @@ -1,7 +1,7 @@ -namespace SeedBasicAuthOptional; +namespace SeedBasicAuthPwOmitted; /// /// Base exception class for all exceptions thrown by the SDK. /// -public class SeedBasicAuthOptionalException(string message, Exception? innerException = null) +public class SeedBasicAuthPwOmittedException(string message, Exception? innerException = null) : Exception(message, innerException); diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/Version.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Public/Version.cs similarity index 71% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/Version.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Public/Version.cs index a15c59ea7031..1788b02355c9 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/Version.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Public/Version.cs @@ -1,4 +1,4 @@ -namespace SeedBasicAuthOptional; +namespace SeedBasicAuthPwOmitted; [Serializable] internal class Version diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/WithRawResponse.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Public/WithRawResponse.cs similarity index 93% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/WithRawResponse.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Public/WithRawResponse.cs index f50dbfdb493b..dc3b1d1fc464 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/WithRawResponse.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Public/WithRawResponse.cs @@ -1,4 +1,4 @@ -namespace SeedBasicAuthOptional; +namespace SeedBasicAuthPwOmitted; /// /// Wraps a parsed response value with its raw HTTP response metadata. diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/WithRawResponseTask.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Public/WithRawResponseTask.cs similarity index 99% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/WithRawResponseTask.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Public/WithRawResponseTask.cs index ce2975aac51c..6902fb36feba 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/Public/WithRawResponseTask.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/Public/WithRawResponseTask.cs @@ -1,6 +1,6 @@ using global::System.Runtime.CompilerServices; -namespace SeedBasicAuthOptional; +namespace SeedBasicAuthPwOmitted; /// /// A task-like type that wraps Task<WithRawResponse<T>> and provides dual-mode awaiting: diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/QueryStringBuilder.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/QueryStringBuilder.cs similarity index 99% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/QueryStringBuilder.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/QueryStringBuilder.cs index b2eaac863b68..0f1364203a7a 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/QueryStringBuilder.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/QueryStringBuilder.cs @@ -4,7 +4,7 @@ using global::System.Text; #endif -namespace SeedBasicAuthOptional.Core; +namespace SeedBasicAuthPwOmitted.Core; /// /// High-performance query string builder with cross-platform optimizations. diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/QueryStringConverter.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/QueryStringConverter.cs similarity index 99% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/QueryStringConverter.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/QueryStringConverter.cs index 3f9a49404a2c..86710cc989ed 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/QueryStringConverter.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/QueryStringConverter.cs @@ -1,6 +1,6 @@ using global::System.Text.Json; -namespace SeedBasicAuthOptional.Core; +namespace SeedBasicAuthPwOmitted.Core; /// /// Converts an object into a query string collection. diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/RawClient.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/RawClient.cs similarity index 95% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/RawClient.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/RawClient.cs index c0c46ffc8168..6fb38499dad2 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/RawClient.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/RawClient.cs @@ -3,7 +3,7 @@ using global::System.Text; using SystemTask = global::System.Threading.Tasks.Task; -namespace SeedBasicAuthOptional.Core; +namespace SeedBasicAuthPwOmitted.Core; /// /// Utility class for making raw HTTP requests to the API. @@ -25,8 +25,8 @@ internal partial class RawClient(ClientOptions clientOptions) /// internal readonly ClientOptions Options = clientOptions; - internal async global::System.Threading.Tasks.Task SendRequestAsync( - global::SeedBasicAuthOptional.Core.BaseRequest request, + internal async global::System.Threading.Tasks.Task SendRequestAsync( + global::SeedBasicAuthPwOmitted.Core.BaseRequest request, CancellationToken cancellationToken = default ) { @@ -41,7 +41,7 @@ internal partial class RawClient(ClientOptions clientOptions) .ConfigureAwait(false); } - internal async global::System.Threading.Tasks.Task SendRequestAsync( + internal async global::System.Threading.Tasks.Task SendRequestAsync( HttpRequestMessage request, IRequestOptions? options, CancellationToken cancellationToken = default @@ -123,7 +123,7 @@ HttpRequestMessage request /// Sends the request with retries, unless the request content is not retryable, /// such as stream requests and multipart form data with stream content. /// - private async global::System.Threading.Tasks.Task SendWithRetriesAsync( + private async global::System.Threading.Tasks.Task SendWithRetriesAsync( HttpRequestMessage request, IRequestOptions? options, CancellationToken cancellationToken @@ -138,7 +138,7 @@ CancellationToken cancellationToken if (!isRetryableContent) { - return new global::SeedBasicAuthOptional.Core.ApiResponse + return new global::SeedBasicAuthPwOmitted.Core.ApiResponse { StatusCode = (int)response.StatusCode, Raw = response, @@ -164,7 +164,7 @@ CancellationToken cancellationToken .ConfigureAwait(false); } - return new global::SeedBasicAuthOptional.Core.ApiResponse + return new global::SeedBasicAuthPwOmitted.Core.ApiResponse { StatusCode = (int)response.StatusCode, Raw = response, @@ -263,7 +263,7 @@ private static bool IsRetryableContent(HttpRequestMessage request) } internal async global::System.Threading.Tasks.Task CreateHttpRequestAsync( - global::SeedBasicAuthOptional.Core.BaseRequest request + global::SeedBasicAuthPwOmitted.Core.BaseRequest request ) { var url = BuildUrl(request); @@ -274,7 +274,7 @@ private static bool IsRetryableContent(HttpRequestMessage request) return httpRequest; } - private string BuildUrl(global::SeedBasicAuthOptional.Core.BaseRequest request) + private string BuildUrl(global::SeedBasicAuthPwOmitted.Core.BaseRequest request) { var baseUrl = request.Options?.BaseUrl ?? request.BaseUrl ?? Options.BaseUrl; diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/RawResponse.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/RawResponse.cs similarity index 93% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/RawResponse.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/RawResponse.cs index 86c0a315bdf0..077d13633b63 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/RawResponse.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/RawResponse.cs @@ -1,6 +1,6 @@ using global::System.Net; -namespace SeedBasicAuthOptional.Core; +namespace SeedBasicAuthPwOmitted.Core; /// /// Contains HTTP response metadata including status code, URL, and headers. diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/ResponseHeaders.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/ResponseHeaders.cs similarity index 98% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/ResponseHeaders.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/ResponseHeaders.cs index e2c7c2b77595..dd31d0998143 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/ResponseHeaders.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/ResponseHeaders.cs @@ -1,7 +1,7 @@ using global::System.Collections; using global::System.Net.Http.Headers; -namespace SeedBasicAuthOptional.Core; +namespace SeedBasicAuthPwOmitted.Core; /// /// Represents HTTP response headers with case-insensitive lookup. diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/StreamRequest.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/StreamRequest.cs similarity index 94% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/StreamRequest.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/StreamRequest.cs index 6d98922df29b..e797e911ce8d 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/StreamRequest.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/StreamRequest.cs @@ -1,7 +1,7 @@ using global::System.Net.Http; using global::System.Net.Http.Headers; -namespace SeedBasicAuthOptional.Core; +namespace SeedBasicAuthPwOmitted.Core; /// /// The request object to be sent for streaming uploads. diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/StringEnum.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/StringEnum.cs similarity index 69% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/StringEnum.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/StringEnum.cs index 3d3a3a39d207..2209b84b0609 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/StringEnum.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/StringEnum.cs @@ -1,4 +1,4 @@ -namespace SeedBasicAuthOptional.Core; +namespace SeedBasicAuthPwOmitted.Core; public interface IStringEnum : IEquatable { diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/StringEnumExtensions.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/StringEnumExtensions.cs similarity index 77% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/StringEnumExtensions.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/StringEnumExtensions.cs index ec99d9954684..c5461cddd104 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/StringEnumExtensions.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/StringEnumExtensions.cs @@ -1,4 +1,4 @@ -namespace SeedBasicAuthOptional.Core; +namespace SeedBasicAuthPwOmitted.Core; internal static class StringEnumExtensions { diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/ValueConvert.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/ValueConvert.cs similarity index 99% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/ValueConvert.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/ValueConvert.cs index 0507e04f0e08..9a58da3f3533 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Core/ValueConvert.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Core/ValueConvert.cs @@ -1,6 +1,6 @@ using global::System.Globalization; -namespace SeedBasicAuthOptional.Core; +namespace SeedBasicAuthPwOmitted.Core; /// /// Convert values to string for path and query parameters. diff --git a/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Errors/Exceptions/BadRequest.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Errors/Exceptions/BadRequest.cs new file mode 100644 index 000000000000..0d283125f8a6 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Errors/Exceptions/BadRequest.cs @@ -0,0 +1,7 @@ +namespace SeedBasicAuthPwOmitted; + +/// +/// This exception type will be thrown for any non-2XX API responses. +/// +[Serializable] +public class BadRequest(object body) : SeedBasicAuthPwOmittedApiException("BadRequest", 400, body); diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Errors/Exceptions/UnauthorizedRequest.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Errors/Exceptions/UnauthorizedRequest.cs similarity index 75% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Errors/Exceptions/UnauthorizedRequest.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Errors/Exceptions/UnauthorizedRequest.cs index 3cb3690f6b3a..dbd2c4c29dd5 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Errors/Exceptions/UnauthorizedRequest.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Errors/Exceptions/UnauthorizedRequest.cs @@ -1,11 +1,11 @@ -namespace SeedBasicAuthOptional; +namespace SeedBasicAuthPwOmitted; /// /// This exception type will be thrown for any non-2XX API responses. /// [Serializable] public class UnauthorizedRequest(UnauthorizedRequestErrorBody body) - : SeedBasicAuthOptionalApiException("UnauthorizedRequest", 401, body) + : SeedBasicAuthPwOmittedApiException("UnauthorizedRequest", 401, body) { /// /// The body of the response that triggered the exception. diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Errors/Types/UnauthorizedRequestErrorBody.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Errors/Types/UnauthorizedRequestErrorBody.cs similarity index 91% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Errors/Types/UnauthorizedRequestErrorBody.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Errors/Types/UnauthorizedRequestErrorBody.cs index 038ffe868775..51966acf8a9c 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/Errors/Types/UnauthorizedRequestErrorBody.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/Errors/Types/UnauthorizedRequestErrorBody.cs @@ -1,8 +1,8 @@ using global::System.Text.Json; using global::System.Text.Json.Serialization; -using SeedBasicAuthOptional.Core; +using SeedBasicAuthPwOmitted.Core; -namespace SeedBasicAuthOptional; +namespace SeedBasicAuthPwOmitted; [Serializable] public record UnauthorizedRequestErrorBody : IJsonOnDeserialized diff --git a/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/ISeedBasicAuthPwOmittedClient.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/ISeedBasicAuthPwOmittedClient.cs new file mode 100644 index 000000000000..688e0a89a529 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/ISeedBasicAuthPwOmittedClient.cs @@ -0,0 +1,6 @@ +namespace SeedBasicAuthPwOmitted; + +public partial interface ISeedBasicAuthPwOmittedClient +{ + public IBasicAuthClient BasicAuth { get; } +} diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/SeedBasicAuthOptional.Custom.props b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/SeedBasicAuthPwOmitted.Custom.props similarity index 100% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/SeedBasicAuthOptional.Custom.props rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/SeedBasicAuthPwOmitted.Custom.props diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/SeedBasicAuthOptional.csproj b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/SeedBasicAuthPwOmitted.csproj similarity index 90% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/SeedBasicAuthOptional.csproj rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/SeedBasicAuthPwOmitted.csproj index 51c362c8292d..a15bde5446ef 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/SeedBasicAuthOptional.csproj +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/SeedBasicAuthPwOmitted.csproj @@ -8,7 +8,7 @@ $(Version) $(Version) README.md - https://github.com/basic-auth-optional/fern + https://github.com/basic-auth-pw-omitted/fern true @@ -52,12 +52,12 @@ - <_Parameter1>SeedBasicAuthOptional.Test + <_Parameter1>SeedBasicAuthPwOmitted.Test diff --git a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/SeedBasicAuthOptionalClient.cs b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/SeedBasicAuthPwOmittedClient.cs similarity index 76% rename from seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/SeedBasicAuthOptionalClient.cs rename to seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/SeedBasicAuthPwOmittedClient.cs index faa7f44d8808..24cde014eb9e 100644 --- a/seed/csharp-sdk/basic-auth-optional/src/SeedBasicAuthOptional/SeedBasicAuthOptionalClient.cs +++ b/seed/csharp-sdk/basic-auth-pw-omitted/src/SeedBasicAuthOptional/SeedBasicAuthPwOmittedClient.cs @@ -1,12 +1,12 @@ -using SeedBasicAuthOptional.Core; +using SeedBasicAuthPwOmitted.Core; -namespace SeedBasicAuthOptional; +namespace SeedBasicAuthPwOmitted; -public partial class SeedBasicAuthOptionalClient : ISeedBasicAuthOptionalClient +public partial class SeedBasicAuthPwOmittedClient : ISeedBasicAuthPwOmittedClient { private readonly RawClient _client; - public SeedBasicAuthOptionalClient( + public SeedBasicAuthPwOmittedClient( string? username = null, string? password = null, ClientOptions? clientOptions = null @@ -17,9 +17,9 @@ public SeedBasicAuthOptionalClient( new Dictionary() { { "X-Fern-Language", "C#" }, - { "X-Fern-SDK-Name", "SeedBasicAuthOptional" }, + { "X-Fern-SDK-Name", "SeedBasicAuthPwOmitted" }, { "X-Fern-SDK-Version", Version.Current }, - { "User-Agent", "Fernbasic-auth-optional/0.0.1" }, + { "User-Agent", "Fernbasic-auth-pw-omitted/0.0.1" }, } ); foreach (var header in platformHeaders) diff --git a/test-definitions/fern/apis/basic-auth-optional/definition/api.yml b/test-definitions/fern/apis/basic-auth-pw-omitted/definition/api.yml similarity index 86% rename from test-definitions/fern/apis/basic-auth-optional/definition/api.yml rename to test-definitions/fern/apis/basic-auth-pw-omitted/definition/api.yml index 8b1d72b0b769..db01794de599 100644 --- a/test-definitions/fern/apis/basic-auth-optional/definition/api.yml +++ b/test-definitions/fern/apis/basic-auth-pw-omitted/definition/api.yml @@ -1,4 +1,4 @@ -name: basic-auth-optional +name: basic-auth-pw-omitted auth: Basic auth-schemes: Basic: diff --git a/test-definitions/fern/apis/basic-auth-optional/definition/basic-auth.yml b/test-definitions/fern/apis/basic-auth-pw-omitted/definition/basic-auth.yml similarity index 100% rename from test-definitions/fern/apis/basic-auth-optional/definition/basic-auth.yml rename to test-definitions/fern/apis/basic-auth-pw-omitted/definition/basic-auth.yml diff --git a/test-definitions/fern/apis/basic-auth-optional/definition/errors.yml b/test-definitions/fern/apis/basic-auth-pw-omitted/definition/errors.yml similarity index 100% rename from test-definitions/fern/apis/basic-auth-optional/definition/errors.yml rename to test-definitions/fern/apis/basic-auth-pw-omitted/definition/errors.yml diff --git a/test-definitions/fern/apis/basic-auth-optional/generators.yml b/test-definitions/fern/apis/basic-auth-pw-omitted/generators.yml similarity index 86% rename from test-definitions/fern/apis/basic-auth-optional/generators.yml rename to test-definitions/fern/apis/basic-auth-pw-omitted/generators.yml index b30d6ed97cd2..fc35e4407493 100644 --- a/test-definitions/fern/apis/basic-auth-optional/generators.yml +++ b/test-definitions/fern/apis/basic-auth-pw-omitted/generators.yml @@ -9,7 +9,7 @@ groups: token: ${GITHUB_TOKEN} mode: push uri: fern-api/php-sdk-tests - branch: basic-auth-optional + branch: basic-auth-pw-omitted go-sdk: generators: - name: fernapi/fern-go-sdk @@ -19,4 +19,4 @@ groups: token: ${GITHUB_TOKEN} mode: push uri: fern-api/go-sdk-tests - branch: basic-auth-optional + branch: basic-auth-pw-omitted From a906dce6b94235274f98df68da15edf9604f953a Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Fri, 3 Apr 2026 19:03:29 +0000 Subject: [PATCH 13/18] fix(csharp-sdk): bump version to 2.57.0 (feat requires minor bump) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- generators/csharp/sdk/versions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generators/csharp/sdk/versions.yml b/generators/csharp/sdk/versions.yml index a4e0bc950ad3..286a22e0f7ab 100644 --- a/generators/csharp/sdk/versions.yml +++ b/generators/csharp/sdk/versions.yml @@ -1,5 +1,5 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json -- version: 2.56.6 +- version: 2.57.0 changelogEntry: - summary: | Support omitting username or password from basic auth when configured via From fcf385ea37fe80a4f8314cd4b06cae4a9ebf9a90 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Fri, 3 Apr 2026 20:48:26 +0000 Subject: [PATCH 14/18] fix(csharp-sdk): handle usernameOmit/passwordOmit in dynamic snippets generator Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../src/EndpointSnippetGenerator.ts | 20 +++++++++++++------ .../basic-auth-pw-omitted/snippet.json | 6 +++--- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/generators/csharp/dynamic-snippets/src/EndpointSnippetGenerator.ts b/generators/csharp/dynamic-snippets/src/EndpointSnippetGenerator.ts index c2d688f73cae..f8d5de1eb4dc 100644 --- a/generators/csharp/dynamic-snippets/src/EndpointSnippetGenerator.ts +++ b/generators/csharp/dynamic-snippets/src/EndpointSnippetGenerator.ts @@ -385,16 +385,24 @@ export class EndpointSnippetGenerator extends WithGeneration { auth: FernIr.dynamic.BasicAuth; values: FernIr.dynamic.BasicAuthValues; }): NamedArgument[] { - return [ - { + // usernameOmit/passwordOmit may exist in newer IR versions + const authRecord = auth as unknown as Record; + const usernameOmitted = authRecord.usernameOmit === true; + const passwordOmitted = authRecord.passwordOmit === true; + const args: NamedArgument[] = []; + if (!usernameOmitted) { + args.push({ name: this.context.getParameterName(auth.username), assignment: this.csharp.Literal.string(values.username) - }, - { + }); + } + if (!passwordOmitted) { + args.push({ name: this.context.getParameterName(auth.password), assignment: this.csharp.Literal.string(values.password) - } - ]; + }); + } + return args; } private getConstructorBearerAuthArgs({ diff --git a/seed/csharp-sdk/basic-auth-pw-omitted/snippet.json b/seed/csharp-sdk/basic-auth-pw-omitted/snippet.json index d34d7ff2de39..5283209c6ab8 100644 --- a/seed/csharp-sdk/basic-auth-pw-omitted/snippet.json +++ b/seed/csharp-sdk/basic-auth-pw-omitted/snippet.json @@ -10,7 +10,7 @@ }, "snippet": { "type": "csharp", - "client": "using SeedBasicAuthPwOmitted;\n\nvar client = new SeedBasicAuthPwOmittedClient(\"USERNAME\", \"PASSWORD\");\nawait client.BasicAuth.GetWithBasicAuthAsync();\n" + "client": "using SeedBasicAuthPwOmitted;\n\nvar client = new SeedBasicAuthPwOmittedClient(\"USERNAME\");\nawait client.BasicAuth.GetWithBasicAuthAsync();\n" } }, { @@ -22,8 +22,8 @@ }, "snippet": { "type": "csharp", - "client": "using SeedBasicAuthPwOmitted;\n\nvar client = new SeedBasicAuthPwOmittedClient(\"USERNAME\", \"PASSWORD\");\nawait client.BasicAuth.PostWithBasicAuthAsync(\n new Dictionary() { { \"key\", \"value\" } }\n);\n" + "client": "using SeedBasicAuthPwOmitted;\n\nvar client = new SeedBasicAuthPwOmittedClient(\"USERNAME\");\nawait client.BasicAuth.PostWithBasicAuthAsync(\n new Dictionary() { { \"key\", \"value\" } }\n);\n" } } ] -} \ No newline at end of file +} From 6fb25c8843c624496efdbc3a87e0f3e91d808d2f Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Fri, 3 Apr 2026 21:17:32 +0000 Subject: [PATCH 15/18] refactor(csharp-sdk): simplify omit checks from === true to !! Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../csharp/dynamic-snippets/src/EndpointSnippetGenerator.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generators/csharp/dynamic-snippets/src/EndpointSnippetGenerator.ts b/generators/csharp/dynamic-snippets/src/EndpointSnippetGenerator.ts index f8d5de1eb4dc..5561bf9923fc 100644 --- a/generators/csharp/dynamic-snippets/src/EndpointSnippetGenerator.ts +++ b/generators/csharp/dynamic-snippets/src/EndpointSnippetGenerator.ts @@ -387,8 +387,8 @@ export class EndpointSnippetGenerator extends WithGeneration { }): NamedArgument[] { // usernameOmit/passwordOmit may exist in newer IR versions const authRecord = auth as unknown as Record; - const usernameOmitted = authRecord.usernameOmit === true; - const passwordOmitted = authRecord.passwordOmit === true; + const usernameOmitted = !!authRecord.usernameOmit; + const passwordOmitted = !!authRecord.passwordOmit; const args: NamedArgument[] = []; if (!usernameOmitted) { args.push({ From 6dafafde188594abfdff9813d870e52717cb2266 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Fri, 3 Apr 2026 21:21:28 +0000 Subject: [PATCH 16/18] fix(csharp-sdk): update irVersion to 66 to match seed.yml Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- generators/csharp/sdk/versions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generators/csharp/sdk/versions.yml b/generators/csharp/sdk/versions.yml index 52d89b5e5f22..eea9f762cada 100644 --- a/generators/csharp/sdk/versions.yml +++ b/generators/csharp/sdk/versions.yml @@ -9,7 +9,7 @@ both are omitted, the Authorization header is skipped entirely. type: feat createdAt: "2026-04-03" - irVersion: 65 + irVersion: 66 - version: 2.57.0-rc.0 changelogEntry: From 5cb74f26a6179cf7bc8ebae82f8d1d175a5650fd Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Fri, 3 Apr 2026 21:32:19 +0000 Subject: [PATCH 17/18] refactor(csharp-sdk): simplify === true to !! in RootClientGenerator omit checks Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../csharp/sdk/src/root-client/RootClientGenerator.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/generators/csharp/sdk/src/root-client/RootClientGenerator.ts b/generators/csharp/sdk/src/root-client/RootClientGenerator.ts index 7c7925524626..368ef2d66494 100644 --- a/generators/csharp/sdk/src/root-client/RootClientGenerator.ts +++ b/generators/csharp/sdk/src/root-client/RootClientGenerator.ts @@ -468,8 +468,8 @@ export class RootClientGenerator extends FileGenerator Date: Fri, 3 Apr 2026 21:54:07 +0000 Subject: [PATCH 18/18] fix: pass usernameOmit/passwordOmit through DynamicSnippetsConverter to dynamic IR The DynamicSnippetsConverter was constructing dynamic BasicAuth with only username and password fields, dropping usernameOmit/passwordOmit from the main IR's BasicAuthScheme. This caused dynamic snippets generators to always include omitted auth fields (e.g. $password) since they couldn't detect the omit flags in the dynamic IR data. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../DynamicSnippetsConverter.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/cli/generation/ir-generator/src/dynamic-snippets/DynamicSnippetsConverter.ts b/packages/cli/generation/ir-generator/src/dynamic-snippets/DynamicSnippetsConverter.ts index b1750ea01c28..5675bc14999b 100644 --- a/packages/cli/generation/ir-generator/src/dynamic-snippets/DynamicSnippetsConverter.ts +++ b/packages/cli/generation/ir-generator/src/dynamic-snippets/DynamicSnippetsConverter.ts @@ -732,11 +732,22 @@ export class DynamicSnippetsConverter { } const scheme = auth.schemes[0]; switch (scheme.type) { - case "basic": - return DynamicSnippets.Auth.basic({ + case "basic": { + const basicAuth: DynamicSnippets.BasicAuth & { + usernameOmit?: boolean; + passwordOmit?: boolean; + } = { username: this.inflateName(scheme.username), password: this.inflateName(scheme.password) - }); + }; + if (scheme.usernameOmit) { + basicAuth.usernameOmit = scheme.usernameOmit; + } + if (scheme.passwordOmit) { + basicAuth.passwordOmit = scheme.passwordOmit; + } + return DynamicSnippets.Auth.basic(basicAuth); + } case "bearer": return DynamicSnippets.Auth.bearer({ token: this.inflateName(scheme.token)