diff --git a/cspell-words.txt b/cspell-words.txt index cb55632f9a3..8c4ea7654ba 100644 --- a/cspell-words.txt +++ b/cspell-words.txt @@ -1,4 +1,3 @@ -anyapi aadd abarrell abelardo @@ -12,19 +11,24 @@ agateno agentic aget ajgateno +alberto alice allocationa amckinney amespac +AMPM ansi antoniomdk +anyapi anyio APAC apihandlers +apiture APIV apng appveyor aread +aries armando armandobelardo artifactory @@ -35,6 +39,7 @@ astimezone asyncio atlassian atwooddc +ATYA aupdate Authorisation Avenir @@ -44,6 +49,7 @@ BAAI babbage* babbagedocs BAML +belvo Benzo Bhargava bing @@ -58,13 +64,13 @@ bufio buie buildvcs buildx +BUXRGCBT Bvalue BYOT Cartesia Cartesia's Ccomparison CCPA -CCPA certifi Cexample Chalef @@ -79,13 +85,12 @@ clazz cmdrc CMMC cobo -cobo +COBQKQ colorama connormahon34 Contoso Convolutional COPPA -COPPA couldn covcheck cread @@ -111,9 +116,11 @@ dearmor deconflict deconfliction dedupe +deel Deel definitionid deno +depcheckrc descs devrev DEVREV @@ -123,6 +130,7 @@ Discrimimants disney dists Dorg +dotenv dprint dsinghvi dtstart @@ -131,12 +139,14 @@ Dunkerque duplicative Duplicative ecommerce +EDITMSG edrevo EEXIST EMEA endent errormsg esac +esbuild esnext esutils etag @@ -151,8 +161,10 @@ ETIMEDOUT eula evelyn exctb +execa fabubaker faciliate +FACYFZIR faqs faraday fastavro @@ -163,17 +175,19 @@ fernapi ferndevtest fernir FERPA -FERPA fhir Fhir FHIR fhirpath Fhirpath +fibonacci FINRA FISMA -FISMA FLAC flowise +folderb +folderc +folderd forw franciscosolis franklinharvey @@ -181,6 +195,7 @@ fred friday friendsofphp fset +fsmonitor Gantt GAPI Gare @@ -191,6 +206,7 @@ george getenv Gett getzep +GGHE Ginnis GLBA goexec @@ -227,12 +243,16 @@ Identitical idgen idna ietf +IHGUMUOY imdb imdbclient +IMHG impls inforce infty initializers +inlinedrequest +inlinedrequests insteadof ints Invalidenstraße @@ -241,6 +261,7 @@ isinstance isnt isort ITAR +jakarta jamsadr Janessa Jayaswal @@ -251,6 +272,7 @@ jersy jfif jfrog jhpak +jiti jmedway jochs joeschmoe @@ -272,6 +294,7 @@ keyid keyof KHTML kikones +KLRTSR Korolenko Kosaraju kwarg @@ -313,6 +336,7 @@ MPLS mrlint mscolnick MSRP +msvc msys MVVM mwalbeck @@ -325,8 +349,10 @@ nanos ndjson neeeeeeeeeed neopets +nextjs ngrok nint +noauth nodets nofocus noindex @@ -335,9 +361,11 @@ nopycln noqa Nord noreferrer +noreqbody Nort Nort's npmjs +ntropy nuint numpy numpydoc @@ -345,6 +373,8 @@ nunit NUNIT nupkg nuuids +nvmrc +oair objc objx Octo @@ -370,6 +400,7 @@ Openrpc openstruct openxmlformats opicional +OQRDWB Origina osano OSSAPI @@ -380,6 +411,7 @@ oxtbtc oxteth oxtusd palantir +pathparam payg Paylod permissioned @@ -426,6 +458,8 @@ Pytest Pythonic pythonv pyuploadcare +queryparam +QUFHNP rbpt RDBMS rdmd @@ -459,7 +493,6 @@ samsung Sayari sbue SCIM -SCIM sdkman SDKMAN serde @@ -478,7 +511,9 @@ sonatype sqlite squareup squidex +starlette storjusd +strenum stretchr Stringifier stringifiying @@ -499,8 +534,10 @@ trevorblades trippable trivago tsconfg +tsup ttlock twilio +TWRGXXWS typeddicts typer typesript @@ -514,6 +551,7 @@ Uncategorize uncategorized UNCATEGORIZED Uncommment +uncompilable undici undiscriminated Undiscriminated @@ -535,6 +573,7 @@ unthemed untransform updateduk upserted +urllib utdodtn utdsp uuid @@ -545,12 +584,14 @@ uutd Vapi vectorizing venus +vercel Vercel versionof Versionto virtualenvs VISITEE vite +vitest Vitest Vlaue vmcontext @@ -569,6 +610,7 @@ Willem williamluer workerd workos +WXCSO Xdock Xdoclint xlink @@ -588,50 +630,3 @@ zrxusd zurg Zurg zurg's -execa -jakarta -strenum -dotenv -alberto -fsmonitor -EDITMSG -folderb -folderd -folderc -noauth -noreqbody -inlinedrequests -pathparam -queryparam -inlinedrequest -vercel -vitest -AMPM -nvmrc -jiti -tsup -esbuild -depcheckrc -fibonacci -deel -apiture -aries -belvo -ntropy -oair -nextjs -starlette -urllib -TWRGXXWS -COBQKQ -ATYA -FACYFZIR -msvc -BUXRGCBT -KLRTSR -GGHE -IMHG -QUFHNP -IHGUMUOY -OQRDWB -WXCSO \ No newline at end of file diff --git a/fern/pages/changelogs/csharp-sdk/2025-03-03.mdx b/fern/pages/changelogs/csharp-sdk/2025-03-03.mdx index ff7e1fd44cf..f730916fddb 100644 --- a/fern/pages/changelogs/csharp-sdk/2025-03-03.mdx +++ b/fern/pages/changelogs/csharp-sdk/2025-03-03.mdx @@ -1,3 +1,29 @@ ## 1.12.0-rc4 **`(feat):`** Add .editorconfig file to the generated SDK. +## 1.12.0-rc5 +**`(fix):`** Fix hardcoded namespace for Pager.cs + +## 1.12.0-rc6 +**`(fix):`** Fix bug where a lambda for sending HTTP requests would use the HTTP request from the outer scope instead of the local scope. + +## 1.12.0-rc7 +**`(internal):`** Move exception handler into client options as an internal property for SDK authors to configure. + +## 1.12.0-rc8 +**`(feat):`** Several class names are computed differently: +- Environment class name: + - Use `environment-class-name` if configured, + - otherwise, fall back to `exported-client-class-name` if configured, with `Environment` suffix, + - otherwise, fall back to `client-class-name` if configured, with `Environment` suffix, + - otherwise, fall back to the computed client name, with `Environment` suffix. +- Base exception class name: + - Use `base-exception-class-name` if configured, + - otherwise, fall back to `exported-client-class-name` if configured, with `Exception` suffix, + - otherwise, fall back to `client-class-name` if configured, with `Exception` suffix, + - otherwise, fall back to the computed client name, with `Exception` suffix. +- Base API exception class name: + - Use `base-api-exception-class-name` if configured, + - otherwise, fall back to `exported-client-class-name` if configured, with `ApiException` suffix, + - otherwise, fall back to `client-class-name` if configured, with `ApiException` suffix, + - otherwise, fall back to the computed client name, with `ApiException` suffix. \ No newline at end of file diff --git a/generators/csharp/sdk/src/SdkCustomConfig.ts b/generators/csharp/sdk/src/SdkCustomConfig.ts index 00ddf9651c1..8ea03e0bcc5 100644 --- a/generators/csharp/sdk/src/SdkCustomConfig.ts +++ b/generators/csharp/sdk/src/SdkCustomConfig.ts @@ -7,6 +7,7 @@ export const SdkCustomConfigSchema = z.strictObject({ "base-exception-class-name": z.string().optional(), "client-class-name": z.string().optional(), "exported-client-class-name": z.string().optional(), + "environment-class-name": z.string().optional(), "package-id": z.string().optional(), "explicit-namespaces": z.boolean().optional(), "root-namespace-for-core-classes": z.boolean().optional(), diff --git a/generators/csharp/sdk/src/SdkGeneratorContext.ts b/generators/csharp/sdk/src/SdkGeneratorContext.ts index 4605438eca7..999ded9b430 100644 --- a/generators/csharp/sdk/src/SdkGeneratorContext.ts +++ b/generators/csharp/sdk/src/SdkGeneratorContext.ts @@ -316,10 +316,7 @@ export class SdkGeneratorContext extends AbstractCsharpGeneratorContext(Func func) } } - internal async Task TryCatchAsync(Func func) + internal async SystemTask TryCatchAsync(Func func) { if (_interceptor == null) { @@ -86,4 +85,6 @@ internal async Task TryCatchAsync(Func> func) throw _interceptor.Intercept(ex); } } + + internal ExceptionHandler Clone() => new ExceptionHandler(_interceptor); } diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/Public/ClientOptions.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/Public/ClientOptions.cs index 8bd68695a45..ab5184250a0 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/Public/ClientOptions.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/Public/ClientOptions.cs @@ -37,6 +37,11 @@ public partial class ClientOptions /// internal Headers Headers { get; init; } = new(); + /// + /// A handler that will handle exceptions thrown by the client. + /// + internal ExceptionHandler ExceptionHandler { get; set; } = new ExceptionHandler(null); + /// /// Clones this and returns a new instance /// @@ -49,6 +54,7 @@ internal ClientOptions Clone() MaxRetries = MaxRetries, Timeout = Timeout, Headers = new Headers(new Dictionary(Headers)), + ExceptionHandler = ExceptionHandler.Clone(), }; } } diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/DataserviceClient.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/DataserviceClient.cs index 6e057ad810e..fb49b492705 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/DataserviceClient.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/DataserviceClient.cs @@ -15,12 +15,9 @@ public partial class DataserviceClient private DataService.DataServiceClient _dataService; - private readonly ExceptionHandler _exceptionHandler; - - internal DataserviceClient(RawClient client, ExceptionHandler exceptionHandler) + internal DataserviceClient(RawClient client) { _client = client; - _exceptionHandler = exceptionHandler; _grpc = _client.Grpc; _dataService = new DataService.DataServiceClient(_grpc.Channel); } @@ -35,8 +32,8 @@ public async Task FooAsync( CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { var response = await _client .SendRequestAsync( @@ -98,8 +95,8 @@ public async Task UploadAsync( CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { try { @@ -139,8 +136,8 @@ public async Task DeleteAsync( CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { try { @@ -180,8 +177,8 @@ public async Task DescribeAsync( CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { try { @@ -221,8 +218,8 @@ public async Task FetchAsync( CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { try { @@ -262,8 +259,8 @@ public async Task ListAsync( CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { try { @@ -303,8 +300,8 @@ public async Task QueryAsync( CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { try { @@ -344,8 +341,8 @@ public async Task UpdateAsync( CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { try { diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/SeedApiClient.cs index 2403ecda005..2a333050f65 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/SeedApiClient.cs @@ -6,12 +6,7 @@ public partial class SeedApiClient { private readonly RawClient _client; - private readonly ExceptionHandler _exceptionHandler; - - public SeedApiClient( - IExceptionInterceptor? exceptionInterceptor = null, - ClientOptions? clientOptions = null - ) + public SeedApiClient(ClientOptions? clientOptions = null) { var defaultHeaders = new Headers( new Dictionary() @@ -30,9 +25,8 @@ public SeedApiClient( clientOptions.Headers[header.Key] = header.Value; } } - _exceptionHandler = new ExceptionHandler(exceptionInterceptor); _client = new RawClient(clientOptions); - Dataservice = new DataserviceClient(_client, _exceptionHandler); + Dataservice = new DataserviceClient(_client); } public DataserviceClient Dataservice { get; init; } diff --git a/seed/csharp-sdk/imdb/exported-client-class-name/README.md b/seed/csharp-sdk/imdb/exported-client-class-name/README.md index 71fca918b09..73790323dd3 100644 --- a/seed/csharp-sdk/imdb/exported-client-class-name/README.md +++ b/seed/csharp-sdk/imdb/exported-client-class-name/README.md @@ -32,7 +32,7 @@ using SeedApi; try { var response = await client.Imdb.CreateMovieAsync(...); -} catch (BaseClientApiException e) { +} catch (CustomClientApiException e) { System.Console.WriteLine(e.Body); System.Console.WriteLine(e.StatusCode); } diff --git a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Core/Public/BaseClientApiException.cs b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Core/Public/CustomClientApiException.cs similarity index 77% rename from seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Core/Public/BaseClientApiException.cs rename to seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Core/Public/CustomClientApiException.cs index d0ec47330e6..5b911835577 100644 --- a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Core/Public/BaseClientApiException.cs +++ b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Core/Public/CustomClientApiException.cs @@ -3,8 +3,8 @@ namespace SeedApi; /// /// This exception type will be thrown for any non-2XX API responses. /// -public class BaseClientApiException(string message, int statusCode, object body) - : BaseClientException(message) +public class CustomClientApiException(string message, int statusCode, object body) + : CustomClientException(message) { /// /// The error code of the response that triggered the exception. diff --git a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Core/Public/BaseClientException.cs b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Core/Public/CustomClientException.cs similarity index 66% rename from seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Core/Public/BaseClientException.cs rename to seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Core/Public/CustomClientException.cs index 4cfc52f8348..45e70d520ef 100644 --- a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Core/Public/BaseClientException.cs +++ b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Core/Public/CustomClientException.cs @@ -5,5 +5,5 @@ namespace SeedApi; /// /// Base exception class for all exceptions thrown by the SDK. /// -public class BaseClientException(string message, Exception? innerException = null) +public class CustomClientException(string message, Exception? innerException = null) : Exception(message, innerException); diff --git a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Imdb/Exceptions/MovieDoesNotExistError.cs b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Imdb/Exceptions/MovieDoesNotExistError.cs index 8e6b8ce0117..be8a62b3ca1 100644 --- a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Imdb/Exceptions/MovieDoesNotExistError.cs +++ b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Imdb/Exceptions/MovieDoesNotExistError.cs @@ -4,7 +4,7 @@ namespace SeedApi; /// This exception type will be thrown for any non-2XX API responses. /// public class MovieDoesNotExistError(string body) - : BaseClientApiException("MovieDoesNotExistError", 404, body) + : CustomClientApiException("MovieDoesNotExistError", 404, body) { /// /// The body of the response that triggered the exception. diff --git a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Imdb/ImdbClient.cs b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Imdb/ImdbClient.cs index c47394bf47c..83c49b796cd 100644 --- a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Imdb/ImdbClient.cs +++ b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Imdb/ImdbClient.cs @@ -50,13 +50,13 @@ public async Task CreateMovieAsync( } catch (JsonException e) { - throw new BaseClientException("Failed to deserialize response", e); + throw new CustomClientException("Failed to deserialize response", e); } } { var responseBody = await response.Raw.Content.ReadAsStringAsync(); - throw new BaseClientApiException( + throw new CustomClientApiException( $"Error with status code {response.StatusCode}", response.StatusCode, responseBody @@ -96,7 +96,7 @@ public async Task GetMovieAsync( } catch (JsonException e) { - throw new BaseClientException("Failed to deserialize response", e); + throw new CustomClientException("Failed to deserialize response", e); } } @@ -116,7 +116,7 @@ public async Task GetMovieAsync( { // unable to map error response, throwing generic error } - throw new BaseClientApiException( + throw new CustomClientApiException( $"Error with status code {response.StatusCode}", response.StatusCode, responseBody diff --git a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Core/ExceptionHandler.cs b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Core/ExceptionHandler.cs index c219cbcc009..a9e75f7bdfc 100644 --- a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Core/ExceptionHandler.cs +++ b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Core/ExceptionHandler.cs @@ -1,5 +1,4 @@ -using System; -using System.Threading.Tasks; +using SystemTask = global::System.Threading.Tasks.Task; namespace SeedApi.Core; @@ -52,7 +51,7 @@ internal T TryCatch(Func func) } } - internal async Task TryCatchAsync(Func func) + internal async SystemTask TryCatchAsync(Func func) { if (_interceptor == null) { @@ -86,4 +85,6 @@ internal async Task TryCatchAsync(Func> func) throw _interceptor.Intercept(ex); } } + + internal ExceptionHandler Clone() => new ExceptionHandler(_interceptor); } diff --git a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Core/Public/ClientOptions.cs b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Core/Public/ClientOptions.cs index 271bd19ea9a..d2370e8ed49 100644 --- a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Core/Public/ClientOptions.cs +++ b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Core/Public/ClientOptions.cs @@ -31,6 +31,11 @@ public partial class ClientOptions /// internal Headers Headers { get; init; } = new(); + /// + /// A handler that will handle exceptions thrown by the client. + /// + internal ExceptionHandler ExceptionHandler { get; set; } = new ExceptionHandler(null); + /// /// Clones this and returns a new instance /// @@ -43,6 +48,7 @@ internal ClientOptions Clone() MaxRetries = MaxRetries, Timeout = Timeout, Headers = new Headers(new Dictionary(Headers)), + ExceptionHandler = ExceptionHandler.Clone(), }; } } diff --git a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Imdb/ImdbClient.cs b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Imdb/ImdbClient.cs index ea83690e160..91676a3b1b5 100644 --- a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Imdb/ImdbClient.cs +++ b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Imdb/ImdbClient.cs @@ -9,12 +9,9 @@ public partial class ImdbClient { private RawClient _client; - private readonly ExceptionHandler _exceptionHandler; - - internal ImdbClient(RawClient client, ExceptionHandler exceptionHandler) + internal ImdbClient(RawClient client) { _client = client; - _exceptionHandler = exceptionHandler; } /// @@ -31,8 +28,8 @@ public async Task CreateMovieAsync( CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { var response = await _client .SendRequestAsync( @@ -83,8 +80,8 @@ public async Task GetMovieAsync( CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { var response = await _client .SendRequestAsync( diff --git a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/SeedApiClient.cs index c84786b614f..b9dfcce7487 100644 --- a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/SeedApiClient.cs @@ -6,13 +6,7 @@ public partial class SeedApiClient { private readonly RawClient _client; - private readonly ExceptionHandler _exceptionHandler; - - public SeedApiClient( - string token, - IExceptionInterceptor? exceptionInterceptor = null, - ClientOptions? clientOptions = null - ) + public SeedApiClient(string token, ClientOptions? clientOptions = null) { var defaultHeaders = new Headers( new Dictionary() @@ -32,9 +26,8 @@ public SeedApiClient( clientOptions.Headers[header.Key] = header.Value; } } - _exceptionHandler = new ExceptionHandler(exceptionInterceptor); _client = new RawClient(clientOptions); - Imdb = new ImdbClient(_client, _exceptionHandler); + Imdb = new ImdbClient(_client); } public ImdbClient Imdb { get; init; } diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/.editorconfig b/seed/csharp-sdk/multi-url-environment/environment-class-name/.editorconfig new file mode 100644 index 00000000000..96559c47517 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/.editorconfig @@ -0,0 +1,26 @@ +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_collection_never_updated_global_highlighting = hint +resharper_member_can_be_private_global_highlighting = hint +resharper_partial_type_with_single_part_highlighting = hint +resharper_property_can_be_made_init_only_global_highlighting = hint +resharper_redundant_name_qualifier_highlighting = none +resharper_redundant_using_directive_highlighting = hint +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_private_field_can_be_converted_to_local_variable_highlighting = hint +resharper_not_accessed_field_local_highlighting = hint +resharper_inconsistent_naming_highlighting = hint + +[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/multi-url-environment/environment-class-name/.github/workflows/ci.yml b/seed/csharp-sdk/multi-url-environment/environment-class-name/.github/workflows/ci.yml new file mode 100644 index 00000000000..a7ac9d4ae02 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/.github/workflows/ci.yml @@ -0,0 +1,69 @@ +name: ci + +on: [push] + +jobs: + compile: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - uses: actions/checkout@master + + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 8.x + + - name: Install tools + run: | + dotnet tool restore + + - name: Build Release + run: dotnet build src -c Release /p:ContinuousIntegrationBuild=true + + unit-tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - uses: actions/checkout@master + + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 8.x + + - name: Install tools + run: | + dotnet tool restore + + - name: Run Tests + run: | + dotnet test src + + + publish: + needs: [compile] + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 8.x + + - name: Publish + env: + NUGET_API_KEY: ${{ secrets.NUGET_API_TOKEN }} + run: | + dotnet pack src -c Release + dotnet nuget push src/SeedMultiUrlEnvironment/bin/Release/*.nupkg --api-key $NUGET_API_KEY --source "nuget.org" diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/.gitignore b/seed/csharp-sdk/multi-url-environment/environment-class-name/.gitignore new file mode 100644 index 00000000000..11014f2b33d --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/.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/multi-url-environment/environment-class-name/.mock/definition/api.yml b/seed/csharp-sdk/multi-url-environment/environment-class-name/.mock/definition/api.yml new file mode 100644 index 00000000000..f362d991927 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/.mock/definition/api.yml @@ -0,0 +1,14 @@ +name: multi-url-environment +auth: bearer +environments: + Production: + urls: + ec2: https://ec2.aws.com + s3: https://s3.aws.com + Staging: + urls: + ec2: https://staging.ec2.aws.com + s3: https://staging.s3.aws.com +default-environment: Production +error-discrimination: + strategy: status-code diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/.mock/definition/ec2.yml b/seed/csharp-sdk/multi-url-environment/environment-class-name/.mock/definition/ec2.yml new file mode 100644 index 00000000000..a3acc4216ff --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/.mock/definition/ec2.yml @@ -0,0 +1,16 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json + +service: + auth: true + url: ec2 + base-path: /ec2 + endpoints: + bootInstance: + auth: true + path: /boot + method: POST + request: + name: BootInstanceRequest + body: + properties: + size: string diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/.mock/definition/s3.yml b/seed/csharp-sdk/multi-url-environment/environment-class-name/.mock/definition/s3.yml new file mode 100644 index 00000000000..ca741b783f5 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/.mock/definition/s3.yml @@ -0,0 +1,17 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json + +service: + auth: true + url: s3 + base-path: /s3 + endpoints: + getPresignedUrl: + auth: true + path: /presigned-url + method: POST + request: + name: GetPresignedUrlRequest + body: + properties: + s3Key: string + response: string diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/.mock/fern.config.json b/seed/csharp-sdk/multi-url-environment/environment-class-name/.mock/fern.config.json new file mode 100644 index 00000000000..4c8e54ac313 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/.mock/fern.config.json @@ -0,0 +1 @@ +{"organization": "fern-test", "version": "*"} \ No newline at end of file diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/.mock/generators.yml b/seed/csharp-sdk/multi-url-environment/environment-class-name/.mock/generators.yml new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/.mock/generators.yml @@ -0,0 +1 @@ +{} diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/README.md b/seed/csharp-sdk/multi-url-environment/environment-class-name/README.md new file mode 100644 index 00000000000..ed13c5523d0 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/README.md @@ -0,0 +1,87 @@ +# 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/SeedMultiUrlEnvironment)](https://nuget.org/packages/SeedMultiUrlEnvironment) + +The Seed C# library provides convenient access to the Seed API from C#. + +## Installation + +```sh +nuget install SeedMultiUrlEnvironment +``` + +## Usage + +Instantiate and use the client with the following: + +```csharp +using SeedMultiUrlEnvironment; + +var client = new SeedMultiUrlEnvironmentClient("TOKEN"); +await client.Ec2.BootInstanceAsync(new BootInstanceRequest { Size = "size" }); +``` + +## 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 SeedMultiUrlEnvironment; + +try { + var response = await client.Ec2.BootInstanceAsync(...); +} catch (SeedMultiUrlEnvironmentApiException 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.Ec2.BootInstanceAsync( + ..., + 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.Ec2.BootInstanceAsync( + ..., + new RequestOptions { + Timeout: TimeSpan.FromSeconds(3) // Override timeout to 3s + } +); +``` + +## 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! \ No newline at end of file diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/reference.md b/seed/csharp-sdk/multi-url-environment/environment-class-name/reference.md new file mode 100644 index 00000000000..40dd16d26e2 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/reference.md @@ -0,0 +1,82 @@ +# Reference +## Ec2 +
client.Ec2.BootInstanceAsync(BootInstanceRequest { ... }) +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Ec2.BootInstanceAsync(new BootInstanceRequest { Size = "size" }); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `BootInstanceRequest` + +
+
+
+
+ + +
+
+
+ +## S3 +
client.S3.GetPresignedUrlAsync(GetPresignedUrlRequest { ... }) -> string +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.S3.GetPresignedUrlAsync(new GetPresignedUrlRequest { S3Key = "s3Key" }); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `GetPresignedUrlRequest` + +
+
+
+
+ + +
+
+
diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/snippet-templates.json b/seed/csharp-sdk/multi-url-environment/environment-class-name/snippet-templates.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/snippet.json b/seed/csharp-sdk/multi-url-environment/environment-class-name/snippet.json new file mode 100644 index 00000000000..deb5a9bfd04 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/snippet.json @@ -0,0 +1,29 @@ +{ + "types": {}, + "endpoints": [ + { + "example_identifier": null, + "id": { + "path": "/ec2/boot", + "method": "POST", + "identifier_override": "endpoint_ec2.bootInstance" + }, + "snippet": { + "type": "csharp", + "client": "using SeedMultiUrlEnvironment;\n\nvar client = new SeedMultiUrlEnvironmentClient(\"TOKEN\");\nawait client.Ec2.BootInstanceAsync(new BootInstanceRequest { Size = \"size\" });\n" + } + }, + { + "example_identifier": null, + "id": { + "path": "/s3/presigned-url", + "method": "POST", + "identifier_override": "endpoint_s3.getPresignedUrl" + }, + "snippet": { + "type": "csharp", + "client": "using SeedMultiUrlEnvironment;\n\nvar client = new SeedMultiUrlEnvironmentClient(\"TOKEN\");\nawait client.S3.GetPresignedUrlAsync(new GetPresignedUrlRequest { S3Key = \"s3Key\" });\n" + } + } + ] +} \ No newline at end of file diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Core/Json/DateOnlyJsonTests.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Core/Json/DateOnlyJsonTests.cs new file mode 100644 index 00000000000..1a99bb9203e --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Core/Json/DateOnlyJsonTests.cs @@ -0,0 +1,76 @@ +using NUnit.Framework; +using SeedMultiUrlEnvironment.Core; + +namespace SeedMultiUrlEnvironment.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)); + } + } +} diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Core/Json/DateTimeJsonTests.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Core/Json/DateTimeJsonTests.cs new file mode 100644 index 00000000000..8c740c5a25f --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Core/Json/DateTimeJsonTests.cs @@ -0,0 +1,110 @@ +using NUnit.Framework; +using SeedMultiUrlEnvironment.Core; + +namespace SeedMultiUrlEnvironment.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)); + } + } +} diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Core/Json/EnumSerializerTests.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Core/Json/EnumSerializerTests.cs new file mode 100644 index 00000000000..e495a0441af --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Core/Json/EnumSerializerTests.cs @@ -0,0 +1,60 @@ +using System.Runtime.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; +using NUnit.Framework; +using SeedMultiUrlEnvironment.Core; + +namespace SeedMultiUrlEnvironment.Test.Core.Json; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class StringEnumSerializerTests +{ + private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; + + private const DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; + private const string KnownEnumValue2String = "known_value2"; + + private const string JsonWithKnownEnum2 = $$""" + { + "enum_property": "{{KnownEnumValue2String}}" + } + """; + + [Test] + public void ShouldParseKnownEnumValue2() + { + var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); + Assert.That(obj, Is.Not.Null); + Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); + } + + [Test] + public void ShouldSerializeKnownEnumValue2() + { + var json = JsonSerializer.SerializeToElement( + new DummyObject { EnumProperty = KnownEnumValue2 }, + JsonOptions + ); + TestContext.Out.WriteLine("Serialized JSON: \n" + json); + var enumString = json.GetProperty("enum_property").GetString(); + Assert.That(enumString, Is.Not.Null); + Assert.That(enumString, Is.EqualTo(KnownEnumValue2String)); + } +} + +public class DummyObject +{ + [JsonPropertyName("enum_property")] + public DummyEnum EnumProperty { get; set; } +} + +[JsonConverter(typeof(EnumSerializer))] +public enum DummyEnum +{ + [EnumMember(Value = "known_value1")] + KnownValue1, + + [EnumMember(Value = "known_value2")] + KnownValue2, +} diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..4565e6423b0 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedMultiUrlEnvironment.Core; + +namespace SeedMultiUrlEnvironment.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; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Core/Json/OneOfSerializerTests.cs new file mode 100644 index 00000000000..7571ef81797 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Core/Json/OneOfSerializerTests.cs @@ -0,0 +1,311 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using NUnit.Framework; +using OneOf; +using SeedMultiUrlEnvironment.Core; + +namespace SeedMultiUrlEnvironment.Test.Core.Json; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class OneOfSerializerTests +{ + private class Foo + { + [JsonPropertyName("string_prop")] + public required string StringProp { get; set; } + } + + private class Bar + { + [JsonPropertyName("int_prop")] + public required int IntProp { get; set; } + } + + private static readonly OneOf OneOf1 = OneOf< + string, + int, + object, + Foo, + Bar + >.FromT2(new { }); + private const string OneOf1String = "{}"; + + private static readonly OneOf OneOf2 = OneOf< + string, + int, + object, + Foo, + Bar + >.FromT0("test"); + private const string OneOf2String = "\"test\""; + + private static readonly OneOf OneOf3 = OneOf< + string, + int, + object, + Foo, + Bar + >.FromT1(123); + private const string OneOf3String = "123"; + + private static readonly OneOf OneOf4 = OneOf< + string, + int, + object, + Foo, + Bar + >.FromT3(new Foo { StringProp = "test" }); + private const string OneOf4String = "{\n \"string_prop\": \"test\"\n}"; + + private static readonly OneOf OneOf5 = OneOf< + string, + int, + object, + Foo, + Bar + >.FromT4(new Bar { IntProp = 5 }); + private const string OneOf5String = "{\n \"int_prop\": 5\n}"; + + [Test] + public void Serialize_OneOfs_Should_Return_Expected_String() + { + (OneOf, string)[] testData = + [ + (OneOf1, OneOf1String), + (OneOf2, OneOf2String), + (OneOf3, OneOf3String), + (OneOf4, OneOf4String), + (OneOf5, OneOf5String), + ]; + Assert.Multiple(() => + { + foreach (var (oneOf, expected) in testData) + { + var result = JsonUtils.Serialize(oneOf); + Assert.That(result, Is.EqualTo(expected)); + } + }); + } + + [Test] + public void OneOfs_Should_Deserialize_From_String() + { + (OneOf, string)[] testData = + [ + (OneOf1, OneOf1String), + (OneOf2, OneOf2String), + (OneOf3, OneOf3String), + (OneOf4, OneOf4String), + (OneOf5, OneOf5String), + ]; + Assert.Multiple(() => + { + foreach (var (oneOf, json) in testData) + { + var result = JsonUtils.Deserialize>(json); + Assert.That(result.Index, Is.EqualTo(oneOf.Index)); + Assert.That(json, Is.EqualTo(JsonUtils.Serialize(result.Value))); + } + }); + } + + private static readonly OneOf? NullableOneOf1 = null; + private const string NullableOneOf1String = "null"; + + private static readonly OneOf? NullableOneOf2 = OneOf< + string, + int, + object, + Foo, + Bar + >.FromT4(new Bar { IntProp = 5 }); + private const string NullableOneOf2String = "{\n \"int_prop\": 5\n}"; + + [Test] + public void Serialize_NullableOneOfs_Should_Return_Expected_String() + { + (OneOf?, string)[] testData = + [ + (NullableOneOf1, NullableOneOf1String), + (NullableOneOf2, NullableOneOf2String), + ]; + Assert.Multiple(() => + { + foreach (var (oneOf, expected) in testData) + { + var result = JsonUtils.Serialize(oneOf); + Assert.That(result, Is.EqualTo(expected)); + } + }); + } + + [Test] + public void NullableOneOfs_Should_Deserialize_From_String() + { + (OneOf?, string)[] testData = + [ + (NullableOneOf1, NullableOneOf1String), + (NullableOneOf2, NullableOneOf2String), + ]; + Assert.Multiple(() => + { + foreach (var (oneOf, json) in testData) + { + var result = JsonUtils.Deserialize?>(json); + Assert.That(result?.Index, Is.EqualTo(oneOf?.Index)); + Assert.That(json, Is.EqualTo(JsonUtils.Serialize(result?.Value))); + } + }); + } + + private static readonly OneOf OneOfWithNullable1 = OneOf< + string, + int, + Foo? + >.FromT2(null); + private const string OneOfWithNullable1String = "null"; + + private static readonly OneOf OneOfWithNullable2 = OneOf< + string, + int, + Foo? + >.FromT2(new Foo { StringProp = "test" }); + private const string OneOfWithNullable2String = "{\n \"string_prop\": \"test\"\n}"; + + private static readonly OneOf OneOfWithNullable3 = OneOf< + string, + int, + Foo? + >.FromT0("test"); + private const string OneOfWithNullable3String = "\"test\""; + + [Test] + public void Serialize_OneOfWithNullables_Should_Return_Expected_String() + { + (OneOf, string)[] testData = + [ + (OneOfWithNullable1, OneOfWithNullable1String), + (OneOfWithNullable2, OneOfWithNullable2String), + (OneOfWithNullable3, OneOfWithNullable3String), + ]; + Assert.Multiple(() => + { + foreach (var (oneOf, expected) in testData) + { + var result = JsonUtils.Serialize(oneOf); + Assert.That(result, Is.EqualTo(expected)); + } + }); + } + + [Test] + public void OneOfWithNullables_Should_Deserialize_From_String() + { + (OneOf, string)[] testData = + [ + // (OneOfWithNullable1, OneOfWithNullable1String), // not possible with .NET's JSON serializer + (OneOfWithNullable2, OneOfWithNullable2String), + (OneOfWithNullable3, OneOfWithNullable3String), + ]; + Assert.Multiple(() => + { + foreach (var (oneOf, json) in testData) + { + var result = JsonUtils.Deserialize>(json); + Assert.That(result.Index, Is.EqualTo(oneOf.Index)); + Assert.That(json, Is.EqualTo(JsonUtils.Serialize(result.Value))); + } + }); + } + + [Test] + public void Serialize_OneOfWithObjectLast_Should_Return_Expected_String() + { + var oneOfWithObjectLast = OneOf.FromT4( + new { random = "data" } + ); + const string oneOfWithObjectLastString = "{\n \"random\": \"data\"\n}"; + + var result = JsonUtils.Serialize(oneOfWithObjectLast); + Assert.That(result, Is.EqualTo(oneOfWithObjectLastString)); + } + + [Test] + public void OneOfWithObjectLast_Should_Deserialize_From_String() + { + const string oneOfWithObjectLastString = "{\n \"random\": \"data\"\n}"; + var result = JsonUtils.Deserialize>( + oneOfWithObjectLastString + ); + Assert.Multiple(() => + { + Assert.That(result.Index, Is.EqualTo(4)); + Assert.That(result.Value, Is.InstanceOf()); + Assert.That(JsonUtils.Serialize(result.Value), Is.EqualTo(oneOfWithObjectLastString)); + }); + } + + [Test] + public void Serialize_OneOfWithObjectNotLast_Should_Return_Expected_String() + { + var oneOfWithObjectNotLast = OneOf.FromT1( + new { random = "data" } + ); + const string oneOfWithObjectNotLastString = "{\n \"random\": \"data\"\n}"; + + var result = JsonUtils.Serialize(oneOfWithObjectNotLast); + Assert.That(result, Is.EqualTo(oneOfWithObjectNotLastString)); + } + + [Test] + public void OneOfWithObjectNotLast_Should_Deserialize_From_String() + { + const string oneOfWithObjectNotLastString = "{\n \"random\": \"data\"\n}"; + var result = JsonUtils.Deserialize>( + oneOfWithObjectNotLastString + ); + Assert.Multiple(() => + { + Assert.That(result.Index, Is.EqualTo(1)); + Assert.That(result.Value, Is.InstanceOf()); + Assert.That( + JsonUtils.Serialize(result.Value), + Is.EqualTo(oneOfWithObjectNotLastString) + ); + }); + } + + [Test] + public void Serialize_OneOfSingleType_Should_Return_Expected_String() + { + var oneOfSingle = OneOf.FromT0("single"); + const string oneOfSingleString = "\"single\""; + + var result = JsonUtils.Serialize(oneOfSingle); + Assert.That(result, Is.EqualTo(oneOfSingleString)); + } + + [Test] + public void OneOfSingleType_Should_Deserialize_From_String() + { + const string oneOfSingleString = "\"single\""; + var result = JsonUtils.Deserialize>(oneOfSingleString); + Assert.Multiple(() => + { + Assert.That(result.Index, Is.EqualTo(0)); + Assert.That(result.Value, Is.EqualTo("single")); + }); + } + + [Test] + public void Deserialize_InvalidData_Should_Throw_Exception() + { + const string invalidJson = "{\"invalid\": \"data\"}"; + + Assert.Throws(() => + { + JsonUtils.Deserialize>(invalidJson); + }); + } +} diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Core/RawClientTests.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Core/RawClientTests.cs new file mode 100644 index 00000000000..dc9f052d519 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Core/RawClientTests.cs @@ -0,0 +1,109 @@ +using NUnit.Framework; +using SeedMultiUrlEnvironment.Core; +using WireMock.Server; +using SystemTask = System.Threading.Tasks.Task; +using WireMockRequest = WireMock.RequestBuilders.Request; +using WireMockResponse = WireMock.ResponseBuilders.Response; + +namespace SeedMultiUrlEnvironment.Test.Core; + +[TestFixture] +[Parallelizable(ParallelScope.Self)] +public class RawClientTests +{ + 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 RawClient.BaseApiRequest + { + 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.That(content, Is.EqualTo("Success")); + + Assert.That(_server.LogEntries.Count, Is.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 RawClient.BaseApiRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Get, + Path = "/test", + }; + + var response = await _rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(statusCode)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + Assert.That(content, Is.EqualTo("Failure")); + + Assert.That(_server.LogEntries.Count, Is.EqualTo(1)); + } + + [TearDown] + public void TearDown() + { + _server.Dispose(); + _httpClient.Dispose(); + } +} diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/SeedMultiUrlEnvironment.Test.Custom.props b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/SeedMultiUrlEnvironment.Test.Custom.props new file mode 100644 index 00000000000..55e683b0772 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/SeedMultiUrlEnvironment.Test.Custom.props @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/SeedMultiUrlEnvironment.Test.csproj b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/SeedMultiUrlEnvironment.Test.csproj new file mode 100644 index 00000000000..7ffb55032c2 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/SeedMultiUrlEnvironment.Test.csproj @@ -0,0 +1,38 @@ + + + + net8.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 + + + + + + + + + + + \ No newline at end of file diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/TestClient.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/TestClient.cs new file mode 100644 index 00000000000..7c75180081c --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/TestClient.cs @@ -0,0 +1,6 @@ +using NUnit.Framework; + +namespace SeedMultiUrlEnvironment.Test; + +[TestFixture] +public class TestClient; diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Unit/MockServer/BaseMockServerTest.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Unit/MockServer/BaseMockServerTest.cs new file mode 100644 index 00000000000..88f62893531 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Unit/MockServer/BaseMockServerTest.cs @@ -0,0 +1,38 @@ +using NUnit.Framework; +using SeedMultiUrlEnvironment; +using WireMock.Logging; +using WireMock.Server; +using WireMock.Settings; + +namespace SeedMultiUrlEnvironment.Test.Unit.MockServer; + +[SetUpFixture] +public class BaseMockServerTest +{ + protected static WireMockServer Server { get; set; } = null!; + + protected static SeedMultiUrlEnvironmentClient Client { get; set; } = null!; + + protected static RequestOptions RequestOptions { get; set; } = null!; + + [OneTimeSetUp] + public void GlobalSetup() + { + // Start the WireMock server + Server = WireMockServer.Start( + new WireMockServerSettings { Logger = new WireMockConsoleLogger() } + ); + + // Initialize the Client + Client = new SeedMultiUrlEnvironmentClient("TOKEN"); + + RequestOptions = new RequestOptions { BaseUrl = Server.Urls[0] }; + } + + [OneTimeTearDown] + public void GlobalTeardown() + { + Server.Stop(); + Server.Dispose(); + } +} diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Unit/MockServer/BootInstanceTest.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Unit/MockServer/BootInstanceTest.cs new file mode 100644 index 00000000000..bb026dbdbbb --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Unit/MockServer/BootInstanceTest.cs @@ -0,0 +1,36 @@ +using NUnit.Framework; +using SeedMultiUrlEnvironment; + +namespace SeedMultiUrlEnvironment.Test.Unit.MockServer; + +[TestFixture] +public class BootInstanceTest : BaseMockServerTest +{ + [Test] + public void MockServerTest() + { + const string requestJson = """ + { + "size": "size" + } + """; + + Server + .Given( + WireMock + .RequestBuilders.Request.Create() + .WithPath("/ec2/boot") + .UsingPost() + .WithBodyAsJson(requestJson) + ) + .RespondWith(WireMock.ResponseBuilders.Response.Create().WithStatusCode(200)); + + Assert.DoesNotThrowAsync( + async () => + await Client.Ec2.BootInstanceAsync( + new BootInstanceRequest { Size = "size" }, + RequestOptions + ) + ); + } +} diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Unit/MockServer/GetPresignedUrlTest.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Unit/MockServer/GetPresignedUrlTest.cs new file mode 100644 index 00000000000..c023822bdb0 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Unit/MockServer/GetPresignedUrlTest.cs @@ -0,0 +1,50 @@ +using FluentAssertions.Json; +using global::System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using SeedMultiUrlEnvironment; +using SeedMultiUrlEnvironment.Core; + +namespace SeedMultiUrlEnvironment.Test.Unit.MockServer; + +[TestFixture] +public class GetPresignedUrlTest : BaseMockServerTest +{ + [Test] + public async global::System.Threading.Tasks.Task MockServerTest() + { + const string requestJson = """ + { + "s3Key": "s3Key" + } + """; + + const string mockResponse = """ + "string" + """; + + Server + .Given( + WireMock + .RequestBuilders.Request.Create() + .WithPath("/s3/presigned-url") + .UsingPost() + .WithBodyAsJson(requestJson) + ) + .RespondWith( + WireMock + .ResponseBuilders.Response.Create() + .WithStatusCode(200) + .WithBody(mockResponse) + ); + + var response = await Client.S3.GetPresignedUrlAsync( + new GetPresignedUrlRequest { S3Key = "s3Key" }, + RequestOptions + ); + JToken + .Parse(mockResponse) + .Should() + .BeEquivalentTo(JToken.Parse(JsonUtils.Serialize(response))); + } +} diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/CollectionItemSerializer.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/CollectionItemSerializer.cs new file mode 100644 index 00000000000..24bd4de62ff --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/CollectionItemSerializer.cs @@ -0,0 +1,89 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace SeedMultiUrlEnvironment.Core; + +/// +/// Json collection converter. +/// +/// Type of item to convert. +/// Converter to use for individual items. +internal class CollectionItemSerializer + : JsonConverter> + where TConverterType : JsonConverter +{ + /// + /// 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(Activator.CreateInstance()); + + 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 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 == null) + { + writer.WriteNullValue(); + return; + } + + var jsonSerializerOptions = new JsonSerializerOptions(options); + jsonSerializerOptions.Converters.Clear(); + jsonSerializerOptions.Converters.Add(Activator.CreateInstance()); + + writer.WriteStartArray(); + + foreach (var data in value) + { + JsonSerializer.Serialize(writer, data, jsonSerializerOptions); + } + + writer.WriteEndArray(); + } +} diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/Constants.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/Constants.cs new file mode 100644 index 00000000000..9983811bcd7 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/Constants.cs @@ -0,0 +1,7 @@ +namespace SeedMultiUrlEnvironment.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/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/DateOnlyConverter.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/DateOnlyConverter.cs new file mode 100644 index 00000000000..c7a7fa5a7a8 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/DateOnlyConverter.cs @@ -0,0 +1,748 @@ +// 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 SeedMultiUrlEnvironment.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 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/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/DateTimeSerializer.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/DateTimeSerializer.cs new file mode 100644 index 00000000000..b35629da11a --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/DateTimeSerializer.cs @@ -0,0 +1,22 @@ +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace SeedMultiUrlEnvironment.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)); + } +} diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/EnumSerializer.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/EnumSerializer.cs new file mode 100644 index 00000000000..39827f62b48 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/EnumSerializer.cs @@ -0,0 +1,53 @@ +using System.Runtime.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace SeedMultiUrlEnvironment.Core; + +internal class EnumSerializer : JsonConverter + where TEnum : struct, Enum +{ + private readonly Dictionary _enumToString = new(); + private readonly Dictionary _stringToEnum = new(); + + public EnumSerializer() + { + var type = typeof(TEnum); + var values = Enum.GetValues(type); + + foreach (var value in values) + { + var enumValue = (TEnum)value; + var enumMember = type.GetField(enumValue.ToString())!; + var attr = enumMember + .GetCustomAttributes(typeof(EnumMemberAttribute), false) + .Cast() + .FirstOrDefault(); + + var stringValue = + attr?.Value + ?? value.ToString() + ?? throw new Exception("Unexpected null enum toString value"); + + _enumToString.Add(enumValue, stringValue); + _stringToEnum.Add(stringValue, enumValue); + } + } + + public override TEnum Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new Exception("The JSON value could not be read as a string."); + return _stringToEnum.TryGetValue(stringValue, out var enumValue) ? enumValue : default; + } + + public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options) + { + writer.WriteStringValue(_enumToString[value]); + } +} diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/Extensions.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/Extensions.cs new file mode 100644 index 00000000000..0e8166d37bc --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/Extensions.cs @@ -0,0 +1,14 @@ +using System.Runtime.Serialization; + +namespace SeedMultiUrlEnvironment.Core; + +internal static class Extensions +{ + public static string Stringify(this Enum value) + { + var field = value.GetType().GetField(value.ToString()); + var attribute = (EnumMemberAttribute) + Attribute.GetCustomAttribute(field, typeof(EnumMemberAttribute)); + return attribute?.Value ?? value.ToString(); + } +} diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/HeaderValue.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/HeaderValue.cs new file mode 100644 index 00000000000..c26f2b89eb6 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/HeaderValue.cs @@ -0,0 +1,17 @@ +using OneOf; + +namespace SeedMultiUrlEnvironment.Core; + +internal sealed class HeaderValue(OneOf> value) + : OneOfBase>(value) +{ + public static implicit operator HeaderValue(string value) + { + return new HeaderValue(value); + } + + public static implicit operator HeaderValue(Func value) + { + return new HeaderValue(value); + } +} diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/Headers.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/Headers.cs new file mode 100644 index 00000000000..18bfc7df172 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/Headers.cs @@ -0,0 +1,17 @@ +namespace SeedMultiUrlEnvironment.Core; + +internal sealed class Headers : Dictionary +{ + public Headers() { } + + public Headers(Dictionary value) + { + foreach (var kvp in value) + { + this[kvp.Key] = new HeaderValue(kvp.Value); + } + } + + public Headers(IEnumerable> value) + : base(value.ToDictionary(e => e.Key, e => e.Value)) { } +} diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/HttpMethodExtensions.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/HttpMethodExtensions.cs new file mode 100644 index 00000000000..1edc24dc1f5 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/HttpMethodExtensions.cs @@ -0,0 +1,8 @@ +using System.Net.Http; + +namespace SeedMultiUrlEnvironment.Core; + +internal static class HttpMethodExtensions +{ + public static readonly HttpMethod Patch = new("PATCH"); +} diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/IRequestOptions.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/IRequestOptions.cs new file mode 100644 index 00000000000..5119671fa56 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/IRequestOptions.cs @@ -0,0 +1,32 @@ +using System; +using System.Net.Http; + +namespace SeedMultiUrlEnvironment.Core; + +internal interface IRequestOptions +{ + /// + /// The Base URL for the API. + /// + public string? BaseUrl { get; init; } + + /// + /// The http client used to make requests. + /// + public HttpClient? HttpClient { get; init; } + + /// + /// The http headers sent with the request. + /// + internal Headers Headers { get; init; } + + /// + /// The http client used to make requests. + /// + public int? MaxRetries { get; init; } + + /// + /// The timeout for the request. + /// + public TimeSpan? Timeout { get; init; } +} diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..fcd408ffd66 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedMultiUrlEnvironment.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/JsonConfiguration.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/JsonConfiguration.cs new file mode 100644 index 00000000000..259b4c7d461 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/JsonConfiguration.cs @@ -0,0 +1,88 @@ +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; + +namespace SeedMultiUrlEnvironment.Core; + +internal static partial class JsonOptions +{ + public static readonly JsonSerializerOptions JsonSerializerOptions; + + static JsonOptions() + { + var options = new JsonSerializerOptions + { + Converters = { new DateTimeSerializer(), +#if USE_PORTABLE_DATE_ONLY + new DateOnlyConverter(), +#endif + new OneOfSerializer() }, +#if DEBUG + WriteIndented = true, +#endif + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static 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 != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, + }; + ConfigureJsonSerializerOptions(options); + JsonSerializerOptions = options; + } + + static partial void ConfigureJsonSerializerOptions(JsonSerializerOptions defaultOptions); +} + +internal static class JsonUtils +{ + public static string Serialize(T obj) + { + return JsonSerializer.Serialize(obj, JsonOptions.JsonSerializerOptions); + } + + public static string SerializeAsString(T obj) + { + var json = JsonSerializer.Serialize(obj, JsonOptions.JsonSerializerOptions); + return json.Trim('"'); + } + + public static T Deserialize(string json) + { + return JsonSerializer.Deserialize(json, JsonOptions.JsonSerializerOptions)!; + } +} diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/OneOfSerializer.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/OneOfSerializer.cs new file mode 100644 index 00000000000..f9088708325 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/OneOfSerializer.cs @@ -0,0 +1,91 @@ +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using OneOf; + +namespace SeedMultiUrlEnvironment.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); + } + + 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 != 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/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/Public/ClientOptions.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/Public/ClientOptions.cs new file mode 100644 index 00000000000..db83a519ee7 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/Public/ClientOptions.cs @@ -0,0 +1,48 @@ +using System; +using System.Net.Http; +using SeedMultiUrlEnvironment.Core; + +namespace SeedMultiUrlEnvironment; + +public partial class ClientOptions +{ + /// + /// The Environment for the API. + /// + public CustomEnvironment Environment { get; init; } = CustomEnvironment.Production; + + /// + /// The http client used to make requests. + /// + public HttpClient HttpClient { get; init; } = new HttpClient(); + + /// + /// The http client used to make requests. + /// + public int MaxRetries { get; init; } = 2; + + /// + /// The timeout for the request. + /// + public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(30); + + /// + /// The http headers sent with the request. + /// + internal Headers Headers { get; init; } = new(); + + /// + /// Clones this and returns a new instance + /// + internal ClientOptions Clone() + { + return new ClientOptions + { + Environment = Environment, + HttpClient = HttpClient, + MaxRetries = MaxRetries, + Timeout = Timeout, + Headers = new Headers(new Dictionary(Headers)), + }; + } +} diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/Public/CustomEnvironment.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/Public/CustomEnvironment.cs new file mode 100644 index 00000000000..2137415d29c --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/Public/CustomEnvironment.cs @@ -0,0 +1,26 @@ +namespace SeedMultiUrlEnvironment; + +public class CustomEnvironment +{ + public static readonly CustomEnvironment Production = new CustomEnvironment + { + Ec2 = "https://ec2.aws.com", + S3 = "https://s3.aws.com", + }; + + public static readonly CustomEnvironment Staging = new CustomEnvironment + { + Ec2 = "https://staging.ec2.aws.com", + S3 = "https://staging.s3.aws.com", + }; + + /// + /// URL for the ec2 service + /// + public string Ec2 { get; init; } + + /// + /// URL for the s3 service + /// + public string S3 { get; init; } +} diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/Public/RequestOptions.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/Public/RequestOptions.cs new file mode 100644 index 00000000000..608688ae852 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/Public/RequestOptions.cs @@ -0,0 +1,33 @@ +using System; +using System.Net.Http; +using SeedMultiUrlEnvironment.Core; + +namespace SeedMultiUrlEnvironment; + +public partial class RequestOptions : IRequestOptions +{ + /// + /// The Base URL for the API. + /// + public string? BaseUrl { get; init; } + + /// + /// The http client used to make requests. + /// + public HttpClient? HttpClient { get; init; } + + /// + /// The http client used to make requests. + /// + public int? MaxRetries { get; init; } + + /// + /// The timeout for the request. + /// + public TimeSpan? Timeout { get; init; } + + /// + /// The http headers sent with the request. + /// + Headers IRequestOptions.Headers { get; init; } = new(); +} diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/Public/SeedMultiUrlEnvironmentApiException.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/Public/SeedMultiUrlEnvironmentApiException.cs new file mode 100644 index 00000000000..334befade35 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/Public/SeedMultiUrlEnvironmentApiException.cs @@ -0,0 +1,18 @@ +namespace SeedMultiUrlEnvironment; + +/// +/// This exception type will be thrown for any non-2XX API responses. +/// +public class SeedMultiUrlEnvironmentApiException(string message, int statusCode, object body) + : SeedMultiUrlEnvironmentException(message) +{ + /// + /// 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/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/Public/SeedMultiUrlEnvironmentException.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/Public/SeedMultiUrlEnvironmentException.cs new file mode 100644 index 00000000000..6095d22f20f --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/Public/SeedMultiUrlEnvironmentException.cs @@ -0,0 +1,9 @@ +using System; + +namespace SeedMultiUrlEnvironment; + +/// +/// Base exception class for all exceptions thrown by the SDK. +/// +public class SeedMultiUrlEnvironmentException(string message, Exception? innerException = null) + : Exception(message, innerException); diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/Public/Version.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/Public/Version.cs new file mode 100644 index 00000000000..4be0adde624 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/Public/Version.cs @@ -0,0 +1,6 @@ +namespace SeedMultiUrlEnvironment; + +internal class Version +{ + public const string Current = "0.0.1"; +} diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/RawClient.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/RawClient.cs new file mode 100644 index 00000000000..b97052e7f18 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/RawClient.cs @@ -0,0 +1,256 @@ +using global::System.Net.Http; +using global::System.Net.Http.Headers; +using global::System.Text; +using SystemTask = global::System.Threading.Tasks.Task; + +namespace SeedMultiUrlEnvironment.Core; + +/// +/// Utility class for making raw HTTP requests to the API. +/// +internal class RawClient(ClientOptions clientOptions) +{ + private const int MaxRetryDelayMs = 60000; + internal int BaseRetryDelay { get; set; } = 1000; + + /// + /// The client options applied on every request. + /// + internal readonly ClientOptions Options = clientOptions; + + [Obsolete("Use SendRequestAsync instead.")] + internal Task MakeRequestAsync( + BaseApiRequest request, + CancellationToken cancellationToken = default + ) + { + return SendRequestAsync(request, cancellationToken); + } + + internal async Task SendRequestAsync( + BaseApiRequest request, + CancellationToken cancellationToken = default + ) + { + // Apply the request timeout. + var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var timeout = request.Options?.Timeout ?? Options.Timeout; + cts.CancelAfter(timeout); + + var httpRequest = CreateHttpRequest(request); + // Send the request. + return await SendWithRetriesAsync(httpRequest, request.Options, cts.Token) + .ConfigureAwait(false); + } + + internal async Task SendRequestAsync( + HttpRequestMessage request, + IRequestOptions? options, + CancellationToken cancellationToken = default + ) + { + // Apply the request timeout. + 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 HttpRequestMessage CloneRequest(HttpRequestMessage request) + { + var clonedRequest = new HttpRequestMessage(request.Method, request.RequestUri); + clonedRequest.Version = request.Version; + clonedRequest.Content = request.Content; + foreach (var header in request.Headers) + { + clonedRequest.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + return clonedRequest; + } + + internal record BaseApiRequest + { + internal required string BaseUrl { get; init; } + + internal required HttpMethod Method { get; init; } + + internal required string Path { get; init; } + + internal string? ContentType { get; init; } + + internal Dictionary Query { get; init; } = new(); + + internal Headers Headers { get; init; } = new(); + + internal IRequestOptions? Options { get; init; } + } + + /// + /// The request object to be sent for streaming uploads. + /// + internal record StreamApiRequest : BaseApiRequest + { + internal Stream? Body { get; init; } + } + + /// + /// The request object to be sent for JSON APIs. + /// + internal record JsonApiRequest : BaseApiRequest + { + internal object? Body { get; init; } + } + + /// + /// The response object returned from the API. + /// + internal record ApiResponse + { + internal required int StatusCode { get; init; } + + internal required HttpResponseMessage Raw { get; init; } + } + + private async 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, cancellationToken).ConfigureAwait(false); + var isRetryableContent = IsRetryableContent(request); + + if (!isRetryableContent) + { + return new ApiResponse { StatusCode = (int)response.StatusCode, Raw = response }; + } + + for (var i = 0; i < maxRetries; i++) + { + if (!ShouldRetry(response)) + { + break; + } + + var delayMs = Math.Min(BaseRetryDelay * (int)Math.Pow(2, i), MaxRetryDelayMs); + await SystemTask.Delay(delayMs, cancellationToken).ConfigureAwait(false); + using var retryRequest = CloneRequest(request); + response = await httpClient + .SendAsync(retryRequest, cancellationToken) + .ConfigureAwait(false); + } + + return new 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 bool IsRetryableContent(HttpRequestMessage request) + { + return request.Content switch + { + StreamContent or MultipartContent => false, + _ => true, + }; + } + + internal HttpRequestMessage CreateHttpRequest(BaseApiRequest request) + { + var url = BuildUrl(request); + var httpRequest = new HttpRequestMessage(request.Method, url); + switch (request) + { + // Add the request body to the request. + case JsonApiRequest jsonRequest: + { + if (jsonRequest.Body != null) + { + httpRequest.Content = new StringContent( + JsonUtils.Serialize(jsonRequest.Body), + Encoding.UTF8, + "application/json" + ); + } + + break; + } + case StreamApiRequest { Body: not null } streamRequest: + httpRequest.Content = new StreamContent(streamRequest.Body); + break; + } + + if (request.ContentType != null) + { + httpRequest.Content.Headers.ContentType = MediaTypeHeaderValue.Parse( + request.ContentType + ); + } + + SetHeaders(httpRequest, Options.Headers); + SetHeaders(httpRequest, request.Headers); + SetHeaders(httpRequest, request.Options?.Headers ?? new Headers()); + + return httpRequest; + } + + private static string BuildUrl(BaseApiRequest request) + { + var baseUrl = request.Options?.BaseUrl ?? request.BaseUrl; + var trimmedBaseUrl = baseUrl.TrimEnd('/'); + var trimmedBasePath = request.Path.TrimStart('/'); + var url = $"{trimmedBaseUrl}/{trimmedBasePath}"; + if (request.Query.Count <= 0) + return url; + url += "?"; + url = request.Query.Aggregate( + url, + (current, queryItem) => + { + if ( + queryItem.Value + is global::System.Collections.IEnumerable collection + and not string + ) + { + var items = collection + .Cast() + .Select(value => $"{queryItem.Key}={value}") + .ToList(); + if (items.Any()) + { + current += string.Join("&", items) + "&"; + } + } + else + { + current += $"{queryItem.Key}={queryItem.Value}&"; + } + + return current; + } + ); + url = url[..^1]; + return url; + } + + private static void SetHeaders(HttpRequestMessage httpRequest, Headers headers) + { + foreach (var header in headers) + { + var value = header.Value?.Match(str => str, func => func.Invoke()); + if (value != null) + { + httpRequest.Headers.TryAddWithoutValidation(header.Key, value); + } + } + } +} diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Ec2/Ec2Client.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Ec2/Ec2Client.cs new file mode 100644 index 00000000000..e01e595f291 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Ec2/Ec2Client.cs @@ -0,0 +1,54 @@ +using System.Net.Http; +using System.Threading; +using global::System.Threading.Tasks; +using SeedMultiUrlEnvironment.Core; + +namespace SeedMultiUrlEnvironment; + +public partial class Ec2Client +{ + private RawClient _client; + + internal Ec2Client(RawClient client) + { + _client = client; + } + + /// + /// + /// await client.Ec2.BootInstanceAsync(new BootInstanceRequest { Size = "size" }); + /// + /// + public async global::System.Threading.Tasks.Task BootInstanceAsync( + BootInstanceRequest request, + RequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + var response = await _client + .SendRequestAsync( + new RawClient.JsonApiRequest + { + BaseUrl = _client.Options.Environment.Ec2, + Method = HttpMethod.Post, + Path = "/ec2/boot", + Body = request, + Options = options, + }, + cancellationToken + ) + .ConfigureAwait(false); + if (response.StatusCode is >= 200 and < 400) + { + return; + } + { + var responseBody = await response.Raw.Content.ReadAsStringAsync(); + throw new SeedMultiUrlEnvironmentApiException( + $"Error with status code {response.StatusCode}", + response.StatusCode, + responseBody + ); + } + } +} diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Ec2/Requests/BootInstanceRequest.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Ec2/Requests/BootInstanceRequest.cs new file mode 100644 index 00000000000..e7a951a091c --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Ec2/Requests/BootInstanceRequest.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; +using SeedMultiUrlEnvironment.Core; + +namespace SeedMultiUrlEnvironment; + +public record BootInstanceRequest +{ + [JsonPropertyName("size")] + public required string Size { get; set; } + + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/S3/Requests/GetPresignedUrlRequest.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/S3/Requests/GetPresignedUrlRequest.cs new file mode 100644 index 00000000000..317b1188822 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/S3/Requests/GetPresignedUrlRequest.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; +using SeedMultiUrlEnvironment.Core; + +namespace SeedMultiUrlEnvironment; + +public record GetPresignedUrlRequest +{ + [JsonPropertyName("s3Key")] + public required string S3Key { get; set; } + + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/S3/S3Client.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/S3/S3Client.cs new file mode 100644 index 00000000000..970a22de1a3 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/S3/S3Client.cs @@ -0,0 +1,63 @@ +using System.Net.Http; +using System.Text.Json; +using System.Threading; +using SeedMultiUrlEnvironment.Core; + +namespace SeedMultiUrlEnvironment; + +public partial class S3Client +{ + private RawClient _client; + + internal S3Client(RawClient client) + { + _client = client; + } + + /// + /// + /// await client.S3.GetPresignedUrlAsync(new GetPresignedUrlRequest { S3Key = "s3Key" }); + /// + /// + public async Task GetPresignedUrlAsync( + GetPresignedUrlRequest request, + RequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + var response = await _client + .SendRequestAsync( + new RawClient.JsonApiRequest + { + BaseUrl = _client.Options.Environment.S3, + Method = HttpMethod.Post, + Path = "/s3/presigned-url", + Body = request, + Options = options, + }, + cancellationToken + ) + .ConfigureAwait(false); + if (response.StatusCode is >= 200 and < 400) + { + var responseBody = await response.Raw.Content.ReadAsStringAsync(); + try + { + return JsonUtils.Deserialize(responseBody)!; + } + catch (JsonException e) + { + throw new SeedMultiUrlEnvironmentException("Failed to deserialize response", e); + } + } + + { + var responseBody = await response.Raw.Content.ReadAsStringAsync(); + throw new SeedMultiUrlEnvironmentApiException( + $"Error with status code {response.StatusCode}", + response.StatusCode, + responseBody + ); + } + } +} diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/SeedMultiUrlEnvironment.Custom.props b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/SeedMultiUrlEnvironment.Custom.props new file mode 100644 index 00000000000..70df2849401 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/SeedMultiUrlEnvironment.Custom.props @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/SeedMultiUrlEnvironment.csproj b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/SeedMultiUrlEnvironment.csproj new file mode 100644 index 00000000000..7b0d093d5d7 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/SeedMultiUrlEnvironment.csproj @@ -0,0 +1,53 @@ + + + + net462;net8.0;net7.0;net6.0;netstandard2.0 + enable + 12 + enable + 0.0.1 + $(Version) + $(Version) + README.md + https://github.com/multi-url-environment/fern + true + + + + false + + + $(DefineConstants);USE_PORTABLE_DATE_ONLY + true + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + <_Parameter1>SeedMultiUrlEnvironment.Test + + + + + diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/SeedMultiUrlEnvironmentClient.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/SeedMultiUrlEnvironmentClient.cs new file mode 100644 index 00000000000..d14e3d734aa --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/SeedMultiUrlEnvironmentClient.cs @@ -0,0 +1,37 @@ +using SeedMultiUrlEnvironment.Core; + +namespace SeedMultiUrlEnvironment; + +public partial class SeedMultiUrlEnvironmentClient +{ + private readonly RawClient _client; + + public SeedMultiUrlEnvironmentClient(string? token = null, ClientOptions? clientOptions = null) + { + var defaultHeaders = new Headers( + new Dictionary() + { + { "Authorization", $"Bearer {token}" }, + { "X-Fern-Language", "C#" }, + { "X-Fern-SDK-Name", "SeedMultiUrlEnvironment" }, + { "X-Fern-SDK-Version", Version.Current }, + { "User-Agent", "Fernmulti-url-environment/0.0.1" }, + } + ); + clientOptions ??= new ClientOptions(); + foreach (var header in defaultHeaders) + { + if (!clientOptions.Headers.ContainsKey(header.Key)) + { + clientOptions.Headers[header.Key] = header.Value; + } + } + _client = new RawClient(clientOptions); + Ec2 = new Ec2Client(_client); + S3 = new S3Client(_client); + } + + public Ec2Client Ec2 { get; init; } + + public S3Client S3 { get; init; } +} diff --git a/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials/Auth/AuthClient.cs b/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials/Auth/AuthClient.cs index 439b4ef3191..93a80440ef0 100644 --- a/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials/Auth/AuthClient.cs +++ b/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials/Auth/AuthClient.cs @@ -9,12 +9,9 @@ public partial class AuthClient { private RawClient _client; - private readonly ExceptionHandler _exceptionHandler; - - internal AuthClient(RawClient client, ExceptionHandler exceptionHandler) + internal AuthClient(RawClient client) { _client = client; - _exceptionHandler = exceptionHandler; } /// @@ -37,8 +34,8 @@ public async Task GetTokenWithClientCredentialsAsync( CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { var response = await _client .SendRequestAsync( @@ -102,8 +99,8 @@ public async Task RefreshTokenAsync( CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { var response = await _client .SendRequestAsync( diff --git a/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials/Core/ExceptionHandler.cs b/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials/Core/ExceptionHandler.cs index dfc0831be3c..06412fd88a8 100644 --- a/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials/Core/ExceptionHandler.cs +++ b/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials/Core/ExceptionHandler.cs @@ -1,5 +1,4 @@ -using System; -using System.Threading.Tasks; +using SystemTask = global::System.Threading.Tasks.Task; namespace SeedOauthClientCredentials.Core; @@ -52,7 +51,7 @@ internal T TryCatch(Func func) } } - internal async Task TryCatchAsync(Func func) + internal async SystemTask TryCatchAsync(Func func) { if (_interceptor == null) { @@ -86,4 +85,6 @@ internal async Task TryCatchAsync(Func> func) throw _interceptor.Intercept(ex); } } + + internal ExceptionHandler Clone() => new ExceptionHandler(_interceptor); } diff --git a/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials/Core/Public/ClientOptions.cs b/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials/Core/Public/ClientOptions.cs index 2823a74fed5..0f4145be4e1 100644 --- a/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials/Core/Public/ClientOptions.cs +++ b/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials/Core/Public/ClientOptions.cs @@ -31,6 +31,11 @@ public partial class ClientOptions /// internal Headers Headers { get; init; } = new(); + /// + /// A handler that will handle exceptions thrown by the client. + /// + internal ExceptionHandler ExceptionHandler { get; set; } = new ExceptionHandler(null); + /// /// Clones this and returns a new instance /// @@ -43,6 +48,7 @@ internal ClientOptions Clone() MaxRetries = MaxRetries, Timeout = Timeout, Headers = new Headers(new Dictionary(Headers)), + ExceptionHandler = ExceptionHandler.Clone(), }; } } diff --git a/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials/SeedOauthClientCredentialsClient.cs b/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials/SeedOauthClientCredentialsClient.cs index ac877141e94..ac194bfa831 100644 --- a/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials/SeedOauthClientCredentialsClient.cs +++ b/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials/SeedOauthClientCredentialsClient.cs @@ -6,12 +6,9 @@ public partial class SeedOauthClientCredentialsClient { private readonly RawClient _client; - private readonly ExceptionHandler _exceptionHandler; - public SeedOauthClientCredentialsClient( string clientId, string clientSecret, - IExceptionInterceptor? exceptionInterceptor = null, ClientOptions? clientOptions = null ) { @@ -32,17 +29,16 @@ public SeedOauthClientCredentialsClient( clientOptions.Headers[header.Key] = header.Value; } } - _exceptionHandler = new ExceptionHandler(exceptionInterceptor); var tokenProvider = new OAuthTokenProvider( clientId, clientSecret, - new AuthClient(new RawClient(clientOptions.Clone()), _exceptionHandler) + new AuthClient(new RawClient(clientOptions.Clone())) ); clientOptions.Headers["Authorization"] = new Func( () => tokenProvider.GetAccessTokenAsync().Result ); _client = new RawClient(clientOptions); - Auth = new AuthClient(_client, _exceptionHandler); + Auth = new AuthClient(_client); } public AuthClient Auth { get; init; } diff --git a/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Complex/ComplexClient.cs b/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Complex/ComplexClient.cs index 07c9c5f556d..3f7fae9287d 100644 --- a/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Complex/ComplexClient.cs +++ b/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Complex/ComplexClient.cs @@ -9,12 +9,9 @@ public partial class ComplexClient { private RawClient _client; - private readonly ExceptionHandler _exceptionHandler; - - internal ComplexClient(RawClient client, ExceptionHandler exceptionHandler) + internal ComplexClient(RawClient client) { _client = client; - _exceptionHandler = exceptionHandler; } /// @@ -39,8 +36,8 @@ public async Task> SearchAsync( CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { if (request is not null) { @@ -78,8 +75,8 @@ private async Task SearchInternalAsync( CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { var response = await _client .SendRequestAsync( diff --git a/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Core/ExceptionHandler.cs b/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Core/ExceptionHandler.cs index b0bd74bd17b..8673920cc02 100644 --- a/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Core/ExceptionHandler.cs +++ b/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Core/ExceptionHandler.cs @@ -1,5 +1,4 @@ -using System; -using System.Threading.Tasks; +using SystemTask = global::System.Threading.Tasks.Task; namespace SeedPagination.Core; @@ -52,7 +51,7 @@ internal T TryCatch(Func func) } } - internal async Task TryCatchAsync(Func func) + internal async SystemTask TryCatchAsync(Func func) { if (_interceptor == null) { @@ -86,4 +85,6 @@ internal async Task TryCatchAsync(Func> func) throw _interceptor.Intercept(ex); } } + + internal ExceptionHandler Clone() => new ExceptionHandler(_interceptor); } diff --git a/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Core/Public/ClientOptions.cs b/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Core/Public/ClientOptions.cs index c9f29e6d0bc..f8aa799b06b 100644 --- a/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Core/Public/ClientOptions.cs +++ b/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Core/Public/ClientOptions.cs @@ -31,6 +31,11 @@ public partial class ClientOptions /// internal Headers Headers { get; init; } = new(); + /// + /// A handler that will handle exceptions thrown by the client. + /// + internal ExceptionHandler ExceptionHandler { get; set; } = new ExceptionHandler(null); + /// /// Clones this and returns a new instance /// @@ -43,6 +48,7 @@ internal ClientOptions Clone() MaxRetries = MaxRetries, Timeout = Timeout, Headers = new Headers(new Dictionary(Headers)), + ExceptionHandler = ExceptionHandler.Clone(), }; } } diff --git a/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/SeedPaginationClient.cs b/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/SeedPaginationClient.cs index e271c6d89c5..7cb8f5e134b 100644 --- a/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/SeedPaginationClient.cs +++ b/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/SeedPaginationClient.cs @@ -6,13 +6,7 @@ public partial class SeedPaginationClient { private readonly RawClient _client; - private readonly ExceptionHandler _exceptionHandler; - - public SeedPaginationClient( - string token, - IExceptionInterceptor? exceptionInterceptor = null, - ClientOptions? clientOptions = null - ) + public SeedPaginationClient(string token, ClientOptions? clientOptions = null) { var defaultHeaders = new Headers( new Dictionary() @@ -32,10 +26,9 @@ public SeedPaginationClient( clientOptions.Headers[header.Key] = header.Value; } } - _exceptionHandler = new ExceptionHandler(exceptionInterceptor); _client = new RawClient(clientOptions); - Complex = new ComplexClient(_client, _exceptionHandler); - Users = new UsersClient(_client, _exceptionHandler); + Complex = new ComplexClient(_client); + Users = new UsersClient(_client); } public ComplexClient Complex { get; init; } diff --git a/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Users/UsersClient.cs b/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Users/UsersClient.cs index 9dea6239456..b64328f4f03 100644 --- a/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Users/UsersClient.cs +++ b/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Users/UsersClient.cs @@ -9,12 +9,9 @@ public partial class UsersClient { private RawClient _client; - private readonly ExceptionHandler _exceptionHandler; - - internal UsersClient(RawClient client, ExceptionHandler exceptionHandler) + internal UsersClient(RawClient client) { _client = client; - _exceptionHandler = exceptionHandler; } /// @@ -36,8 +33,8 @@ public async Task> ListWithCursorPaginationAsync( CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { if (request is not null) { @@ -81,8 +78,8 @@ public async Task> ListWithMixedTypeCursorPaginationAsync( CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { if (request is not null) { @@ -126,8 +123,8 @@ public async Task> ListWithBodyCursorPaginationAsync( CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { if (request is not null) { @@ -178,8 +175,8 @@ public async Task> ListWithOffsetPaginationAsync( CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { if (request is not null) { @@ -232,8 +229,8 @@ public async Task> ListWithDoubleOffsetPaginationAsync( CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { if (request is not null) { @@ -280,8 +277,8 @@ public async Task> ListWithBodyOffsetPaginationAsync( CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { if (request is not null) { @@ -334,8 +331,8 @@ public async Task> ListWithOffsetStepPaginationAsync( CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { if (request is not null) { @@ -387,8 +384,8 @@ public async Task> ListWithOffsetPaginationHasNextPageAsync( CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { if (request is not null) { @@ -435,8 +432,8 @@ public async Task> ListWithExtendedResultsAsync( CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { if (request is not null) { @@ -480,8 +477,8 @@ public async Task> ListWithExtendedResultsAndOptionalDataAsync( CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { if (request is not null) { @@ -525,8 +522,8 @@ public async Task> ListUsernamesAsync( CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { if (request is not null) { @@ -570,8 +567,8 @@ public async Task> ListUsernamesCustomAsync( CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { var _query = new Dictionary(); if (request.StartingAfter != null) @@ -628,8 +625,8 @@ public async Task> ListWithGlobalConfigAsync( CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { if (request is not null) { @@ -669,8 +666,8 @@ private async Task ListWithCursorPaginationInternal CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { var _query = new Dictionary(); if (request.Page != null) @@ -733,8 +730,8 @@ private async Task ListWithMixedTypeCursor CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { var _query = new Dictionary(); if (request.Cursor != null) @@ -787,8 +784,8 @@ private async Task ListWithBodyCursorPaginationInte CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { var response = await _client .SendRequestAsync( @@ -834,8 +831,8 @@ private async Task ListWithOffsetPaginationInternal CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { var _query = new Dictionary(); if (request.Page != null) @@ -898,8 +895,8 @@ private async Task ListWithDoubleOffsetPaginationIn CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { var _query = new Dictionary(); if (request.Page != null) @@ -962,8 +959,8 @@ private async Task ListWithBodyOffsetPaginationInte CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { var response = await _client .SendRequestAsync( @@ -1009,8 +1006,8 @@ private async Task ListWithOffsetStepPaginationInte CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { var _query = new Dictionary(); if (request.Page != null) @@ -1069,8 +1066,8 @@ private async Task ListWithOffsetPaginationHasNextP CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { var _query = new Dictionary(); if (request.Page != null) @@ -1129,8 +1126,8 @@ private async Task ListWithExtendedResultsInternalAsy CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { var _query = new Dictionary(); if (request.Cursor != null) @@ -1181,8 +1178,8 @@ private async Task ListWithExtendedResult CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { var _query = new Dictionary(); if (request.Cursor != null) @@ -1235,8 +1232,8 @@ private async Task ListUsernamesInternalAsync( CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { var _query = new Dictionary(); if (request.StartingAfter != null) @@ -1287,8 +1284,8 @@ private async Task ListWithGlobalConfigInternalAsync( CancellationToken cancellationToken = default ) { - return await _exceptionHandler - .TryCatchAsync(async () => + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => { var _query = new Dictionary(); if (request.Offset != null) diff --git a/seed/csharp-sdk/seed.yml b/seed/csharp-sdk/seed.yml index 3d47ccd55c4..762e42ad35a 100644 --- a/seed/csharp-sdk/seed.yml +++ b/seed/csharp-sdk/seed.yml @@ -92,6 +92,9 @@ fixtures: - customConfig: pascal-case-environments: false outputFolder: no-pascal-case-environments + - customConfig: + environment-class-name: CustomEnvironment + outputFolder: environment-class-name exhaustive: - customConfig: explicit-namespaces: true