From 3e7655da9aac3197e845b34ee13f1cb47aa97edf Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Wed, 8 Sep 2021 15:51:55 -0700 Subject: [PATCH 01/25] intial set-up for llc tests --- .../Azure.Core.Experimental.Tests.csproj | 13 ++ .../tests/Generated/PetStoreClient.cs | 147 ++++++++++++++++++ .../tests/Generated/PetStoreClientOptions.cs | 37 +++++ .../tests/LlcModels/Error.Serialization.cs | 35 +++++ .../tests/LlcModels/Error.cs | 32 ++++ .../tests/LlcModels/Pet.Serialization.cs | 46 ++++++ .../tests/LlcModels/Pet.cs | 33 ++++ .../tests/LowLevelClientTests.cs | 46 ++++++ .../Azure.Core.Experimental/tests/autorest.md | 25 +++ .../tests/swagger/petStoreService.json | 102 ++++++++++++ 10 files changed, 516 insertions(+) create mode 100644 sdk/core/Azure.Core.Experimental/tests/Generated/PetStoreClient.cs create mode 100644 sdk/core/Azure.Core.Experimental/tests/Generated/PetStoreClientOptions.cs create mode 100644 sdk/core/Azure.Core.Experimental/tests/LlcModels/Error.Serialization.cs create mode 100644 sdk/core/Azure.Core.Experimental/tests/LlcModels/Error.cs create mode 100644 sdk/core/Azure.Core.Experimental/tests/LlcModels/Pet.Serialization.cs create mode 100644 sdk/core/Azure.Core.Experimental/tests/LlcModels/Pet.cs create mode 100644 sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs create mode 100644 sdk/core/Azure.Core.Experimental/tests/autorest.md create mode 100644 sdk/core/Azure.Core.Experimental/tests/swagger/petStoreService.json diff --git a/sdk/core/Azure.Core.Experimental/tests/Azure.Core.Experimental.Tests.csproj b/sdk/core/Azure.Core.Experimental/tests/Azure.Core.Experimental.Tests.csproj index b3b93480b660e..3edda79f2951a 100644 --- a/sdk/core/Azure.Core.Experimental/tests/Azure.Core.Experimental.Tests.csproj +++ b/sdk/core/Azure.Core.Experimental/tests/Azure.Core.Experimental.Tests.csproj @@ -18,4 +18,17 @@ + + + + + + + + + + + + + diff --git a/sdk/core/Azure.Core.Experimental/tests/Generated/PetStoreClient.cs b/sdk/core/Azure.Core.Experimental/tests/Generated/PetStoreClient.cs new file mode 100644 index 0000000000000..8f77ddcdf55e9 --- /dev/null +++ b/sdk/core/Azure.Core.Experimental/tests/Generated/PetStoreClient.cs @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// + +#nullable disable + +using System; +using System.Threading.Tasks; +using Azure; +using Azure.Core; +using Azure.Core.Pipeline; + +namespace Azure.Core.Experimental.Tests +{ + /// The PetStore service client. + public partial class PetStoreClient + { + /// The HTTP pipeline for sending and receiving REST requests and responses. + public virtual HttpPipeline Pipeline { get; } + private readonly string[] AuthorizationScopes = { "https://example.azurepetshop.com/.default" }; + private readonly TokenCredential _tokenCredential; + private Uri endpoint; + private readonly string apiVersion; + private readonly ClientDiagnostics _clientDiagnostics; + + /// Initializes a new instance of PetStoreClient for mocking. + protected PetStoreClient() + { + } + + /// Initializes a new instance of PetStoreClient. + /// The workspace development endpoint, for example https://myworkspace.dev.azuresynapse.net. + /// A credential used to authenticate to an Azure Service. + /// The options for configuring the client. + public PetStoreClient(Uri endpoint, TokenCredential credential, PetStoreClientOptions options = null) + { + if (endpoint == null) + { + throw new ArgumentNullException(nameof(endpoint)); + } + if (credential == null) + { + throw new ArgumentNullException(nameof(credential)); + } + + options ??= new PetStoreClientOptions(); + _clientDiagnostics = new ClientDiagnostics(options); + _tokenCredential = credential; + var authPolicy = new BearerTokenAuthenticationPolicy(_tokenCredential, AuthorizationScopes); + Pipeline = HttpPipelineBuilder.Build(options, new HttpPipelinePolicy[] { new LowLevelCallbackPolicy() }, new HttpPipelinePolicy[] { authPolicy }, new ResponseClassifier()); + this.endpoint = endpoint; + apiVersion = options.Version; + } + + /// Get a pet by its Id. + /// Id of pet to return. + /// The request options. +#pragma warning disable AZC0002 + public virtual async Task GetPetAsync(string id, RequestOptions options = null) +#pragma warning restore AZC0002 + { + options ??= new RequestOptions(); + using HttpMessage message = CreateGetPetRequest(id, options); + RequestOptions.Apply(options, message); + using var scope = _clientDiagnostics.CreateScope("PetStoreClient.GetPet"); + scope.Start(); + try + { + await Pipeline.SendAsync(message, options.CancellationToken).ConfigureAwait(false); + if (options.StatusOption == ResponseStatusOption.Default) + { + switch (message.Response.Status) + { + case 200: + return message.Response; + default: + throw await _clientDiagnostics.CreateRequestFailedExceptionAsync(message.Response).ConfigureAwait(false); + } + } + else + { + return message.Response; + } + } + catch (Exception e) + { + scope.Failed(e); + throw; + } + } + + /// Get a pet by its Id. + /// Id of pet to return. + /// The request options. +#pragma warning disable AZC0002 + public virtual Response GetPet(string id, RequestOptions options = null) +#pragma warning restore AZC0002 + { + options ??= new RequestOptions(); + using HttpMessage message = CreateGetPetRequest(id, options); + RequestOptions.Apply(options, message); + using var scope = _clientDiagnostics.CreateScope("PetStoreClient.GetPet"); + scope.Start(); + try + { + Pipeline.Send(message, options.CancellationToken); + if (options.StatusOption == ResponseStatusOption.Default) + { + switch (message.Response.Status) + { + case 200: + return message.Response; + default: + throw _clientDiagnostics.CreateRequestFailedException(message.Response); + } + } + else + { + return message.Response; + } + } + catch (Exception e) + { + scope.Failed(e); + throw; + } + } + + /// Create Request for and operations. + /// Id of pet to return. + /// The request options. + private HttpMessage CreateGetPetRequest(string id, RequestOptions options = null) + { + var message = Pipeline.CreateMessage(); + var request = message.Request; + request.Method = RequestMethod.Get; + var uri = new RawRequestUriBuilder(); + uri.Reset(endpoint); + uri.AppendPath("/pets/", false); + uri.AppendPath(id, true); + request.Uri = uri; + request.Headers.Add("Accept", "application/json, text/json"); + return message; + } + } +} diff --git a/sdk/core/Azure.Core.Experimental/tests/Generated/PetStoreClientOptions.cs b/sdk/core/Azure.Core.Experimental/tests/Generated/PetStoreClientOptions.cs new file mode 100644 index 0000000000000..404e9dde7bb90 --- /dev/null +++ b/sdk/core/Azure.Core.Experimental/tests/Generated/PetStoreClientOptions.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// + +#nullable disable + +using System; +using Azure.Core; + +namespace Azure.Core.Experimental.Tests +{ + /// Client options for PetStoreClient. + public partial class PetStoreClientOptions : ClientOptions + { + private const ServiceVersion LatestVersion = ServiceVersion.V2020_12_01; + + /// The version of the service to use. + public enum ServiceVersion + { + /// Service version "2020-12-01". + V2020_12_01 = 1, + } + + internal string Version { get; } + + /// Initializes new instance of PetStoreClientOptions. + public PetStoreClientOptions(ServiceVersion version = LatestVersion) + { + Version = version switch + { + ServiceVersion.V2020_12_01 => "2020-12-01", + _ => throw new NotSupportedException() + }; + } + } +} diff --git a/sdk/core/Azure.Core.Experimental/tests/LlcModels/Error.Serialization.cs b/sdk/core/Azure.Core.Experimental/tests/LlcModels/Error.Serialization.cs new file mode 100644 index 0000000000000..86a33e90f4e2a --- /dev/null +++ b/sdk/core/Azure.Core.Experimental/tests/LlcModels/Error.Serialization.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// + +#nullable disable + +using System.Text.Json; +using Azure.Core; + +namespace Azure.Core.Experimental.Tests.Models +{ + internal partial class Error + { + internal static Error DeserializeError(JsonElement element) + { + Optional message = default; + Optional code = default; + foreach (var property in element.EnumerateObject()) + { + if (property.NameEquals("message")) + { + message = property.Value.GetString(); + continue; + } + if (property.NameEquals("code")) + { + code = property.Value.GetString(); + continue; + } + } + return new Error(message.Value, code.Value); + } + } +} diff --git a/sdk/core/Azure.Core.Experimental/tests/LlcModels/Error.cs b/sdk/core/Azure.Core.Experimental/tests/LlcModels/Error.cs new file mode 100644 index 0000000000000..fa70d6c9c7830 --- /dev/null +++ b/sdk/core/Azure.Core.Experimental/tests/LlcModels/Error.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// + +#nullable disable + +namespace Azure.Core.Experimental.Tests.Models +{ + /// Error details. + internal partial class Error + { + /// Initializes a new instance of Error. + internal Error() + { + } + + /// Initializes a new instance of Error. + /// Error message. + /// Error code. + internal Error(string message, string code) + { + Message = message; + Code = code; + } + + /// Error message. + public string Message { get; } + /// Error code. + public string Code { get; } + } +} diff --git a/sdk/core/Azure.Core.Experimental/tests/LlcModels/Pet.Serialization.cs b/sdk/core/Azure.Core.Experimental/tests/LlcModels/Pet.Serialization.cs new file mode 100644 index 0000000000000..0cdf3621b5d83 --- /dev/null +++ b/sdk/core/Azure.Core.Experimental/tests/LlcModels/Pet.Serialization.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// + +#nullable disable + +using System.Text.Json; +using Azure.Core; + +namespace Azure.Core.Experimental.Tests.Models +{ + public partial class Pet + { + internal static Pet DeserializePet(JsonElement element) + { + Optional id = default; + Optional name = default; + Optional species = default; + foreach (var property in element.EnumerateObject()) + { + if (property.NameEquals("id")) + { + if (property.Value.ValueKind == JsonValueKind.Null) + { + property.ThrowNonNullablePropertyIsNull(); + continue; + } + id = property.Value.GetInt32(); + continue; + } + if (property.NameEquals("name")) + { + name = property.Value.GetString(); + continue; + } + if (property.NameEquals("species")) + { + species = property.Value.GetString(); + continue; + } + } + return new Pet(Optional.ToNullable(id), name.Value, species.Value); + } + } +} diff --git a/sdk/core/Azure.Core.Experimental/tests/LlcModels/Pet.cs b/sdk/core/Azure.Core.Experimental/tests/LlcModels/Pet.cs new file mode 100644 index 0000000000000..0af76cfb55e7d --- /dev/null +++ b/sdk/core/Azure.Core.Experimental/tests/LlcModels/Pet.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// + +#nullable disable + +namespace Azure.Core.Experimental.Tests.Models +{ + /// The Pet. + public partial class Pet + { + /// Initializes a new instance of Pet. + internal Pet() + { + } + + /// Initializes a new instance of Pet. + /// + /// + /// + internal Pet(int? id, string name, string species) + { + Id = id; + Name = name; + Species = species; + } + + public int? Id { get; } + public string Name { get; } + public string Species { get; } + } +} diff --git a/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs b/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs new file mode 100644 index 0000000000000..2e43c18b40ce1 --- /dev/null +++ b/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Azure.Core.Experimental.Tests; +using Azure.Core.Experimental.Tests.Models; +using Azure.Core.TestFramework; +using Azure.Identity; +using NUnit.Framework; + +namespace Azure.Core.Tests +{ + public class LowLevelClientTests : ClientTestBase + { + public LowLevelClientTests(bool isAsync) : base(isAsync) + { + } + + private PetStoreClient client { get; set; } + private readonly Uri _url = new Uri("https://example.azurepetstore.com"); + + private TokenCredential GetCredential() + { + return new EnvironmentCredential(); + } + + [SetUp] + public void TestSetup() + { + client = InstrumentClient(new PetStoreClient(_url, GetCredential())); + } + + [Test] + public async Task CanCallLlcGetMethodAsync() + { + Response response = await client.GetPetAsync("pet1", new RequestOptions()); + } + + [Test] + public async Task CanCallHlcGetMethodAsync() + { + Response pet = await client.GetPetAsync("pet1"); + } + } +} diff --git a/sdk/core/Azure.Core.Experimental/tests/autorest.md b/sdk/core/Azure.Core.Experimental/tests/autorest.md new file mode 100644 index 0000000000000..9a67b503b44a7 --- /dev/null +++ b/sdk/core/Azure.Core.Experimental/tests/autorest.md @@ -0,0 +1,25 @@ +# Pet Shop Service + +Run `dotnet build /t:GenerateCode` to generate code. + +### AutoRest Configuration +> see https://aka.ms/autorest + +``` yaml +input-file: + - $(this-folder)/swagger/petStoreService.json +#namespace: Azure.Analytics.Synapse.Administration +public-clients: true +low-level-client: true +security: AADToken +security-scopes: https://example.azurepetshop.com/.default +``` + +### Make Endpoint type as Uri + +``` yaml +directive: + from: swagger-document + where: $.parameters.Endpoint + transform: $.format = "url" +``` diff --git a/sdk/core/Azure.Core.Experimental/tests/swagger/petStoreService.json b/sdk/core/Azure.Core.Experimental/tests/swagger/petStoreService.json new file mode 100644 index 0000000000000..c025938a60352 --- /dev/null +++ b/sdk/core/Azure.Core.Experimental/tests/swagger/petStoreService.json @@ -0,0 +1,102 @@ +{ + "swagger": "2.0", + "info": { + "version": "2020-12-01", + "title": "PetStoreClient" + }, + "paths": { + "/pets/{id}": { + "get": { + "operationId": "PetStore_GetPet", + "description": "Get a pet by its Id", + "consumes": [], + "produces": [ + "application/json", + "text/json" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Id of pet to return", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Success response.", + "schema": { + "$ref": "#/definitions/Pet" + } + }, + "default": { + "description": "Error response describing why the operation failed", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + } + }, + "x-ms-parameterized-host": { + "hostTemplate": "{endpoint}", + "useSchemePrefix": false, + "parameters": [ + { + "$ref": "#/parameters/Endpoint" + } + ] + }, + "parameters": { + "Endpoint": { + "name": "endpoint", + "description": "The workspace development endpoint, for example https://myworkspace.dev.azuresynapse.net.", + "required": true, + "type": "string", + "in": "path", + "x-ms-skip-url-encoding": true, + "x-ms-parameter-location": "client" + }, + "Id": { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "The Id of the pet.", + "minLength": 1, + "x-ms-parameter-location": "method" + } + }, + "definitions": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "species": { + "type": "string" + } + } + }, + "Error": { + "description": "Error details", + "type": "object", + "properties": { + "message": { + "description": "Error message", + "type": "string" + }, + "code": { + "description": "Error code", + "type": "string" + } + } + } + } +} From 501c49d8c81e6db964b9b042fd5cbf42cd5b3eb7 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Wed, 8 Sep 2021 16:49:28 -0700 Subject: [PATCH 02/25] don't generate the code --- .../Azure.Core.Experimental.Tests.csproj | 7 +- .../Error.Serialization.cs | 0 .../LowLevelClientModels}/Error.cs | 0 .../Pet.Serialization.cs | 0 .../LowLevelClientModels}/Pet.cs | 0 .../PetStoreClient.cs | 2 - .../PetStoreClientOptions.cs | 0 .../tests/LowLevelClientTests.cs | 7 +- .../Azure.Core.Experimental/tests/autorest.md | 25 ----- .../tests/swagger/petStoreService.json | 102 ------------------ 10 files changed, 6 insertions(+), 137 deletions(-) rename sdk/core/Azure.Core.Experimental/tests/{LlcModels => LowLevelClient/LowLevelClientModels}/Error.Serialization.cs (100%) rename sdk/core/Azure.Core.Experimental/tests/{LlcModels => LowLevelClient/LowLevelClientModels}/Error.cs (100%) rename sdk/core/Azure.Core.Experimental/tests/{LlcModels => LowLevelClient/LowLevelClientModels}/Pet.Serialization.cs (100%) rename sdk/core/Azure.Core.Experimental/tests/{LlcModels => LowLevelClient/LowLevelClientModels}/Pet.cs (100%) rename sdk/core/Azure.Core.Experimental/tests/{Generated => LowLevelClient}/PetStoreClient.cs (99%) rename sdk/core/Azure.Core.Experimental/tests/{Generated => LowLevelClient}/PetStoreClientOptions.cs (100%) delete mode 100644 sdk/core/Azure.Core.Experimental/tests/autorest.md delete mode 100644 sdk/core/Azure.Core.Experimental/tests/swagger/petStoreService.json diff --git a/sdk/core/Azure.Core.Experimental/tests/Azure.Core.Experimental.Tests.csproj b/sdk/core/Azure.Core.Experimental/tests/Azure.Core.Experimental.Tests.csproj index 3edda79f2951a..520e549bcefe1 100644 --- a/sdk/core/Azure.Core.Experimental/tests/Azure.Core.Experimental.Tests.csproj +++ b/sdk/core/Azure.Core.Experimental/tests/Azure.Core.Experimental.Tests.csproj @@ -1,6 +1,7 @@  $(RequiredTargetFrameworks) + true @@ -17,18 +18,14 @@ - + - - - - diff --git a/sdk/core/Azure.Core.Experimental/tests/LlcModels/Error.Serialization.cs b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Error.Serialization.cs similarity index 100% rename from sdk/core/Azure.Core.Experimental/tests/LlcModels/Error.Serialization.cs rename to sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Error.Serialization.cs diff --git a/sdk/core/Azure.Core.Experimental/tests/LlcModels/Error.cs b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Error.cs similarity index 100% rename from sdk/core/Azure.Core.Experimental/tests/LlcModels/Error.cs rename to sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Error.cs diff --git a/sdk/core/Azure.Core.Experimental/tests/LlcModels/Pet.Serialization.cs b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Pet.Serialization.cs similarity index 100% rename from sdk/core/Azure.Core.Experimental/tests/LlcModels/Pet.Serialization.cs rename to sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Pet.Serialization.cs diff --git a/sdk/core/Azure.Core.Experimental/tests/LlcModels/Pet.cs b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Pet.cs similarity index 100% rename from sdk/core/Azure.Core.Experimental/tests/LlcModels/Pet.cs rename to sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Pet.cs diff --git a/sdk/core/Azure.Core.Experimental/tests/Generated/PetStoreClient.cs b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs similarity index 99% rename from sdk/core/Azure.Core.Experimental/tests/Generated/PetStoreClient.cs rename to sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs index 8f77ddcdf55e9..fdcbcc68229d7 100644 --- a/sdk/core/Azure.Core.Experimental/tests/Generated/PetStoreClient.cs +++ b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -// - #nullable disable using System; diff --git a/sdk/core/Azure.Core.Experimental/tests/Generated/PetStoreClientOptions.cs b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClientOptions.cs similarity index 100% rename from sdk/core/Azure.Core.Experimental/tests/Generated/PetStoreClientOptions.cs rename to sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClientOptions.cs diff --git a/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs b/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs index 2e43c18b40ce1..1aed80513552a 100644 --- a/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs +++ b/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs @@ -11,9 +11,9 @@ namespace Azure.Core.Tests { - public class LowLevelClientTests : ClientTestBase + public class LowLevelClientTests { - public LowLevelClientTests(bool isAsync) : base(isAsync) + public LowLevelClientTests() { } @@ -28,7 +28,7 @@ private TokenCredential GetCredential() [SetUp] public void TestSetup() { - client = InstrumentClient(new PetStoreClient(_url, GetCredential())); + client = new PetStoreClient(_url, GetCredential()); } [Test] @@ -40,6 +40,7 @@ public async Task CanCallLlcGetMethodAsync() [Test] public async Task CanCallHlcGetMethodAsync() { + // This currently fails to build. Response pet = await client.GetPetAsync("pet1"); } } diff --git a/sdk/core/Azure.Core.Experimental/tests/autorest.md b/sdk/core/Azure.Core.Experimental/tests/autorest.md deleted file mode 100644 index 9a67b503b44a7..0000000000000 --- a/sdk/core/Azure.Core.Experimental/tests/autorest.md +++ /dev/null @@ -1,25 +0,0 @@ -# Pet Shop Service - -Run `dotnet build /t:GenerateCode` to generate code. - -### AutoRest Configuration -> see https://aka.ms/autorest - -``` yaml -input-file: - - $(this-folder)/swagger/petStoreService.json -#namespace: Azure.Analytics.Synapse.Administration -public-clients: true -low-level-client: true -security: AADToken -security-scopes: https://example.azurepetshop.com/.default -``` - -### Make Endpoint type as Uri - -``` yaml -directive: - from: swagger-document - where: $.parameters.Endpoint - transform: $.format = "url" -``` diff --git a/sdk/core/Azure.Core.Experimental/tests/swagger/petStoreService.json b/sdk/core/Azure.Core.Experimental/tests/swagger/petStoreService.json deleted file mode 100644 index c025938a60352..0000000000000 --- a/sdk/core/Azure.Core.Experimental/tests/swagger/petStoreService.json +++ /dev/null @@ -1,102 +0,0 @@ -{ - "swagger": "2.0", - "info": { - "version": "2020-12-01", - "title": "PetStoreClient" - }, - "paths": { - "/pets/{id}": { - "get": { - "operationId": "PetStore_GetPet", - "description": "Get a pet by its Id", - "consumes": [], - "produces": [ - "application/json", - "text/json" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "description": "Id of pet to return", - "required": true, - "type": "string" - } - ], - "responses": { - "200": { - "description": "Success response.", - "schema": { - "$ref": "#/definitions/Pet" - } - }, - "default": { - "description": "Error response describing why the operation failed", - "schema": { - "$ref": "#/definitions/Error" - } - } - } - } - } - }, - "x-ms-parameterized-host": { - "hostTemplate": "{endpoint}", - "useSchemePrefix": false, - "parameters": [ - { - "$ref": "#/parameters/Endpoint" - } - ] - }, - "parameters": { - "Endpoint": { - "name": "endpoint", - "description": "The workspace development endpoint, for example https://myworkspace.dev.azuresynapse.net.", - "required": true, - "type": "string", - "in": "path", - "x-ms-skip-url-encoding": true, - "x-ms-parameter-location": "client" - }, - "Id": { - "name": "id", - "in": "path", - "required": true, - "type": "string", - "description": "The Id of the pet.", - "minLength": 1, - "x-ms-parameter-location": "method" - } - }, - "definitions": { - "Pet": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "species": { - "type": "string" - } - } - }, - "Error": { - "description": "Error details", - "type": "object", - "properties": { - "message": { - "description": "Error message", - "type": "string" - }, - "code": { - "description": "Error code", - "type": "string" - } - } - } - } -} From 7142fb9a6fa95f6f6de4d68dec7325f6cdeafcd6 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Mon, 13 Sep 2021 08:03:35 -0700 Subject: [PATCH 03/25] saving work in progress --- .../tests/LowLevelClient/PetStoreClient.cs | 6 +- .../Azure.Core/src/Pipeline/AzureError.cs | 37 +++ .../Azure.Core/src/Pipeline/HttpPipeline.cs | 5 +- sdk/core/Azure.Core/src/Response.cs | 47 ++++ sdk/core/Azure.Core/src/ResponseClassifier.cs | 217 ++++++++++++++++++ 5 files changed, 309 insertions(+), 3 deletions(-) create mode 100644 sdk/core/Azure.Core/src/Pipeline/AzureError.cs diff --git a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs index fdcbcc68229d7..ca581865cf693 100644 --- a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs +++ b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs @@ -21,6 +21,7 @@ public partial class PetStoreClient private Uri endpoint; private readonly string apiVersion; private readonly ClientDiagnostics _clientDiagnostics; + private readonly ResponseClassifier _responseClassifier; /// Initializes a new instance of PetStoreClient for mocking. protected PetStoreClient() @@ -44,6 +45,7 @@ public PetStoreClient(Uri endpoint, TokenCredential credential, PetStoreClientOp options ??= new PetStoreClientOptions(); _clientDiagnostics = new ClientDiagnostics(options); + _responseClassifier = new ResponseClassifier(options); _tokenCredential = credential; var authPolicy = new BearerTokenAuthenticationPolicy(_tokenCredential, AuthorizationScopes); Pipeline = HttpPipelineBuilder.Build(options, new HttpPipelinePolicy[] { new LowLevelCallbackPolicy() }, new HttpPipelinePolicy[] { authPolicy }, new ResponseClassifier()); @@ -73,7 +75,7 @@ public virtual async Task GetPetAsync(string id, RequestOptions option case 200: return message.Response; default: - throw await _clientDiagnostics.CreateRequestFailedExceptionAsync(message.Response).ConfigureAwait(false); + throw await _responseClassifier.CreateRequestFailedExceptionAsync(message.Response).ConfigureAwait(false); } } else @@ -110,7 +112,7 @@ public virtual Response GetPet(string id, RequestOptions options = null) case 200: return message.Response; default: - throw _clientDiagnostics.CreateRequestFailedException(message.Response); + throw _responseClassifier.CreateRequestFailedException(message.Response); } } else diff --git a/sdk/core/Azure.Core/src/Pipeline/AzureError.cs b/sdk/core/Azure.Core/src/Pipeline/AzureError.cs new file mode 100644 index 0000000000000..6a7a3b1519b74 --- /dev/null +++ b/sdk/core/Azure.Core/src/Pipeline/AzureError.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Azure.Core.Pipeline +{ + /// + /// + public class AzureError + { + /// + /// + /// + public AzureError() + { + Data = new Dictionary(); + } + + /// + /// Gets or sets the error message. + /// + public string? Message { get; set; } + + /// + /// Gets or sets the error code. + /// + public string? ErrorCode { get; set; } + + /// + /// Gets an additional data returned with the error response. + /// + public IDictionary Data { get; } + } +} diff --git a/sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs b/sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs index c283475c0d86e..df7278ee6f3e5 100644 --- a/sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs +++ b/sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs @@ -70,7 +70,9 @@ public ValueTask SendAsync(HttpMessage message, CancellationToken cancellationTo { message.CancellationToken = cancellationToken; AddHttpMessageProperties(message); - return _pipeline.Span[0].ProcessAsync(message, _pipeline.Slice(1)); + var value = _pipeline.Span[0].ProcessAsync(message, _pipeline.Slice(1)); + message.Response.EvaluateError(message); + return value; } /// @@ -83,6 +85,7 @@ public void Send(HttpMessage message, CancellationToken cancellationToken) message.CancellationToken = cancellationToken; AddHttpMessageProperties(message); _pipeline.Span[0].Process(message, _pipeline.Slice(1)); + message.Response.EvaluateError(message); } /// /// Invokes the pipeline asynchronously with the provided request. diff --git a/sdk/core/Azure.Core/src/Response.cs b/sdk/core/Azure.Core/src/Response.cs index e31a6ec4752ad..65ea569f9d17b 100644 --- a/sdk/core/Azure.Core/src/Response.cs +++ b/sdk/core/Azure.Core/src/Response.cs @@ -5,7 +5,9 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Threading.Tasks; using Azure.Core; +using Azure.Core.Pipeline; namespace Azure { @@ -111,6 +113,51 @@ public virtual BinaryData Content /// The enumerating in the response. protected internal abstract IEnumerable EnumerateHeaders(); + internal bool? IsError { get; set; } + + internal ResponseClassifier? ResponseClassifier { get; set; } + + internal void EvaluateError(HttpMessage message) + { + if (!IsError.HasValue) + { + IsError = message.ResponseClassifier.IsErrorResponse(message); + ResponseClassifier = message.ResponseClassifier; + } + } + + /// + /// If the response is an error response, throw a RequestFailedException. + /// + public void ThrowIfError() + { + if (!IsError.HasValue) + { + throw new InvalidOperationException("IsError value should have been cached by the pipeline."); + } + + if (IsError.Value) + { + throw ResponseClassifier!.CreateRequestFailedException(this); + } + } + + /// + /// If the response is an error response, throw a RequestFailedException. + /// + public async Task ThrowIfErrorAsync() + { + if (!IsError.HasValue) + { + throw new InvalidOperationException("IsError value should have been cached by the pipeline."); + } + + if (IsError.Value) + { + throw await ResponseClassifier!.CreateRequestFailedExceptionAsync(this).ConfigureAwait(false); + } + } + /// /// Creates a new instance of with the provided value and HTTP response. /// diff --git a/sdk/core/Azure.Core/src/ResponseClassifier.cs b/sdk/core/Azure.Core/src/ResponseClassifier.cs index e770e6e43271f..54d6226975acc 100644 --- a/sdk/core/Azure.Core/src/ResponseClassifier.cs +++ b/sdk/core/Azure.Core/src/ResponseClassifier.cs @@ -2,7 +2,14 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; +using System.Globalization; using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Azure.Core.Pipeline; namespace Azure.Core { @@ -11,6 +18,30 @@ namespace Azure.Core /// public class ResponseClassifier { + private const string DefaultFailureMessage = "Service request failed."; + + private readonly HttpMessageSanitizer _sanitizer; + + /// + /// Initializes a new instance of . + /// + public ResponseClassifier() : this(new DefaultClientOptions()) + { + } + + /// + /// + + /// + /// Initializes a new instance of . + /// + public ResponseClassifier(ClientOptions options) + { + _sanitizer = new HttpMessageSanitizer( + options?.Diagnostics.LoggedQueryParameters.ToArray() ?? Array.Empty(), + options?.Diagnostics.LoggedHeaderNames.ToArray() ?? Array.Empty()); + } + /// /// Specifies if the request contained in the should be retried. /// @@ -57,5 +88,191 @@ public virtual bool IsErrorResponse(HttpMessage message) var statusKind = message.Response.Status / 100; return statusKind == 4 || statusKind == 5; } + + /// + /// Creates an instance of for the provided failed . + /// + /// + /// + /// + /// + public virtual async ValueTask CreateRequestFailedExceptionAsync( + Response response, + AzureError? error = null, + Exception? innerException = null) + { + var content = await ReadContentAsync(response, true).ConfigureAwait(false); + return CreateRequestFailedExceptionWithContent(response, content, error, innerException); + } + + /// + /// Creates an instance of for the provided failed . + /// + /// + /// + /// + /// + public virtual RequestFailedException CreateRequestFailedException( + Response response, + AzureError? error = null, + Exception? innerException = null) + { + string? content = ReadContentAsync(response, false).EnsureCompleted(); + return CreateRequestFailedExceptionWithContent(response, content, error, innerException); + } + + /// + /// Partial method that can optionally be defined to extract the error + /// message, code, and details in a service specific manner. + /// + /// The response headers. + /// The extracted text content + protected virtual AzureError? ExtractFailureContent(Response response, string? textContent) + { + try + { + // Optimistic check for JSON object we expect + if (textContent == null || + !textContent.StartsWith("{", StringComparison.OrdinalIgnoreCase)) + return null; + + var extractFailureContent = new AzureError(); + + using JsonDocument document = JsonDocument.Parse(textContent); + if (document.RootElement.TryGetProperty("error", out var errorProperty)) + { + if (errorProperty.TryGetProperty("code", out var codeProperty)) + { + extractFailureContent.ErrorCode = codeProperty.GetString(); + } + if (errorProperty.TryGetProperty("message", out var messageProperty)) + { + extractFailureContent.Message = messageProperty.GetString(); + } + } + + return extractFailureContent; + } + catch (Exception) + { + // Ignore any failures - unexpected content will be + // included verbatim in the detailed error message + } + + return null; + } + + private RequestFailedException CreateRequestFailedExceptionWithContent( + Response response, + string? content, + AzureError? details, + Exception? innerException) + { + var errorInformation = ExtractFailureContent(response, content); + + var message = details?.Message ?? errorInformation?.Message ?? DefaultFailureMessage; + var errorCode = details?.ErrorCode ?? errorInformation?.ErrorCode; + + IDictionary? data = null; + if (errorInformation?.Data != null) + { + if (details?.Data == null) + { + data = errorInformation.Data; + } + else + { + data = new Dictionary(details.Data); + foreach (var pair in errorInformation.Data) + { + data[pair.Key] = pair.Value; + } + } + } + + StringBuilder messageBuilder = new StringBuilder() + .AppendLine(message) + .Append("Status: ") + .Append(response.Status.ToString(CultureInfo.InvariantCulture)); + + if (!string.IsNullOrEmpty(response.ReasonPhrase)) + { + messageBuilder.Append(" (") + .Append(response.ReasonPhrase) + .AppendLine(")"); + } + else + { + messageBuilder.AppendLine(); + } + + if (!string.IsNullOrWhiteSpace(errorCode)) + { + messageBuilder.Append("ErrorCode: ") + .Append(errorCode) + .AppendLine(); + } + + if (data != null && data.Count > 0) + { + messageBuilder + .AppendLine() + .AppendLine("Additional Information:"); + foreach (KeyValuePair info in data) + { + messageBuilder + .Append(info.Key) + .Append(": ") + .Append(info.Value) + .AppendLine(); + } + } + + if (content != null) + { + messageBuilder + .AppendLine() + .AppendLine("Content:") + .AppendLine(content); + } + + messageBuilder + .AppendLine() + .AppendLine("Headers:"); + + foreach (HttpHeader responseHeader in response.Headers) + { + string headerValue = _sanitizer.SanitizeHeader(responseHeader.Name, responseHeader.Value); + messageBuilder.AppendLine($"{responseHeader.Name}: {headerValue}"); + } + + var exception = new RequestFailedException(response.Status, messageBuilder.ToString(), errorCode, innerException); + + if (data != null) + { + foreach (KeyValuePair keyValuePair in data) + { + exception.Data.Add(keyValuePair.Key, keyValuePair.Value); + } + } + + return exception; + } + + private static async ValueTask ReadContentAsync(Response response, bool async) + { + string? content = null; + + if (response.ContentStream != null && + ContentTypeUtilities.TryGetTextEncoding(response.Headers.ContentType, out var encoding)) + { + using (var streamReader = new StreamReader(response.ContentStream, encoding)) + { + content = async ? await streamReader.ReadToEndAsync().ConfigureAwait(false) : streamReader.ReadToEnd(); + } + } + + return content; + } } } From c62ec9bfc1869093b9ed8d1689291641803496c6 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Thu, 16 Sep 2021 10:19:46 -0700 Subject: [PATCH 04/25] remove edits to Core --- .../Azure.Core/src/Pipeline/AzureError.cs | 37 --- .../Azure.Core/src/Pipeline/HttpPipeline.cs | 5 +- sdk/core/Azure.Core/src/Response.cs | 47 ---- sdk/core/Azure.Core/src/ResponseClassifier.cs | 217 ------------------ 4 files changed, 1 insertion(+), 305 deletions(-) delete mode 100644 sdk/core/Azure.Core/src/Pipeline/AzureError.cs diff --git a/sdk/core/Azure.Core/src/Pipeline/AzureError.cs b/sdk/core/Azure.Core/src/Pipeline/AzureError.cs deleted file mode 100644 index 6a7a3b1519b74..0000000000000 --- a/sdk/core/Azure.Core/src/Pipeline/AzureError.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Text; - -namespace Azure.Core.Pipeline -{ - /// - /// - public class AzureError - { - /// - /// - /// - public AzureError() - { - Data = new Dictionary(); - } - - /// - /// Gets or sets the error message. - /// - public string? Message { get; set; } - - /// - /// Gets or sets the error code. - /// - public string? ErrorCode { get; set; } - - /// - /// Gets an additional data returned with the error response. - /// - public IDictionary Data { get; } - } -} diff --git a/sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs b/sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs index df7278ee6f3e5..c283475c0d86e 100644 --- a/sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs +++ b/sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs @@ -70,9 +70,7 @@ public ValueTask SendAsync(HttpMessage message, CancellationToken cancellationTo { message.CancellationToken = cancellationToken; AddHttpMessageProperties(message); - var value = _pipeline.Span[0].ProcessAsync(message, _pipeline.Slice(1)); - message.Response.EvaluateError(message); - return value; + return _pipeline.Span[0].ProcessAsync(message, _pipeline.Slice(1)); } /// @@ -85,7 +83,6 @@ public void Send(HttpMessage message, CancellationToken cancellationToken) message.CancellationToken = cancellationToken; AddHttpMessageProperties(message); _pipeline.Span[0].Process(message, _pipeline.Slice(1)); - message.Response.EvaluateError(message); } /// /// Invokes the pipeline asynchronously with the provided request. diff --git a/sdk/core/Azure.Core/src/Response.cs b/sdk/core/Azure.Core/src/Response.cs index 65ea569f9d17b..e31a6ec4752ad 100644 --- a/sdk/core/Azure.Core/src/Response.cs +++ b/sdk/core/Azure.Core/src/Response.cs @@ -5,9 +5,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Threading.Tasks; using Azure.Core; -using Azure.Core.Pipeline; namespace Azure { @@ -113,51 +111,6 @@ public virtual BinaryData Content /// The enumerating in the response. protected internal abstract IEnumerable EnumerateHeaders(); - internal bool? IsError { get; set; } - - internal ResponseClassifier? ResponseClassifier { get; set; } - - internal void EvaluateError(HttpMessage message) - { - if (!IsError.HasValue) - { - IsError = message.ResponseClassifier.IsErrorResponse(message); - ResponseClassifier = message.ResponseClassifier; - } - } - - /// - /// If the response is an error response, throw a RequestFailedException. - /// - public void ThrowIfError() - { - if (!IsError.HasValue) - { - throw new InvalidOperationException("IsError value should have been cached by the pipeline."); - } - - if (IsError.Value) - { - throw ResponseClassifier!.CreateRequestFailedException(this); - } - } - - /// - /// If the response is an error response, throw a RequestFailedException. - /// - public async Task ThrowIfErrorAsync() - { - if (!IsError.HasValue) - { - throw new InvalidOperationException("IsError value should have been cached by the pipeline."); - } - - if (IsError.Value) - { - throw await ResponseClassifier!.CreateRequestFailedExceptionAsync(this).ConfigureAwait(false); - } - } - /// /// Creates a new instance of with the provided value and HTTP response. /// diff --git a/sdk/core/Azure.Core/src/ResponseClassifier.cs b/sdk/core/Azure.Core/src/ResponseClassifier.cs index 54d6226975acc..e770e6e43271f 100644 --- a/sdk/core/Azure.Core/src/ResponseClassifier.cs +++ b/sdk/core/Azure.Core/src/ResponseClassifier.cs @@ -2,14 +2,7 @@ // Licensed under the MIT License. using System; -using System.Collections.Generic; -using System.Globalization; using System.IO; -using System.Linq; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; -using Azure.Core.Pipeline; namespace Azure.Core { @@ -18,30 +11,6 @@ namespace Azure.Core /// public class ResponseClassifier { - private const string DefaultFailureMessage = "Service request failed."; - - private readonly HttpMessageSanitizer _sanitizer; - - /// - /// Initializes a new instance of . - /// - public ResponseClassifier() : this(new DefaultClientOptions()) - { - } - - /// - /// - - /// - /// Initializes a new instance of . - /// - public ResponseClassifier(ClientOptions options) - { - _sanitizer = new HttpMessageSanitizer( - options?.Diagnostics.LoggedQueryParameters.ToArray() ?? Array.Empty(), - options?.Diagnostics.LoggedHeaderNames.ToArray() ?? Array.Empty()); - } - /// /// Specifies if the request contained in the should be retried. /// @@ -88,191 +57,5 @@ public virtual bool IsErrorResponse(HttpMessage message) var statusKind = message.Response.Status / 100; return statusKind == 4 || statusKind == 5; } - - /// - /// Creates an instance of for the provided failed . - /// - /// - /// - /// - /// - public virtual async ValueTask CreateRequestFailedExceptionAsync( - Response response, - AzureError? error = null, - Exception? innerException = null) - { - var content = await ReadContentAsync(response, true).ConfigureAwait(false); - return CreateRequestFailedExceptionWithContent(response, content, error, innerException); - } - - /// - /// Creates an instance of for the provided failed . - /// - /// - /// - /// - /// - public virtual RequestFailedException CreateRequestFailedException( - Response response, - AzureError? error = null, - Exception? innerException = null) - { - string? content = ReadContentAsync(response, false).EnsureCompleted(); - return CreateRequestFailedExceptionWithContent(response, content, error, innerException); - } - - /// - /// Partial method that can optionally be defined to extract the error - /// message, code, and details in a service specific manner. - /// - /// The response headers. - /// The extracted text content - protected virtual AzureError? ExtractFailureContent(Response response, string? textContent) - { - try - { - // Optimistic check for JSON object we expect - if (textContent == null || - !textContent.StartsWith("{", StringComparison.OrdinalIgnoreCase)) - return null; - - var extractFailureContent = new AzureError(); - - using JsonDocument document = JsonDocument.Parse(textContent); - if (document.RootElement.TryGetProperty("error", out var errorProperty)) - { - if (errorProperty.TryGetProperty("code", out var codeProperty)) - { - extractFailureContent.ErrorCode = codeProperty.GetString(); - } - if (errorProperty.TryGetProperty("message", out var messageProperty)) - { - extractFailureContent.Message = messageProperty.GetString(); - } - } - - return extractFailureContent; - } - catch (Exception) - { - // Ignore any failures - unexpected content will be - // included verbatim in the detailed error message - } - - return null; - } - - private RequestFailedException CreateRequestFailedExceptionWithContent( - Response response, - string? content, - AzureError? details, - Exception? innerException) - { - var errorInformation = ExtractFailureContent(response, content); - - var message = details?.Message ?? errorInformation?.Message ?? DefaultFailureMessage; - var errorCode = details?.ErrorCode ?? errorInformation?.ErrorCode; - - IDictionary? data = null; - if (errorInformation?.Data != null) - { - if (details?.Data == null) - { - data = errorInformation.Data; - } - else - { - data = new Dictionary(details.Data); - foreach (var pair in errorInformation.Data) - { - data[pair.Key] = pair.Value; - } - } - } - - StringBuilder messageBuilder = new StringBuilder() - .AppendLine(message) - .Append("Status: ") - .Append(response.Status.ToString(CultureInfo.InvariantCulture)); - - if (!string.IsNullOrEmpty(response.ReasonPhrase)) - { - messageBuilder.Append(" (") - .Append(response.ReasonPhrase) - .AppendLine(")"); - } - else - { - messageBuilder.AppendLine(); - } - - if (!string.IsNullOrWhiteSpace(errorCode)) - { - messageBuilder.Append("ErrorCode: ") - .Append(errorCode) - .AppendLine(); - } - - if (data != null && data.Count > 0) - { - messageBuilder - .AppendLine() - .AppendLine("Additional Information:"); - foreach (KeyValuePair info in data) - { - messageBuilder - .Append(info.Key) - .Append(": ") - .Append(info.Value) - .AppendLine(); - } - } - - if (content != null) - { - messageBuilder - .AppendLine() - .AppendLine("Content:") - .AppendLine(content); - } - - messageBuilder - .AppendLine() - .AppendLine("Headers:"); - - foreach (HttpHeader responseHeader in response.Headers) - { - string headerValue = _sanitizer.SanitizeHeader(responseHeader.Name, responseHeader.Value); - messageBuilder.AppendLine($"{responseHeader.Name}: {headerValue}"); - } - - var exception = new RequestFailedException(response.Status, messageBuilder.ToString(), errorCode, innerException); - - if (data != null) - { - foreach (KeyValuePair keyValuePair in data) - { - exception.Data.Add(keyValuePair.Key, keyValuePair.Value); - } - } - - return exception; - } - - private static async ValueTask ReadContentAsync(Response response, bool async) - { - string? content = null; - - if (response.ContentStream != null && - ContentTypeUtilities.TryGetTextEncoding(response.Headers.ContentType, out var encoding)) - { - using (var streamReader = new StreamReader(response.ContentStream, encoding)) - { - content = async ? await streamReader.ReadToEndAsync().ConfigureAwait(false) : streamReader.ReadToEnd(); - } - } - - return content; - } } } From ede089b9956762422063f08083ed74d828d6e053 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Thu, 16 Sep 2021 10:46:36 -0700 Subject: [PATCH 05/25] simplify tests --- .../Error.Serialization.cs | 35 ------------------- .../LowLevelClientModels/Error.cs | 32 ----------------- .../LowLevelClientModels/Pet.Serialization.cs | 2 -- .../LowLevelClientModels/Pet.cs | 9 +++-- .../tests/LowLevelClient/PetStoreClient.cs | 6 ++-- .../LowLevelClient/PetStoreClientOptions.cs | 2 -- .../tests/LowLevelClientTests.cs | 9 +++-- 7 files changed, 15 insertions(+), 80 deletions(-) delete mode 100644 sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Error.Serialization.cs delete mode 100644 sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Error.cs diff --git a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Error.Serialization.cs b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Error.Serialization.cs deleted file mode 100644 index 86a33e90f4e2a..0000000000000 --- a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Error.Serialization.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// - -#nullable disable - -using System.Text.Json; -using Azure.Core; - -namespace Azure.Core.Experimental.Tests.Models -{ - internal partial class Error - { - internal static Error DeserializeError(JsonElement element) - { - Optional message = default; - Optional code = default; - foreach (var property in element.EnumerateObject()) - { - if (property.NameEquals("message")) - { - message = property.Value.GetString(); - continue; - } - if (property.NameEquals("code")) - { - code = property.Value.GetString(); - continue; - } - } - return new Error(message.Value, code.Value); - } - } -} diff --git a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Error.cs b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Error.cs deleted file mode 100644 index fa70d6c9c7830..0000000000000 --- a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Error.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// - -#nullable disable - -namespace Azure.Core.Experimental.Tests.Models -{ - /// Error details. - internal partial class Error - { - /// Initializes a new instance of Error. - internal Error() - { - } - - /// Initializes a new instance of Error. - /// Error message. - /// Error code. - internal Error(string message, string code) - { - Message = message; - Code = code; - } - - /// Error message. - public string Message { get; } - /// Error code. - public string Code { get; } - } -} diff --git a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Pet.Serialization.cs b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Pet.Serialization.cs index 0cdf3621b5d83..80f7c8a9a7502 100644 --- a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Pet.Serialization.cs +++ b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Pet.Serialization.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -// - #nullable disable using System.Text.Json; diff --git a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Pet.cs b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Pet.cs index 0af76cfb55e7d..24062177781b4 100644 --- a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Pet.cs +++ b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Pet.cs @@ -1,13 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -// - #nullable disable +using System; + namespace Azure.Core.Experimental.Tests.Models { - /// The Pet. + /// The Pet output model. public partial class Pet { /// Initializes a new instance of Pet. @@ -29,5 +29,8 @@ internal Pet(int? id, string name, string species) public int? Id { get; } public string Name { get; } public string Species { get; } + + // Cast from Response to Pet + public static implicit operator Pet(Response r) => throw new NotImplementedException(); } } diff --git a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs index ca581865cf693..fdcbcc68229d7 100644 --- a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs +++ b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs @@ -21,7 +21,6 @@ public partial class PetStoreClient private Uri endpoint; private readonly string apiVersion; private readonly ClientDiagnostics _clientDiagnostics; - private readonly ResponseClassifier _responseClassifier; /// Initializes a new instance of PetStoreClient for mocking. protected PetStoreClient() @@ -45,7 +44,6 @@ public PetStoreClient(Uri endpoint, TokenCredential credential, PetStoreClientOp options ??= new PetStoreClientOptions(); _clientDiagnostics = new ClientDiagnostics(options); - _responseClassifier = new ResponseClassifier(options); _tokenCredential = credential; var authPolicy = new BearerTokenAuthenticationPolicy(_tokenCredential, AuthorizationScopes); Pipeline = HttpPipelineBuilder.Build(options, new HttpPipelinePolicy[] { new LowLevelCallbackPolicy() }, new HttpPipelinePolicy[] { authPolicy }, new ResponseClassifier()); @@ -75,7 +73,7 @@ public virtual async Task GetPetAsync(string id, RequestOptions option case 200: return message.Response; default: - throw await _responseClassifier.CreateRequestFailedExceptionAsync(message.Response).ConfigureAwait(false); + throw await _clientDiagnostics.CreateRequestFailedExceptionAsync(message.Response).ConfigureAwait(false); } } else @@ -112,7 +110,7 @@ public virtual Response GetPet(string id, RequestOptions options = null) case 200: return message.Response; default: - throw _responseClassifier.CreateRequestFailedException(message.Response); + throw _clientDiagnostics.CreateRequestFailedException(message.Response); } } else diff --git a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClientOptions.cs b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClientOptions.cs index 404e9dde7bb90..f400dd2b82a64 100644 --- a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClientOptions.cs +++ b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClientOptions.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -// - #nullable disable using System; diff --git a/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs b/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs index 1aed80513552a..a8cd3159c6da8 100644 --- a/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs +++ b/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs @@ -31,17 +31,22 @@ public void TestSetup() client = new PetStoreClient(_url, GetCredential()); } + //[Ignore("This test is not yet implemented.")] [Test] public async Task CanCallLlcGetMethodAsync() { + // This fails because there is no such service. + // We'll need to use the TestFramework's mock transport. Response response = await client.GetPetAsync("pet1", new RequestOptions()); } + //[Ignore("This test is not yet implemented.")] [Test] public async Task CanCallHlcGetMethodAsync() { - // This currently fails to build. - Response pet = await client.GetPetAsync("pet1"); + // This currently fails because cast operator is not implemented. + // We'll also need to use the TestFramework's mock transport here. + Pet pet = await client.GetPetAsync("pet1"); } } } From 2d390754655c7b3c49f661f8ce2fdd39c3bd9271 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Thu, 16 Sep 2021 14:24:31 -0700 Subject: [PATCH 06/25] implement LLC method with mock transport --- .../Azure.Core.Experimental.Tests.csproj | 1 + .../LowLevelClientModels/Pet.Serialization.cs | 28 +++++----- .../LowLevelClientModels/Pet.cs | 4 +- .../SerializationHelpers.cs | 22 ++++++++ .../tests/LowLevelClientTests.cs | 51 ++++++++++++++----- 5 files changed, 76 insertions(+), 30 deletions(-) create mode 100644 sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/SerializationHelpers.cs diff --git a/sdk/core/Azure.Core.Experimental/tests/Azure.Core.Experimental.Tests.csproj b/sdk/core/Azure.Core.Experimental/tests/Azure.Core.Experimental.Tests.csproj index 520e549bcefe1..6b97834af9b43 100644 --- a/sdk/core/Azure.Core.Experimental/tests/Azure.Core.Experimental.Tests.csproj +++ b/sdk/core/Azure.Core.Experimental/tests/Azure.Core.Experimental.Tests.csproj @@ -21,6 +21,7 @@ + diff --git a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Pet.Serialization.cs b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Pet.Serialization.cs index 80f7c8a9a7502..0f4335399d5ea 100644 --- a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Pet.Serialization.cs +++ b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Pet.Serialization.cs @@ -8,25 +8,27 @@ namespace Azure.Core.Experimental.Tests.Models { - public partial class Pet + public partial class Pet : IUtf8JsonSerializable { + void IUtf8JsonSerializable.Write(Utf8JsonWriter writer) + { + writer.WriteStartObject(); + + writer.WritePropertyName("name"); + writer.WriteStringValue(Name); + + writer.WritePropertyName("species"); + writer.WriteStringValue(Species); + + writer.WriteEndObject(); + } + internal static Pet DeserializePet(JsonElement element) { - Optional id = default; Optional name = default; Optional species = default; foreach (var property in element.EnumerateObject()) { - if (property.NameEquals("id")) - { - if (property.Value.ValueKind == JsonValueKind.Null) - { - property.ThrowNonNullablePropertyIsNull(); - continue; - } - id = property.Value.GetInt32(); - continue; - } if (property.NameEquals("name")) { name = property.Value.GetString(); @@ -38,7 +40,7 @@ internal static Pet DeserializePet(JsonElement element) continue; } } - return new Pet(Optional.ToNullable(id), name.Value, species.Value); + return new Pet(name.Value, species.Value); } } } diff --git a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Pet.cs b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Pet.cs index 24062177781b4..cc298ed1bd091 100644 --- a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Pet.cs +++ b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Pet.cs @@ -19,14 +19,12 @@ internal Pet() /// /// /// - internal Pet(int? id, string name, string species) + internal Pet(string name, string species) { - Id = id; Name = name; Species = species; } - public int? Id { get; } public string Name { get; } public string Species { get; } diff --git a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/SerializationHelpers.cs b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/SerializationHelpers.cs new file mode 100644 index 0000000000000..27c5033cccd43 --- /dev/null +++ b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/SerializationHelpers.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Text.Json; +using Azure.Core; + +namespace Azure.Core.Experimental.Tests.Models +{ + internal class SerializationHelpers + { + public delegate void SerializerFunc(ref Utf8JsonWriter writer, T t); + + public static byte[] Serialize(T t, SerializerFunc serializerFunc) + { + var writer = new ArrayBufferWriter(); + var json = new Utf8JsonWriter(writer); + serializerFunc(ref json, t); + json.Flush(); + return writer.WrittenMemory.ToArray(); + } + } +} diff --git a/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs b/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs index a8cd3159c6da8..9ad82b1e88c9f 100644 --- a/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs +++ b/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs @@ -2,42 +2,52 @@ // Licensed under the MIT License. using System; +using System.Text.Json; using System.Threading.Tasks; using Azure.Core.Experimental.Tests; using Azure.Core.Experimental.Tests.Models; +using Azure.Core.Pipeline; using Azure.Core.TestFramework; -using Azure.Identity; using NUnit.Framework; namespace Azure.Core.Tests { - public class LowLevelClientTests + public class LowLevelClientTests : ClientTestBase { - public LowLevelClientTests() + public LowLevelClientTests(bool isAsync) : base(isAsync) { } private PetStoreClient client { get; set; } private readonly Uri _url = new Uri("https://example.azurepetstore.com"); - private TokenCredential GetCredential() + public PetStoreClient CreateClient(HttpPipelineTransport transport) { - return new EnvironmentCredential(); - } + var options = new PetStoreClientOptions() + { + Transport = transport + }; - [SetUp] - public void TestSetup() - { - client = new PetStoreClient(_url, GetCredential()); + return new PetStoreClient(_url, new MockCredential(), options); } - //[Ignore("This test is not yet implemented.")] [Test] public async Task CanCallLlcGetMethodAsync() { - // This fails because there is no such service. - // We'll need to use the TestFramework's mock transport. - Response response = await client.GetPetAsync("pet1", new RequestOptions()); + var mockResponse = new MockResponse(200); + + Pet pet = new Pet("snoopy", "beagle"); + mockResponse.SetContent(SerializationHelpers.Serialize(pet, SerializePet)); + + var mockTransport = new MockTransport(mockResponse); + PetStoreClient client = CreateClient(mockTransport); + + Response response = await client.GetPetAsync("snoopy", new RequestOptions()); + var doc = JsonDocument.Parse(response.Content.ToMemory()); + + Assert.AreEqual(200, response.Status); + Assert.AreEqual("snoopy", doc.RootElement.GetProperty("name").GetString()); + Assert.AreEqual("beagle", doc.RootElement.GetProperty("species").GetString()); } //[Ignore("This test is not yet implemented.")] @@ -48,5 +58,18 @@ public async Task CanCallHlcGetMethodAsync() // We'll also need to use the TestFramework's mock transport here. Pet pet = await client.GetPetAsync("pet1"); } + + private void SerializePet(ref Utf8JsonWriter writer, Pet pet) + { + writer.WriteStartObject(); + + writer.WritePropertyName("name"); + writer.WriteStringValue(pet.Name); + + writer.WritePropertyName("species"); + writer.WriteStringValue(pet.Species); + + writer.WriteEndObject(); + } } } From bab3d6c55d6390fc1da7d6bd53516784080b6cb9 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Thu, 16 Sep 2021 14:59:51 -0700 Subject: [PATCH 07/25] Add in basic model cast functionality, without new Core features --- .../LowLevelClientModels/Pet.cs | 22 ++++++++++++- .../tests/LowLevelClientTests.cs | 33 ++++++++++++++++--- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Pet.cs b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Pet.cs index cc298ed1bd091..e4498142c357a 100644 --- a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Pet.cs +++ b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/LowLevelClientModels/Pet.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using System.Text.Json; namespace Azure.Core.Experimental.Tests.Models { @@ -29,6 +30,25 @@ internal Pet(string name, string species) public string Species { get; } // Cast from Response to Pet - public static implicit operator Pet(Response r) => throw new NotImplementedException(); + public static implicit operator Pet(Response response) + { + // [X] TODO: Add in HLC error semantics + // [ ] TODO: Use response.IsError + // [ ] TODO: Use throw new ResponseFailedException(response); + switch (response.Status) + { + case 200: + return DeserializePet(JsonDocument.Parse(response.Content.ToMemory())); + default: + throw new RequestFailedException("Received a non-success status code."); + } + } + + private static Pet DeserializePet(JsonDocument document) + { + return new Pet( + document.RootElement.GetProperty("name").GetString(), + document.RootElement.GetProperty("species").GetString()); + } } } diff --git a/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs b/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs index 9ad82b1e88c9f..e557529d67ca0 100644 --- a/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs +++ b/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs @@ -32,7 +32,7 @@ public PetStoreClient CreateClient(HttpPipelineTransport transport) } [Test] - public async Task CanCallLlcGetMethodAsync() + public async Task CanGetResponseFromLlcGetMethodAsync() { var mockResponse = new MockResponse(200); @@ -50,15 +50,36 @@ public async Task CanCallLlcGetMethodAsync() Assert.AreEqual("beagle", doc.RootElement.GetProperty("species").GetString()); } - //[Ignore("This test is not yet implemented.")] [Test] - public async Task CanCallHlcGetMethodAsync() + public async Task CanGetOutputModelOnSuccessCodeAsync() { - // This currently fails because cast operator is not implemented. - // We'll also need to use the TestFramework's mock transport here. + var mockResponse = new MockResponse(200); + + Pet petResponse = new Pet("snoopy", "beagle"); + mockResponse.SetContent(SerializationHelpers.Serialize(petResponse, SerializePet)); + + var mockTransport = new MockTransport(mockResponse); + PetStoreClient client = CreateClient(mockTransport); + Pet pet = await client.GetPetAsync("pet1"); + + Assert.AreEqual("snoopy", pet.Name); + Assert.AreEqual("beagle", pet.Species); } + [Test] + public void CannotGetOutputModelOnFailureCodeAsync() + { + var mockResponse = new MockResponse(404); + + var mockTransport = new MockTransport(mockResponse); + PetStoreClient client = CreateClient(mockTransport); + + Assert.ThrowsAsync(async () => await client.GetPetAsync("pet1")); + } + + #region Helpers + private void SerializePet(ref Utf8JsonWriter writer, Pet pet) { writer.WriteStartObject(); @@ -71,5 +92,7 @@ private void SerializePet(ref Utf8JsonWriter writer, Pet pet) writer.WriteEndObject(); } + + #endregion } } From 71181ebc1c51a3da5e96c29d77f1529632c5b732 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Thu, 16 Sep 2021 18:01:44 -0700 Subject: [PATCH 08/25] intial approach --- .../src/RequestOptions.cs | 16 ++++++++-------- .../tests/LowLevelClient/PetStoreClient.cs | 12 +++++++++++- .../tests/LowLevelClientTests.cs | 1 - .../tests/RequestOptionsTest.cs | 4 ++-- 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/sdk/core/Azure.Core.Experimental/src/RequestOptions.cs b/sdk/core/Azure.Core.Experimental/src/RequestOptions.cs index a529e58a12ae3..8a00899ae5a8e 100644 --- a/sdk/core/Azure.Core.Experimental/src/RequestOptions.cs +++ b/sdk/core/Azure.Core.Experimental/src/RequestOptions.cs @@ -162,7 +162,7 @@ public override bool IsRetriable(HttpMessage message, Exception exception) public override bool IsErrorResponse(HttpMessage message) { - if (Applies(message, ResponseClassification.Throw)) return true; + if (Applies(message, ResponseClassification.Error)) return true; if (Applies(message, ResponseClassification.Success)) return false; return _inner.IsErrorResponse(message); @@ -212,28 +212,28 @@ public override bool TryClassify(HttpMessage message, Exception? exception, out } /// - /// Specifies how response would be processed by the pipeline and the client. + /// Specifies how the response will be processed by the pipeline and the client. /// public enum ResponseClassification { /// - /// The response would be retried. + /// The response will be retried. /// Retry, /// - /// The response would be retried. + /// The response will not be retried. /// DontRetry, /// - /// The client would throw an exception for the response. + /// The client considers this response to be an error. /// - Throw, + Error, /// - /// The client would tread the response a successful. + /// The client considers this reponse to be a success. /// Success, } -} \ No newline at end of file +} diff --git a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs index fdcbcc68229d7..5554c21d2b077 100644 --- a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs +++ b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs @@ -59,6 +59,7 @@ public virtual async Task GetPetAsync(string id, RequestOptions option #pragma warning restore AZC0002 { options ??= new RequestOptions(); + using HttpMessage message = CreateGetPetRequest(id, options); RequestOptions.Apply(options, message); using var scope = _clientDiagnostics.CreateScope("PetStoreClient.GetPet"); @@ -95,7 +96,16 @@ public virtual async Task GetPetAsync(string id, RequestOptions option public virtual Response GetPet(string id, RequestOptions options = null) #pragma warning restore AZC0002 { - options ??= new RequestOptions(); + if (options == null) + { + // We're in a helper method, not an LLC method + options = new RequestOptions(); + options.AddClassifier(new[] { 200 }, ResponseClassification.Success); + options.AddClassifier(new[] { 404 }, ResponseClassification.Success); + + // TODO: Do we need to designate errors explicitly? + } + using HttpMessage message = CreateGetPetRequest(id, options); RequestOptions.Apply(options, message); using var scope = _clientDiagnostics.CreateScope("PetStoreClient.GetPet"); diff --git a/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs b/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs index e557529d67ca0..ab49f2b406e8a 100644 --- a/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs +++ b/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs @@ -18,7 +18,6 @@ public LowLevelClientTests(bool isAsync) : base(isAsync) { } - private PetStoreClient client { get; set; } private readonly Uri _url = new Uri("https://example.azurepetstore.com"); public PetStoreClient CreateClient(HttpPipelineTransport transport) diff --git a/sdk/core/Azure.Core.Experimental/tests/RequestOptionsTest.cs b/sdk/core/Azure.Core.Experimental/tests/RequestOptionsTest.cs index 51b96bc8c8b5a..0e849e7b41589 100644 --- a/sdk/core/Azure.Core.Experimental/tests/RequestOptionsTest.cs +++ b/sdk/core/Azure.Core.Experimental/tests/RequestOptionsTest.cs @@ -27,7 +27,7 @@ public void CanOverrideDefaultClassificationThrow() var m = CreateTestMessage(200); var options = new RequestOptions(); - options.AddClassifier(new[] { 200 }, ResponseClassification.Throw); + options.AddClassifier(new[] { 200 }, ResponseClassification.Error); RequestOptions.Apply(options, m); @@ -85,4 +85,4 @@ private static HttpMessage CreateTestMessage(int status) return m; } } -} \ No newline at end of file +} From d7108c9f00af0ab147f2b280a48a17c55800559a Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Fri, 17 Sep 2021 09:10:56 -0700 Subject: [PATCH 09/25] use ReqOpts to get default classifier functionality and do ro.Apply() --- .../tests/LowLevelClient/PetStoreClient.cs | 11 ++++++++++- .../tests/LowLevelClientTests.cs | 1 - 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs index fdcbcc68229d7..496157db5cef4 100644 --- a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs +++ b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs @@ -95,7 +95,16 @@ public virtual async Task GetPetAsync(string id, RequestOptions option public virtual Response GetPet(string id, RequestOptions options = null) #pragma warning restore AZC0002 { - options ??= new RequestOptions(); + if (options == null) + { + // We're in a helper method, not an LLC method + options = new RequestOptions(); + options.AddClassifier(new[] { 200 }, ResponseClassification.Success); + options.AddClassifier(new[] { 404 }, ResponseClassification.Success); + + // TODO: Do we need to designate errors explicitly? + } + using HttpMessage message = CreateGetPetRequest(id, options); RequestOptions.Apply(options, message); using var scope = _clientDiagnostics.CreateScope("PetStoreClient.GetPet"); diff --git a/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs b/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs index e557529d67ca0..ab49f2b406e8a 100644 --- a/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs +++ b/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs @@ -18,7 +18,6 @@ public LowLevelClientTests(bool isAsync) : base(isAsync) { } - private PetStoreClient client { get; set; } private readonly Uri _url = new Uri("https://example.azurepetstore.com"); public PetStoreClient CreateClient(HttpPipelineTransport transport) From b70c9d8f0318f0c9129b93e3c07ecf19f8f7102d Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Mon, 20 Sep 2021 08:59:50 -0700 Subject: [PATCH 10/25] simplify RequestOptions API; experiment with generating classifiers directly --- .../src/RequestOptions.cs | 159 +----------------- .../tests/LowLevelClient/PetStoreClient.cs | 39 ++++- .../tests/RequestOptionsTest.cs | 76 --------- 3 files changed, 41 insertions(+), 233 deletions(-) diff --git a/sdk/core/Azure.Core.Experimental/src/RequestOptions.cs b/sdk/core/Azure.Core.Experimental/src/RequestOptions.cs index 8a00899ae5a8e..a7e405529bf47 100644 --- a/sdk/core/Azure.Core.Experimental/src/RequestOptions.cs +++ b/sdk/core/Azure.Core.Experimental/src/RequestOptions.cs @@ -14,8 +14,6 @@ namespace Azure /// public class RequestOptions { - private List? _classifiers; - /// /// Initializes a new instance of the class. /// @@ -35,48 +33,6 @@ public RequestOptions() /// public RequestOptions(Action perCall) => PerCallPolicy = new ActionPolicy(perCall); - /// - /// Initializes a new instance of the class. - /// - /// The status codes to treat as successful. - public RequestOptions(params int[] treatAsSuccess) : this(treatAsSuccess, ResponseClassification.Success) - { - } - - /// - /// Initializes a new instance of the class. - /// Applying provided classification to a set of status codes. - /// - /// The status codes to classify. - /// The classification. - public RequestOptions(int[] statusCodes, ResponseClassification classification) - { - AddClassifier(statusCodes, classification); - } - - /// - /// Adds the classification for provided status codes. - /// - /// The status codes to classify. - /// The classification. - public void AddClassifier(int[] statusCodes, ResponseClassification classification) - { - foreach (var statusCode in statusCodes) - { - AddClassifier(message => message.Response.Status == statusCode ? classification : null); - } - } - - /// - /// Adds a function that allows to specify how response would be processed by the pipeline. - /// - /// - public void AddClassifier(Func classifier) - { - _classifiers ??= new(); - _classifiers.Add(new FuncHttpMessageClassifier(classifier)); - } - /// /// Initializes a new instance of the class using the given . /// @@ -106,14 +62,14 @@ public void AddClassifier(Func classifier) /// public static void Apply(RequestOptions requestOptions, HttpMessage message) { - if (requestOptions.PerCallPolicy != null) + if (requestOptions == null) { - message.SetProperty("RequestOptionsPerCallPolicyCallback", requestOptions.PerCallPolicy); + return; } - if (requestOptions._classifiers != null) + if (requestOptions.PerCallPolicy != null) { - message.ResponseClassifier = new PerCallResponseClassifier(message.ResponseClassifier, requestOptions._classifiers); + message.SetProperty("RequestOptionsPerCallPolicyCallback", requestOptions.PerCallPolicy); } } @@ -128,112 +84,5 @@ internal class ActionPolicy : HttpPipelineSynchronousPolicy public override void OnSendingRequest(HttpMessage message) => Action.Invoke(message); } - - private class PerCallResponseClassifier : ResponseClassifier - { - private readonly ResponseClassifier _inner; - private readonly List _classifiers; - - public PerCallResponseClassifier(ResponseClassifier inner, List classifiers) - { - _inner = inner; - _classifiers = classifiers; - } - - public override bool IsRetriableResponse(HttpMessage message) - { - if (Applies(message, ResponseClassification.DontRetry)) return false; - if (Applies(message, ResponseClassification.Retry)) return true; - - return _inner.IsRetriableResponse(message); - } - - public override bool IsRetriableException(Exception exception) - { - return _inner.IsRetriableException(exception); - } - - public override bool IsRetriable(HttpMessage message, Exception exception) - { - if (Applies(message, ResponseClassification.DontRetry)) return false; - - return _inner.IsRetriable(message, exception); - } - - public override bool IsErrorResponse(HttpMessage message) - { - if (Applies(message, ResponseClassification.Error)) return true; - if (Applies(message, ResponseClassification.Success)) return false; - - return _inner.IsErrorResponse(message); - } - - private bool Applies(HttpMessage message, ResponseClassification responseClassification) - { - foreach (var classifier in _classifiers) - { - if (classifier.TryClassify(message, null, out var c) && - c == responseClassification) - { - return true; - } - } - - return false; - } - } - - private abstract class HttpMessageClassifier - { - public abstract bool TryClassify(HttpMessage message, Exception? exception, out ResponseClassification classification); - } - - private class FuncHttpMessageClassifier : HttpMessageClassifier - { - private readonly Func _func; - - public FuncHttpMessageClassifier(Func func) - { - _func = func; - } - - public override bool TryClassify(HttpMessage message, Exception? exception, out ResponseClassification classification) - { - if (_func(message) is ResponseClassification c) - { - classification = c; - return true; - } - - classification = default; - return false; - } - } - } - - /// - /// Specifies how the response will be processed by the pipeline and the client. - /// - public enum ResponseClassification - { - /// - /// The response will be retried. - /// - Retry, - - /// - /// The response will not be retried. - /// - DontRetry, - - /// - /// The client considers this response to be an error. - /// - Error, - - /// - /// The client considers this reponse to be a success. - /// - Success, } } diff --git a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs index 496157db5cef4..dbf86a5403f68 100644 --- a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs +++ b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using System.Collections.Generic; using System.Threading.Tasks; using Azure; using Azure.Core; @@ -21,6 +22,7 @@ public partial class PetStoreClient private Uri endpoint; private readonly string apiVersion; private readonly ClientDiagnostics _clientDiagnostics; + private Dictionary _responseClassifierCache; /// Initializes a new instance of PetStoreClient for mocking. protected PetStoreClient() @@ -49,6 +51,18 @@ public PetStoreClient(Uri endpoint, TokenCredential credential, PetStoreClientOp Pipeline = HttpPipelineBuilder.Build(options, new HttpPipelinePolicy[] { new LowLevelCallbackPolicy() }, new HttpPipelinePolicy[] { authPolicy }, new ResponseClassifier()); this.endpoint = endpoint; apiVersion = options.Version; + + _responseClassifierCache = CreateResponseClassifierCache(); + } + + private Dictionary CreateResponseClassifierCache() + { + Dictionary cache = new(); + + // Add a RequestOptions per method + cache["GetPet"] = new GetPetResponseClassifier(_clientDiagnostics); + + return cache; } /// Get a pet by its Id. @@ -99,8 +113,8 @@ public virtual Response GetPet(string id, RequestOptions options = null) { // We're in a helper method, not an LLC method options = new RequestOptions(); - options.AddClassifier(new[] { 200 }, ResponseClassification.Success); - options.AddClassifier(new[] { 404 }, ResponseClassification.Success); + //options.AddClassifier(new[] { 200 }, ResponseClassification.Success); + //options.AddClassifier(new[] { 404 }, ResponseClassification.Success); // TODO: Do we need to designate errors explicitly? } @@ -150,5 +164,26 @@ private HttpMessage CreateGetPetRequest(string id, RequestOptions options = null request.Headers.Add("Accept", "application/json, text/json"); return message; } + + private class GetPetResponseClassifier : ResponseClassifier + { + private ClientDiagnostics _clientDiagnostics; + + public GetPetResponseClassifier(ClientDiagnostics clientDiagnostics) + { + _clientDiagnostics = clientDiagnostics; + } + + public override bool IsErrorResponse(HttpMessage message) + { + switch (message.Response.Status) + { + case 200: + return false; + default: + throw _clientDiagnostics.CreateRequestFailedException(message.Response); + } + } + } } } diff --git a/sdk/core/Azure.Core.Experimental/tests/RequestOptionsTest.cs b/sdk/core/Azure.Core.Experimental/tests/RequestOptionsTest.cs index 0e849e7b41589..082d0074ef615 100644 --- a/sdk/core/Azure.Core.Experimental/tests/RequestOptionsTest.cs +++ b/sdk/core/Azure.Core.Experimental/tests/RequestOptionsTest.cs @@ -8,81 +8,5 @@ namespace Azure.Core.Tests { public class RequestOptionsTest { - [Test] - public void CanOverrideDefaultClassificationSuccess() - { - var m = CreateTestMessage(404); - - var options = new RequestOptions(); - options.AddClassifier(new[] { 404 }, ResponseClassification.Success); - - RequestOptions.Apply(options, m); - - Assert.False(m.ResponseClassifier.IsErrorResponse(m)); - } - - [Test] - public void CanOverrideDefaultClassificationThrow() - { - var m = CreateTestMessage(200); - - var options = new RequestOptions(); - options.AddClassifier(new[] { 200 }, ResponseClassification.Error); - - RequestOptions.Apply(options, m); - - Assert.True(m.ResponseClassifier.IsErrorResponse(m)); - } - - [Test] - public void CanOverrideDefaultClassificationRetry() - { - var m = CreateTestMessage(200); - - var options = new RequestOptions(); - options.AddClassifier(new[] { 200 }, ResponseClassification.Retry); - - RequestOptions.Apply(options, m); - - Assert.True(m.ResponseClassifier.IsRetriableResponse(m)); - } - - [Test] - public void CanOverrideDefaultClassificationNoRetry() - { - var m = CreateTestMessage(500); - - var options = new RequestOptions(); - options.AddClassifier(new[] { 500 }, ResponseClassification.DontRetry); - - RequestOptions.Apply(options, m); - - Assert.False(m.ResponseClassifier.IsRetriableResponse(m)); - } - - [Test] - public void CanOverrideDefaultClassificationWithFunc() - { - HttpMessage m = new HttpMessage(new MockRequest(), new ResponseClassifier()) - { - Response = new MockResponse(500) - }; - - var options = new RequestOptions(); - options.AddClassifier(_ => ResponseClassification.Success); - - RequestOptions.Apply(options, m); - - Assert.False(m.ResponseClassifier.IsErrorResponse(m)); - } - - private static HttpMessage CreateTestMessage(int status) - { - HttpMessage m = new HttpMessage(new MockRequest(), new ResponseClassifier()) - { - Response = new MockResponse(status) - }; - return m; - } } } From 4636282b922423bf1ad027fd1f06fb12a54ce6b0 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Mon, 20 Sep 2021 10:55:51 -0700 Subject: [PATCH 11/25] update statusoptions value names and add tests --- .../src/RequestOptions.cs | 2 +- .../src/ResponseStatusOption.cs | 4 +- .../tests/LowLevelClient/PetStoreClient.cs | 67 +++++++++---------- .../tests/LowLevelClientTests.cs | 61 +++++++++++++++++ 4 files changed, 97 insertions(+), 37 deletions(-) diff --git a/sdk/core/Azure.Core.Experimental/src/RequestOptions.cs b/sdk/core/Azure.Core.Experimental/src/RequestOptions.cs index a7e405529bf47..bade58a4c90a0 100644 --- a/sdk/core/Azure.Core.Experimental/src/RequestOptions.cs +++ b/sdk/core/Azure.Core.Experimental/src/RequestOptions.cs @@ -47,7 +47,7 @@ public RequestOptions() /// /// Controls under what conditions the operation raises an exception if the underlying response indicates a failure. /// - public ResponseStatusOption StatusOption { get; set; } = ResponseStatusOption.Default; + public ResponseStatusOption StatusOption { get; set; } = ResponseStatusOption.ThrowOnError; /// /// A to use as part of this operation. This policy will be applied at the start diff --git a/sdk/core/Azure.Core.Experimental/src/ResponseStatusOption.cs b/sdk/core/Azure.Core.Experimental/src/ResponseStatusOption.cs index ebdedfd231aac..3a5a3f2aeef9e 100644 --- a/sdk/core/Azure.Core.Experimental/src/ResponseStatusOption.cs +++ b/sdk/core/Azure.Core.Experimental/src/ResponseStatusOption.cs @@ -11,10 +11,10 @@ public enum ResponseStatusOption /// /// Indicates that an operation should throw an exception when the response indicates a failure. /// - Default = 0, + ThrowOnError = 0, /// /// Indicates that an operation should not throw an exception when the response indicates a failure. /// - NoThrow = 1, + SuppressExceptions = 1, } } diff --git a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs index dbf86a5403f68..45ea35dd7bdea 100644 --- a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs +++ b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs @@ -62,6 +62,10 @@ private Dictionary CreateResponseClassifierCache() // Add a RequestOptions per method cache["GetPet"] = new GetPetResponseClassifier(_clientDiagnostics); + // Code generator will not generate new classifier types for classifiers + // with duplicate status code logic, but will reuse previously-generated + // ones. It will need to identify duplicates. + return cache; } @@ -72,27 +76,28 @@ private Dictionary CreateResponseClassifierCache() public virtual async Task GetPetAsync(string id, RequestOptions options = null) #pragma warning restore AZC0002 { - options ??= new RequestOptions(); using HttpMessage message = CreateGetPetRequest(id, options); RequestOptions.Apply(options, message); using var scope = _clientDiagnostics.CreateScope("PetStoreClient.GetPet"); scope.Start(); try { - await Pipeline.SendAsync(message, options.CancellationToken).ConfigureAwait(false); - if (options.StatusOption == ResponseStatusOption.Default) + await Pipeline.SendAsync(message, options?.CancellationToken).ConfigureAwait(false); + + if (options.StatusOption == ResponseStatusOption.SuppressExceptions) { - switch (message.Response.Status) - { - case 200: - return message.Response; - default: - throw await _clientDiagnostics.CreateRequestFailedExceptionAsync(message.Response).ConfigureAwait(false); - } + return message.Response; } - else + else // options.StatusOption == ResponseStatusOption.ThrowOnError { - return message.Response; + if (!message.ResponseClassifier.IsErrorResponse(message)) + { + return message.Response; + } + else + { + throw await _clientDiagnostics.CreateRequestFailedExceptionAsync(message.Response).ConfigureAwait(false); + } } } catch (Exception e) @@ -109,36 +114,29 @@ public virtual async Task GetPetAsync(string id, RequestOptions option public virtual Response GetPet(string id, RequestOptions options = null) #pragma warning restore AZC0002 { - if (options == null) - { - // We're in a helper method, not an LLC method - options = new RequestOptions(); - //options.AddClassifier(new[] { 200 }, ResponseClassification.Success); - //options.AddClassifier(new[] { 404 }, ResponseClassification.Success); - - // TODO: Do we need to designate errors explicitly? - } - using HttpMessage message = CreateGetPetRequest(id, options); RequestOptions.Apply(options, message); using var scope = _clientDiagnostics.CreateScope("PetStoreClient.GetPet"); scope.Start(); try { - Pipeline.Send(message, options.CancellationToken); - if (options.StatusOption == ResponseStatusOption.Default) + Pipeline.Send(message, options?.CancellationToken); + + if (options.StatusOption == ResponseStatusOption.SuppressExceptions) { - switch (message.Response.Status) - { - case 200: - return message.Response; - default: - throw _clientDiagnostics.CreateRequestFailedException(message.Response); - } + return message.Response; } - else + else // options.StatusOption == ResponseStatusOption.ThrowOnError { - return message.Response; + // This will change to message.Response.IsError in a later PR + if (!message.ResponseClassifier.IsErrorResponse(message)) + { + return message.Response; + } + else + { + throw _clientDiagnostics.CreateRequestFailedException(message.Response); + } } } catch (Exception e) @@ -162,6 +160,7 @@ private HttpMessage CreateGetPetRequest(string id, RequestOptions options = null uri.AppendPath(id, true); request.Uri = uri; request.Headers.Add("Accept", "application/json, text/json"); + message.ResponseClassifier = _responseClassifierCache["GetPet"]; return message; } @@ -181,7 +180,7 @@ public override bool IsErrorResponse(HttpMessage message) case 200: return false; default: - throw _clientDiagnostics.CreateRequestFailedException(message.Response); + return true; } } } diff --git a/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs b/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs index ab49f2b406e8a..325949bc3d754 100644 --- a/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs +++ b/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs @@ -92,6 +92,67 @@ private void SerializePet(ref Utf8JsonWriter writer, Pet pet) writer.WriteEndObject(); } + [Test] + public void CanSuppressExceptions() + { + var mockResponse = new MockResponse(404); + + var mockTransport = new MockTransport(mockResponse); + PetStoreClient client = CreateClient(mockTransport); + + RequestOptions options = new RequestOptions() + { + StatusOption = ResponseStatusOption.SuppressExceptions + }; + + Response response = default; + Assert.DoesNotThrowAsync(async () => + { + response = await client.GetPetAsync("snoopy", options); + }); + + Assert.AreEqual(404, response.Status); + } + + [Test] + public async Task ThrowOnErrorDoesntThrowOnSuccess() + { + var mockResponse = new MockResponse(200); + + Pet pet = new Pet("snoopy", "beagle"); + mockResponse.SetContent(SerializationHelpers.Serialize(pet, SerializePet)); + + var mockTransport = new MockTransport(mockResponse); + PetStoreClient client = CreateClient(mockTransport); + + Response response = await client.GetPetAsync("snoopy", new RequestOptions() + { + StatusOption = ResponseStatusOption.ThrowOnError + }); + var doc = JsonDocument.Parse(response.Content.ToMemory()); + + Assert.AreEqual(200, response.Status); + Assert.AreEqual("snoopy", doc.RootElement.GetProperty("name").GetString()); + Assert.AreEqual("beagle", doc.RootElement.GetProperty("species").GetString()); + } + + [Test] + public void ThrowOnErrorThrowsOnError() + { + var mockResponse = new MockResponse(404); + + var mockTransport = new MockTransport(mockResponse); + PetStoreClient client = CreateClient(mockTransport); + + Assert.ThrowsAsync(async () => + { + await client.GetPetAsync("snoopy", new RequestOptions() + { + StatusOption = ResponseStatusOption.ThrowOnError + }); + }); + } + #endregion } } From d1744929851f8f5e5c557ccd7127ff8b963734a3 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Mon, 20 Sep 2021 11:15:14 -0700 Subject: [PATCH 12/25] handle null options --- .../tests/LowLevelClient/PetStoreClient.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs index 45ea35dd7bdea..864dafe6cd04d 100644 --- a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs +++ b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs @@ -82,13 +82,14 @@ public virtual async Task GetPetAsync(string id, RequestOptions option scope.Start(); try { - await Pipeline.SendAsync(message, options?.CancellationToken).ConfigureAwait(false); + await Pipeline.SendAsync(message, options?.CancellationToken ?? default).ConfigureAwait(false); + var statusOption = options?.StatusOption ?? ResponseStatusOption.ThrowOnError; - if (options.StatusOption == ResponseStatusOption.SuppressExceptions) + if (statusOption == ResponseStatusOption.SuppressExceptions) { return message.Response; } - else // options.StatusOption == ResponseStatusOption.ThrowOnError + else { if (!message.ResponseClassifier.IsErrorResponse(message)) { @@ -120,13 +121,14 @@ public virtual Response GetPet(string id, RequestOptions options = null) scope.Start(); try { - Pipeline.Send(message, options?.CancellationToken); + Pipeline.Send(message, options?.CancellationToken ?? default); + var statusOption = options?.StatusOption ?? ResponseStatusOption.ThrowOnError; - if (options.StatusOption == ResponseStatusOption.SuppressExceptions) + if (statusOption == ResponseStatusOption.SuppressExceptions) { return message.Response; } - else // options.StatusOption == ResponseStatusOption.ThrowOnError + else { // This will change to message.Response.IsError in a later PR if (!message.ResponseClassifier.IsErrorResponse(message)) From a5487f6071e94616280c48dedcf142dda157e77c Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Mon, 20 Sep 2021 11:31:32 -0700 Subject: [PATCH 13/25] update api listing --- .../api/Azure.Core.Experimental.netstandard2.0.cs | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs index a95a62e7982d1..224698efbaa67 100644 --- a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs +++ b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs @@ -5,27 +5,16 @@ public partial class RequestOptions public RequestOptions() { } public RequestOptions(Azure.ResponseStatusOption statusOption) { } public RequestOptions(System.Action perCall) { } - public RequestOptions(params int[] treatAsSuccess) { } - public RequestOptions(int[] statusCodes, Azure.ResponseClassification classification) { } public System.Threading.CancellationToken CancellationToken { get { throw null; } set { } } public Azure.Core.Pipeline.HttpPipelinePolicy? PerCallPolicy { get { throw null; } set { } } public Azure.ResponseStatusOption StatusOption { get { throw null; } set { } } - public void AddClassifier(System.Func classifier) { } - public void AddClassifier(int[] statusCodes, Azure.ResponseClassification classification) { } public static void Apply(Azure.RequestOptions requestOptions, Azure.Core.HttpMessage message) { } public static implicit operator Azure.RequestOptions (Azure.ResponseStatusOption option) { throw null; } } - public enum ResponseClassification - { - Retry = 0, - DontRetry = 1, - Throw = 2, - Success = 3, - } public enum ResponseStatusOption { - Default = 0, - NoThrow = 1, + ThrowOnError = 0, + SuppressExceptions = 1, } } namespace Azure.Core From 347b16c06a189a12efec9d0ea349e6f3c506828b Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Mon, 20 Sep 2021 12:01:58 -0700 Subject: [PATCH 14/25] add IsError to PipelineResponse --- sdk/core/Azure.Core/src/Pipeline/HttpClientTransport.cs | 5 +++-- sdk/core/Azure.Core/src/Response.cs | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/sdk/core/Azure.Core/src/Pipeline/HttpClientTransport.cs b/sdk/core/Azure.Core/src/Pipeline/HttpClientTransport.cs index cbf592b907773..5b82b2e3634fa 100644 --- a/sdk/core/Azure.Core/src/Pipeline/HttpClientTransport.cs +++ b/sdk/core/Azure.Core/src/Pipeline/HttpClientTransport.cs @@ -129,7 +129,7 @@ private async ValueTask ProcessAsync(HttpMessage message, bool async) throw new RequestFailedException(e.Message, e); } - message.Response = new PipelineResponse(message.Request.ClientRequestId, responseMessage, contentStream); + message.Response = new PipelineResponse(message.Request.ClientRequestId, responseMessage, contentStream, message); } private static HttpClient CreateDefaultClient() @@ -500,12 +500,13 @@ private sealed class PipelineResponse : Response private Stream? _contentStream; #pragma warning restore CA2213 - public PipelineResponse(string requestId, HttpResponseMessage responseMessage, Stream? contentStream) + public PipelineResponse(string requestId, HttpResponseMessage responseMessage, Stream? contentStream, HttpMessage message) { ClientRequestId = requestId ?? throw new ArgumentNullException(nameof(requestId)); _responseMessage = responseMessage ?? throw new ArgumentNullException(nameof(responseMessage)); _contentStream = contentStream; _responseContent = _responseMessage.Content; + IsError = message.ResponseClassifier.IsErrorResponse(message); } public override int Status => (int)_responseMessage.StatusCode; diff --git a/sdk/core/Azure.Core/src/Response.cs b/sdk/core/Azure.Core/src/Response.cs index e31a6ec4752ad..20740d6e0acaf 100644 --- a/sdk/core/Azure.Core/src/Response.cs +++ b/sdk/core/Azure.Core/src/Response.cs @@ -111,6 +111,11 @@ public virtual BinaryData Content /// The enumerating in the response. protected internal abstract IEnumerable EnumerateHeaders(); + /// + /// Indicates whether this response is an error, according to the REST API. + /// + public bool IsError { get; protected set; } + /// /// Creates a new instance of with the provided value and HTTP response. /// From 243a191283cc18e12e7f40f8136062a0ddc76daf Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Mon, 20 Sep 2021 14:20:53 -0700 Subject: [PATCH 15/25] move logic to pipeline --- sdk/core/Azure.Core/src/Pipeline/HttpClientTransport.cs | 5 ++--- sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs | 5 ++++- sdk/core/Azure.Core/src/Response.cs | 5 +++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/sdk/core/Azure.Core/src/Pipeline/HttpClientTransport.cs b/sdk/core/Azure.Core/src/Pipeline/HttpClientTransport.cs index 5b82b2e3634fa..cbf592b907773 100644 --- a/sdk/core/Azure.Core/src/Pipeline/HttpClientTransport.cs +++ b/sdk/core/Azure.Core/src/Pipeline/HttpClientTransport.cs @@ -129,7 +129,7 @@ private async ValueTask ProcessAsync(HttpMessage message, bool async) throw new RequestFailedException(e.Message, e); } - message.Response = new PipelineResponse(message.Request.ClientRequestId, responseMessage, contentStream, message); + message.Response = new PipelineResponse(message.Request.ClientRequestId, responseMessage, contentStream); } private static HttpClient CreateDefaultClient() @@ -500,13 +500,12 @@ private sealed class PipelineResponse : Response private Stream? _contentStream; #pragma warning restore CA2213 - public PipelineResponse(string requestId, HttpResponseMessage responseMessage, Stream? contentStream, HttpMessage message) + public PipelineResponse(string requestId, HttpResponseMessage responseMessage, Stream? contentStream) { ClientRequestId = requestId ?? throw new ArgumentNullException(nameof(requestId)); _responseMessage = responseMessage ?? throw new ArgumentNullException(nameof(responseMessage)); _contentStream = contentStream; _responseContent = _responseMessage.Content; - IsError = message.ResponseClassifier.IsErrorResponse(message); } public override int Status => (int)_responseMessage.StatusCode; diff --git a/sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs b/sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs index c283475c0d86e..df7278ee6f3e5 100644 --- a/sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs +++ b/sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs @@ -70,7 +70,9 @@ public ValueTask SendAsync(HttpMessage message, CancellationToken cancellationTo { message.CancellationToken = cancellationToken; AddHttpMessageProperties(message); - return _pipeline.Span[0].ProcessAsync(message, _pipeline.Slice(1)); + var value = _pipeline.Span[0].ProcessAsync(message, _pipeline.Slice(1)); + message.Response.EvaluateError(message); + return value; } /// @@ -83,6 +85,7 @@ public void Send(HttpMessage message, CancellationToken cancellationToken) message.CancellationToken = cancellationToken; AddHttpMessageProperties(message); _pipeline.Span[0].Process(message, _pipeline.Slice(1)); + message.Response.EvaluateError(message); } /// /// Invokes the pipeline asynchronously with the provided request. diff --git a/sdk/core/Azure.Core/src/Response.cs b/sdk/core/Azure.Core/src/Response.cs index 20740d6e0acaf..dbbcbd992dc42 100644 --- a/sdk/core/Azure.Core/src/Response.cs +++ b/sdk/core/Azure.Core/src/Response.cs @@ -116,6 +116,11 @@ public virtual BinaryData Content /// public bool IsError { get; protected set; } + internal void EvaluateError(HttpMessage message) + { + IsError = message.ResponseClassifier.IsErrorResponse(message); + } + /// /// Creates a new instance of with the provided value and HTTP response. /// From 9fcedc4f4c79323afd704f77907adf7f7b2bcf6c Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Mon, 20 Sep 2021 14:25:48 -0700 Subject: [PATCH 16/25] undo changes to experimental --- .../Azure.Core.Experimental.netstandard2.0.cs | 15 +- .../src/RequestOptions.cs | 161 +++++++++++++++++- .../src/ResponseStatusOption.cs | 4 +- .../tests/LowLevelClient/PetStoreClient.cs | 98 ++++------- .../tests/LowLevelClientTests.cs | 61 ------- .../tests/RequestOptionsTest.cs | 76 +++++++++ 6 files changed, 278 insertions(+), 137 deletions(-) diff --git a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs index 224698efbaa67..a95a62e7982d1 100644 --- a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs +++ b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs @@ -5,16 +5,27 @@ public partial class RequestOptions public RequestOptions() { } public RequestOptions(Azure.ResponseStatusOption statusOption) { } public RequestOptions(System.Action perCall) { } + public RequestOptions(params int[] treatAsSuccess) { } + public RequestOptions(int[] statusCodes, Azure.ResponseClassification classification) { } public System.Threading.CancellationToken CancellationToken { get { throw null; } set { } } public Azure.Core.Pipeline.HttpPipelinePolicy? PerCallPolicy { get { throw null; } set { } } public Azure.ResponseStatusOption StatusOption { get { throw null; } set { } } + public void AddClassifier(System.Func classifier) { } + public void AddClassifier(int[] statusCodes, Azure.ResponseClassification classification) { } public static void Apply(Azure.RequestOptions requestOptions, Azure.Core.HttpMessage message) { } public static implicit operator Azure.RequestOptions (Azure.ResponseStatusOption option) { throw null; } } + public enum ResponseClassification + { + Retry = 0, + DontRetry = 1, + Throw = 2, + Success = 3, + } public enum ResponseStatusOption { - ThrowOnError = 0, - SuppressExceptions = 1, + Default = 0, + NoThrow = 1, } } namespace Azure.Core diff --git a/sdk/core/Azure.Core.Experimental/src/RequestOptions.cs b/sdk/core/Azure.Core.Experimental/src/RequestOptions.cs index bade58a4c90a0..8a00899ae5a8e 100644 --- a/sdk/core/Azure.Core.Experimental/src/RequestOptions.cs +++ b/sdk/core/Azure.Core.Experimental/src/RequestOptions.cs @@ -14,6 +14,8 @@ namespace Azure /// public class RequestOptions { + private List? _classifiers; + /// /// Initializes a new instance of the class. /// @@ -33,6 +35,48 @@ public RequestOptions() /// public RequestOptions(Action perCall) => PerCallPolicy = new ActionPolicy(perCall); + /// + /// Initializes a new instance of the class. + /// + /// The status codes to treat as successful. + public RequestOptions(params int[] treatAsSuccess) : this(treatAsSuccess, ResponseClassification.Success) + { + } + + /// + /// Initializes a new instance of the class. + /// Applying provided classification to a set of status codes. + /// + /// The status codes to classify. + /// The classification. + public RequestOptions(int[] statusCodes, ResponseClassification classification) + { + AddClassifier(statusCodes, classification); + } + + /// + /// Adds the classification for provided status codes. + /// + /// The status codes to classify. + /// The classification. + public void AddClassifier(int[] statusCodes, ResponseClassification classification) + { + foreach (var statusCode in statusCodes) + { + AddClassifier(message => message.Response.Status == statusCode ? classification : null); + } + } + + /// + /// Adds a function that allows to specify how response would be processed by the pipeline. + /// + /// + public void AddClassifier(Func classifier) + { + _classifiers ??= new(); + _classifiers.Add(new FuncHttpMessageClassifier(classifier)); + } + /// /// Initializes a new instance of the class using the given . /// @@ -47,7 +91,7 @@ public RequestOptions() /// /// Controls under what conditions the operation raises an exception if the underlying response indicates a failure. /// - public ResponseStatusOption StatusOption { get; set; } = ResponseStatusOption.ThrowOnError; + public ResponseStatusOption StatusOption { get; set; } = ResponseStatusOption.Default; /// /// A to use as part of this operation. This policy will be applied at the start @@ -62,14 +106,14 @@ public RequestOptions() /// public static void Apply(RequestOptions requestOptions, HttpMessage message) { - if (requestOptions == null) + if (requestOptions.PerCallPolicy != null) { - return; + message.SetProperty("RequestOptionsPerCallPolicyCallback", requestOptions.PerCallPolicy); } - if (requestOptions.PerCallPolicy != null) + if (requestOptions._classifiers != null) { - message.SetProperty("RequestOptionsPerCallPolicyCallback", requestOptions.PerCallPolicy); + message.ResponseClassifier = new PerCallResponseClassifier(message.ResponseClassifier, requestOptions._classifiers); } } @@ -84,5 +128,112 @@ internal class ActionPolicy : HttpPipelineSynchronousPolicy public override void OnSendingRequest(HttpMessage message) => Action.Invoke(message); } + + private class PerCallResponseClassifier : ResponseClassifier + { + private readonly ResponseClassifier _inner; + private readonly List _classifiers; + + public PerCallResponseClassifier(ResponseClassifier inner, List classifiers) + { + _inner = inner; + _classifiers = classifiers; + } + + public override bool IsRetriableResponse(HttpMessage message) + { + if (Applies(message, ResponseClassification.DontRetry)) return false; + if (Applies(message, ResponseClassification.Retry)) return true; + + return _inner.IsRetriableResponse(message); + } + + public override bool IsRetriableException(Exception exception) + { + return _inner.IsRetriableException(exception); + } + + public override bool IsRetriable(HttpMessage message, Exception exception) + { + if (Applies(message, ResponseClassification.DontRetry)) return false; + + return _inner.IsRetriable(message, exception); + } + + public override bool IsErrorResponse(HttpMessage message) + { + if (Applies(message, ResponseClassification.Error)) return true; + if (Applies(message, ResponseClassification.Success)) return false; + + return _inner.IsErrorResponse(message); + } + + private bool Applies(HttpMessage message, ResponseClassification responseClassification) + { + foreach (var classifier in _classifiers) + { + if (classifier.TryClassify(message, null, out var c) && + c == responseClassification) + { + return true; + } + } + + return false; + } + } + + private abstract class HttpMessageClassifier + { + public abstract bool TryClassify(HttpMessage message, Exception? exception, out ResponseClassification classification); + } + + private class FuncHttpMessageClassifier : HttpMessageClassifier + { + private readonly Func _func; + + public FuncHttpMessageClassifier(Func func) + { + _func = func; + } + + public override bool TryClassify(HttpMessage message, Exception? exception, out ResponseClassification classification) + { + if (_func(message) is ResponseClassification c) + { + classification = c; + return true; + } + + classification = default; + return false; + } + } + } + + /// + /// Specifies how the response will be processed by the pipeline and the client. + /// + public enum ResponseClassification + { + /// + /// The response will be retried. + /// + Retry, + + /// + /// The response will not be retried. + /// + DontRetry, + + /// + /// The client considers this response to be an error. + /// + Error, + + /// + /// The client considers this reponse to be a success. + /// + Success, } } diff --git a/sdk/core/Azure.Core.Experimental/src/ResponseStatusOption.cs b/sdk/core/Azure.Core.Experimental/src/ResponseStatusOption.cs index 3a5a3f2aeef9e..ebdedfd231aac 100644 --- a/sdk/core/Azure.Core.Experimental/src/ResponseStatusOption.cs +++ b/sdk/core/Azure.Core.Experimental/src/ResponseStatusOption.cs @@ -11,10 +11,10 @@ public enum ResponseStatusOption /// /// Indicates that an operation should throw an exception when the response indicates a failure. /// - ThrowOnError = 0, + Default = 0, /// /// Indicates that an operation should not throw an exception when the response indicates a failure. /// - SuppressExceptions = 1, + NoThrow = 1, } } diff --git a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs index 864dafe6cd04d..496157db5cef4 100644 --- a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs +++ b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs @@ -4,7 +4,6 @@ #nullable disable using System; -using System.Collections.Generic; using System.Threading.Tasks; using Azure; using Azure.Core; @@ -22,7 +21,6 @@ public partial class PetStoreClient private Uri endpoint; private readonly string apiVersion; private readonly ClientDiagnostics _clientDiagnostics; - private Dictionary _responseClassifierCache; /// Initializes a new instance of PetStoreClient for mocking. protected PetStoreClient() @@ -51,22 +49,6 @@ public PetStoreClient(Uri endpoint, TokenCredential credential, PetStoreClientOp Pipeline = HttpPipelineBuilder.Build(options, new HttpPipelinePolicy[] { new LowLevelCallbackPolicy() }, new HttpPipelinePolicy[] { authPolicy }, new ResponseClassifier()); this.endpoint = endpoint; apiVersion = options.Version; - - _responseClassifierCache = CreateResponseClassifierCache(); - } - - private Dictionary CreateResponseClassifierCache() - { - Dictionary cache = new(); - - // Add a RequestOptions per method - cache["GetPet"] = new GetPetResponseClassifier(_clientDiagnostics); - - // Code generator will not generate new classifier types for classifiers - // with duplicate status code logic, but will reuse previously-generated - // ones. It will need to identify duplicates. - - return cache; } /// Get a pet by its Id. @@ -76,29 +58,27 @@ private Dictionary CreateResponseClassifierCache() public virtual async Task GetPetAsync(string id, RequestOptions options = null) #pragma warning restore AZC0002 { + options ??= new RequestOptions(); using HttpMessage message = CreateGetPetRequest(id, options); RequestOptions.Apply(options, message); using var scope = _clientDiagnostics.CreateScope("PetStoreClient.GetPet"); scope.Start(); try { - await Pipeline.SendAsync(message, options?.CancellationToken ?? default).ConfigureAwait(false); - var statusOption = options?.StatusOption ?? ResponseStatusOption.ThrowOnError; - - if (statusOption == ResponseStatusOption.SuppressExceptions) + await Pipeline.SendAsync(message, options.CancellationToken).ConfigureAwait(false); + if (options.StatusOption == ResponseStatusOption.Default) { - return message.Response; + switch (message.Response.Status) + { + case 200: + return message.Response; + default: + throw await _clientDiagnostics.CreateRequestFailedExceptionAsync(message.Response).ConfigureAwait(false); + } } else { - if (!message.ResponseClassifier.IsErrorResponse(message)) - { - return message.Response; - } - else - { - throw await _clientDiagnostics.CreateRequestFailedExceptionAsync(message.Response).ConfigureAwait(false); - } + return message.Response; } } catch (Exception e) @@ -115,30 +95,36 @@ public virtual async Task GetPetAsync(string id, RequestOptions option public virtual Response GetPet(string id, RequestOptions options = null) #pragma warning restore AZC0002 { + if (options == null) + { + // We're in a helper method, not an LLC method + options = new RequestOptions(); + options.AddClassifier(new[] { 200 }, ResponseClassification.Success); + options.AddClassifier(new[] { 404 }, ResponseClassification.Success); + + // TODO: Do we need to designate errors explicitly? + } + using HttpMessage message = CreateGetPetRequest(id, options); RequestOptions.Apply(options, message); using var scope = _clientDiagnostics.CreateScope("PetStoreClient.GetPet"); scope.Start(); try { - Pipeline.Send(message, options?.CancellationToken ?? default); - var statusOption = options?.StatusOption ?? ResponseStatusOption.ThrowOnError; - - if (statusOption == ResponseStatusOption.SuppressExceptions) + Pipeline.Send(message, options.CancellationToken); + if (options.StatusOption == ResponseStatusOption.Default) { - return message.Response; + switch (message.Response.Status) + { + case 200: + return message.Response; + default: + throw _clientDiagnostics.CreateRequestFailedException(message.Response); + } } else { - // This will change to message.Response.IsError in a later PR - if (!message.ResponseClassifier.IsErrorResponse(message)) - { - return message.Response; - } - else - { - throw _clientDiagnostics.CreateRequestFailedException(message.Response); - } + return message.Response; } } catch (Exception e) @@ -162,29 +148,7 @@ private HttpMessage CreateGetPetRequest(string id, RequestOptions options = null uri.AppendPath(id, true); request.Uri = uri; request.Headers.Add("Accept", "application/json, text/json"); - message.ResponseClassifier = _responseClassifierCache["GetPet"]; return message; } - - private class GetPetResponseClassifier : ResponseClassifier - { - private ClientDiagnostics _clientDiagnostics; - - public GetPetResponseClassifier(ClientDiagnostics clientDiagnostics) - { - _clientDiagnostics = clientDiagnostics; - } - - public override bool IsErrorResponse(HttpMessage message) - { - switch (message.Response.Status) - { - case 200: - return false; - default: - return true; - } - } - } } } diff --git a/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs b/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs index 325949bc3d754..ab49f2b406e8a 100644 --- a/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs +++ b/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs @@ -92,67 +92,6 @@ private void SerializePet(ref Utf8JsonWriter writer, Pet pet) writer.WriteEndObject(); } - [Test] - public void CanSuppressExceptions() - { - var mockResponse = new MockResponse(404); - - var mockTransport = new MockTransport(mockResponse); - PetStoreClient client = CreateClient(mockTransport); - - RequestOptions options = new RequestOptions() - { - StatusOption = ResponseStatusOption.SuppressExceptions - }; - - Response response = default; - Assert.DoesNotThrowAsync(async () => - { - response = await client.GetPetAsync("snoopy", options); - }); - - Assert.AreEqual(404, response.Status); - } - - [Test] - public async Task ThrowOnErrorDoesntThrowOnSuccess() - { - var mockResponse = new MockResponse(200); - - Pet pet = new Pet("snoopy", "beagle"); - mockResponse.SetContent(SerializationHelpers.Serialize(pet, SerializePet)); - - var mockTransport = new MockTransport(mockResponse); - PetStoreClient client = CreateClient(mockTransport); - - Response response = await client.GetPetAsync("snoopy", new RequestOptions() - { - StatusOption = ResponseStatusOption.ThrowOnError - }); - var doc = JsonDocument.Parse(response.Content.ToMemory()); - - Assert.AreEqual(200, response.Status); - Assert.AreEqual("snoopy", doc.RootElement.GetProperty("name").GetString()); - Assert.AreEqual("beagle", doc.RootElement.GetProperty("species").GetString()); - } - - [Test] - public void ThrowOnErrorThrowsOnError() - { - var mockResponse = new MockResponse(404); - - var mockTransport = new MockTransport(mockResponse); - PetStoreClient client = CreateClient(mockTransport); - - Assert.ThrowsAsync(async () => - { - await client.GetPetAsync("snoopy", new RequestOptions() - { - StatusOption = ResponseStatusOption.ThrowOnError - }); - }); - } - #endregion } } diff --git a/sdk/core/Azure.Core.Experimental/tests/RequestOptionsTest.cs b/sdk/core/Azure.Core.Experimental/tests/RequestOptionsTest.cs index 082d0074ef615..0e849e7b41589 100644 --- a/sdk/core/Azure.Core.Experimental/tests/RequestOptionsTest.cs +++ b/sdk/core/Azure.Core.Experimental/tests/RequestOptionsTest.cs @@ -8,5 +8,81 @@ namespace Azure.Core.Tests { public class RequestOptionsTest { + [Test] + public void CanOverrideDefaultClassificationSuccess() + { + var m = CreateTestMessage(404); + + var options = new RequestOptions(); + options.AddClassifier(new[] { 404 }, ResponseClassification.Success); + + RequestOptions.Apply(options, m); + + Assert.False(m.ResponseClassifier.IsErrorResponse(m)); + } + + [Test] + public void CanOverrideDefaultClassificationThrow() + { + var m = CreateTestMessage(200); + + var options = new RequestOptions(); + options.AddClassifier(new[] { 200 }, ResponseClassification.Error); + + RequestOptions.Apply(options, m); + + Assert.True(m.ResponseClassifier.IsErrorResponse(m)); + } + + [Test] + public void CanOverrideDefaultClassificationRetry() + { + var m = CreateTestMessage(200); + + var options = new RequestOptions(); + options.AddClassifier(new[] { 200 }, ResponseClassification.Retry); + + RequestOptions.Apply(options, m); + + Assert.True(m.ResponseClassifier.IsRetriableResponse(m)); + } + + [Test] + public void CanOverrideDefaultClassificationNoRetry() + { + var m = CreateTestMessage(500); + + var options = new RequestOptions(); + options.AddClassifier(new[] { 500 }, ResponseClassification.DontRetry); + + RequestOptions.Apply(options, m); + + Assert.False(m.ResponseClassifier.IsRetriableResponse(m)); + } + + [Test] + public void CanOverrideDefaultClassificationWithFunc() + { + HttpMessage m = new HttpMessage(new MockRequest(), new ResponseClassifier()) + { + Response = new MockResponse(500) + }; + + var options = new RequestOptions(); + options.AddClassifier(_ => ResponseClassification.Success); + + RequestOptions.Apply(options, m); + + Assert.False(m.ResponseClassifier.IsErrorResponse(m)); + } + + private static HttpMessage CreateTestMessage(int status) + { + HttpMessage m = new HttpMessage(new MockRequest(), new ResponseClassifier()) + { + Response = new MockResponse(status) + }; + return m; + } } } From 749502da5c81e3a997e1ea4e0ef589b9ef0bd55f Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Mon, 20 Sep 2021 14:31:29 -0700 Subject: [PATCH 17/25] update Core API listing and undo changes to experimental --- .../src/RequestOptions.cs | 16 ++++++++-------- .../tests/LowLevelClient/PetStoreClient.cs | 11 +---------- .../tests/LowLevelClientTests.cs | 1 + .../tests/RequestOptionsTest.cs | 4 ++-- sdk/core/Azure.Core/api/Azure.Core.net461.cs | 1 + sdk/core/Azure.Core/api/Azure.Core.net5.0.cs | 1 + .../Azure.Core/api/Azure.Core.netcoreapp2.1.cs | 1 + .../Azure.Core/api/Azure.Core.netstandard2.0.cs | 1 + 8 files changed, 16 insertions(+), 20 deletions(-) diff --git a/sdk/core/Azure.Core.Experimental/src/RequestOptions.cs b/sdk/core/Azure.Core.Experimental/src/RequestOptions.cs index 8a00899ae5a8e..a529e58a12ae3 100644 --- a/sdk/core/Azure.Core.Experimental/src/RequestOptions.cs +++ b/sdk/core/Azure.Core.Experimental/src/RequestOptions.cs @@ -162,7 +162,7 @@ public override bool IsRetriable(HttpMessage message, Exception exception) public override bool IsErrorResponse(HttpMessage message) { - if (Applies(message, ResponseClassification.Error)) return true; + if (Applies(message, ResponseClassification.Throw)) return true; if (Applies(message, ResponseClassification.Success)) return false; return _inner.IsErrorResponse(message); @@ -212,28 +212,28 @@ public override bool TryClassify(HttpMessage message, Exception? exception, out } /// - /// Specifies how the response will be processed by the pipeline and the client. + /// Specifies how response would be processed by the pipeline and the client. /// public enum ResponseClassification { /// - /// The response will be retried. + /// The response would be retried. /// Retry, /// - /// The response will not be retried. + /// The response would be retried. /// DontRetry, /// - /// The client considers this response to be an error. + /// The client would throw an exception for the response. /// - Error, + Throw, /// - /// The client considers this reponse to be a success. + /// The client would tread the response a successful. /// Success, } -} +} \ No newline at end of file diff --git a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs index 496157db5cef4..fdcbcc68229d7 100644 --- a/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs +++ b/sdk/core/Azure.Core.Experimental/tests/LowLevelClient/PetStoreClient.cs @@ -95,16 +95,7 @@ public virtual async Task GetPetAsync(string id, RequestOptions option public virtual Response GetPet(string id, RequestOptions options = null) #pragma warning restore AZC0002 { - if (options == null) - { - // We're in a helper method, not an LLC method - options = new RequestOptions(); - options.AddClassifier(new[] { 200 }, ResponseClassification.Success); - options.AddClassifier(new[] { 404 }, ResponseClassification.Success); - - // TODO: Do we need to designate errors explicitly? - } - + options ??= new RequestOptions(); using HttpMessage message = CreateGetPetRequest(id, options); RequestOptions.Apply(options, message); using var scope = _clientDiagnostics.CreateScope("PetStoreClient.GetPet"); diff --git a/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs b/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs index ab49f2b406e8a..e557529d67ca0 100644 --- a/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs +++ b/sdk/core/Azure.Core.Experimental/tests/LowLevelClientTests.cs @@ -18,6 +18,7 @@ public LowLevelClientTests(bool isAsync) : base(isAsync) { } + private PetStoreClient client { get; set; } private readonly Uri _url = new Uri("https://example.azurepetstore.com"); public PetStoreClient CreateClient(HttpPipelineTransport transport) diff --git a/sdk/core/Azure.Core.Experimental/tests/RequestOptionsTest.cs b/sdk/core/Azure.Core.Experimental/tests/RequestOptionsTest.cs index 0e849e7b41589..51b96bc8c8b5a 100644 --- a/sdk/core/Azure.Core.Experimental/tests/RequestOptionsTest.cs +++ b/sdk/core/Azure.Core.Experimental/tests/RequestOptionsTest.cs @@ -27,7 +27,7 @@ public void CanOverrideDefaultClassificationThrow() var m = CreateTestMessage(200); var options = new RequestOptions(); - options.AddClassifier(new[] { 200 }, ResponseClassification.Error); + options.AddClassifier(new[] { 200 }, ResponseClassification.Throw); RequestOptions.Apply(options, m); @@ -85,4 +85,4 @@ private static HttpMessage CreateTestMessage(int status) return m; } } -} +} \ No newline at end of file diff --git a/sdk/core/Azure.Core/api/Azure.Core.net461.cs b/sdk/core/Azure.Core/api/Azure.Core.net461.cs index 4aa4d99bd6555..04adffe51c231 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net461.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net461.cs @@ -198,6 +198,7 @@ protected Response() { } public virtual System.BinaryData Content { get { throw null; } } public abstract System.IO.Stream? ContentStream { get; set; } public virtual Azure.Core.ResponseHeaders Headers { get { throw null; } } + public bool IsError { get { throw null; } protected set { } } public abstract string ReasonPhrase { get; } public abstract int Status { get; } protected internal abstract bool ContainsHeader(string name); diff --git a/sdk/core/Azure.Core/api/Azure.Core.net5.0.cs b/sdk/core/Azure.Core/api/Azure.Core.net5.0.cs index db985af224531..db53eb209a2d7 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net5.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net5.0.cs @@ -198,6 +198,7 @@ protected Response() { } public virtual System.BinaryData Content { get { throw null; } } public abstract System.IO.Stream? ContentStream { get; set; } public virtual Azure.Core.ResponseHeaders Headers { get { throw null; } } + public bool IsError { get { throw null; } protected set { } } public abstract string ReasonPhrase { get; } public abstract int Status { get; } protected internal abstract bool ContainsHeader(string name); diff --git a/sdk/core/Azure.Core/api/Azure.Core.netcoreapp2.1.cs b/sdk/core/Azure.Core/api/Azure.Core.netcoreapp2.1.cs index 4aa4d99bd6555..04adffe51c231 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.netcoreapp2.1.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.netcoreapp2.1.cs @@ -198,6 +198,7 @@ protected Response() { } public virtual System.BinaryData Content { get { throw null; } } public abstract System.IO.Stream? ContentStream { get; set; } public virtual Azure.Core.ResponseHeaders Headers { get { throw null; } } + public bool IsError { get { throw null; } protected set { } } public abstract string ReasonPhrase { get; } public abstract int Status { get; } protected internal abstract bool ContainsHeader(string name); diff --git a/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs b/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs index 4aa4d99bd6555..04adffe51c231 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs @@ -198,6 +198,7 @@ protected Response() { } public virtual System.BinaryData Content { get { throw null; } } public abstract System.IO.Stream? ContentStream { get; set; } public virtual Azure.Core.ResponseHeaders Headers { get { throw null; } } + public bool IsError { get { throw null; } protected set { } } public abstract string ReasonPhrase { get; } public abstract int Status { get; } protected internal abstract bool ContainsHeader(string name); From 38b0143f2c57c4093491b56df6c921ebbdb0f6af Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Mon, 20 Sep 2021 14:51:11 -0700 Subject: [PATCH 18/25] add tests --- sdk/core/Azure.Core/tests/PipelineTests.cs | 48 ++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/sdk/core/Azure.Core/tests/PipelineTests.cs b/sdk/core/Azure.Core/tests/PipelineTests.cs index 69fb3f6fc48a9..5bade09f7b0de 100644 --- a/sdk/core/Azure.Core/tests/PipelineTests.cs +++ b/sdk/core/Azure.Core/tests/PipelineTests.cs @@ -58,6 +58,54 @@ public void TryGetPropertyIsCaseSensitive() Assert.False(message.TryGetProperty("SomeName", out _)); } + [Test] + public async Task PipelineSetsResponseIsErrorTrue() + { + var mockTransport = new MockTransport( + new MockResponse(500)); + + var pipeline = new HttpPipeline(mockTransport); + + Request request = pipeline.CreateRequest(); + request.Method = RequestMethod.Get; + request.Uri.Reset(new Uri("https://contoso.a.io")); + Response response = await pipeline.SendRequestAsync(request, CancellationToken.None); + + Assert.IsTrue(response.IsError); + } + + [Test] + public async Task PipelineSetsResponseIsErrorFalse() + { + var mockTransport = new MockTransport( + new MockResponse(200)); + + var pipeline = new HttpPipeline(mockTransport); + + Request request = pipeline.CreateRequest(); + request.Method = RequestMethod.Get; + request.Uri.Reset(new Uri("https://contoso.a.io")); + Response response = await pipeline.SendRequestAsync(request, CancellationToken.None); + + Assert.IsFalse(response.IsError); + } + + [Test] + public async Task CustomClassifierSetsResponseIsError() + { + var mockTransport = new MockTransport( + new MockResponse(404)); + + var pipeline = new HttpPipeline(mockTransport, responseClassifier: new CustomResponseClassifier()); + + Request request = pipeline.CreateRequest(); + request.Method = RequestMethod.Get; + request.Uri.Reset(new Uri("https://contoso.a.io")); + Response response = await pipeline.SendRequestAsync(request, CancellationToken.None); + + Assert.IsFalse(response.IsError); + } + private class CustomResponseClassifier : ResponseClassifier { public override bool IsRetriableResponse(HttpMessage message) From a78021679180c8c1828b9560a75340f153f80f02 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Mon, 20 Sep 2021 17:18:31 -0700 Subject: [PATCH 19/25] await pipeline call --- sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs b/sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs index df7278ee6f3e5..2644759676746 100644 --- a/sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs +++ b/sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs @@ -66,13 +66,12 @@ public HttpMessage CreateMessage() /// The to send. /// The to use. /// The representing the asynchronous operation. - public ValueTask SendAsync(HttpMessage message, CancellationToken cancellationToken) + public async ValueTask SendAsync(HttpMessage message, CancellationToken cancellationToken) { message.CancellationToken = cancellationToken; AddHttpMessageProperties(message); - var value = _pipeline.Span[0].ProcessAsync(message, _pipeline.Slice(1)); + await _pipeline.Span[0].ProcessAsync(message, _pipeline.Slice(1)).ConfigureAwait(false); message.Response.EvaluateError(message); - return value; } /// From de8442ff2954327aa4656bacc2bbdabc1051ec58 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Tue, 21 Sep 2021 15:07:40 -0700 Subject: [PATCH 20/25] initial move changes to Experimental --- .../Azure.Core.Experimental.netstandard2.0.cs | 6 + .../src/HttpMessageExtensions.cs | 26 +++++ .../src/ResponsePropertiesPolicy.cs | 40 +++++++ .../tests/PipelineTests.cs | 106 ++++++++++++++++++ sdk/core/Azure.Core/api/Azure.Core.net461.cs | 1 - sdk/core/Azure.Core/api/Azure.Core.net5.0.cs | 1 - .../api/Azure.Core.netcoreapp2.1.cs | 1 - .../api/Azure.Core.netstandard2.0.cs | 1 - .../Azure.Core/src/Pipeline/HttpPipeline.cs | 6 +- sdk/core/Azure.Core/src/Response.cs | 10 -- sdk/core/Azure.Core/tests/PipelineTests.cs | 48 -------- 11 files changed, 180 insertions(+), 66 deletions(-) create mode 100644 sdk/core/Azure.Core.Experimental/src/HttpMessageExtensions.cs create mode 100644 sdk/core/Azure.Core.Experimental/src/ResponsePropertiesPolicy.cs create mode 100644 sdk/core/Azure.Core.Experimental/tests/PipelineTests.cs diff --git a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs index a95a62e7982d1..352308fd9214d 100644 --- a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs +++ b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs @@ -183,4 +183,10 @@ public partial class ProtocolClientOptions : Azure.Core.ClientOptions { public ProtocolClientOptions() { } } + public partial class ResponsePropertiesPolicy : Azure.Core.Pipeline.HttpPipelinePolicy + { + public ResponsePropertiesPolicy() { } + public override void Process(Azure.Core.HttpMessage message, System.ReadOnlyMemory pipeline) { } + public override System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message, System.ReadOnlyMemory pipeline) { throw null; } + } } diff --git a/sdk/core/Azure.Core.Experimental/src/HttpMessageExtensions.cs b/sdk/core/Azure.Core.Experimental/src/HttpMessageExtensions.cs new file mode 100644 index 0000000000000..301e6b961be66 --- /dev/null +++ b/sdk/core/Azure.Core.Experimental/src/HttpMessageExtensions.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Azure.Core +{ + internal static class HttpMessageExtensions + { + public static bool ResponseIsError(this HttpMessage message) + { + if (message.TryGetProperty("ResponseIsError", out object? isError)) + { + return (bool)isError!; + } + + throw new InvalidOperationException("ResponseIsError is not set on message."); + } + + internal static void EvaluateError(this HttpMessage message) + { + bool isError = message.ResponseClassifier.IsErrorResponse(message); + message.SetProperty("ResponseIsError", isError); + } + } +} diff --git a/sdk/core/Azure.Core.Experimental/src/ResponsePropertiesPolicy.cs b/sdk/core/Azure.Core.Experimental/src/ResponsePropertiesPolicy.cs new file mode 100644 index 0000000000000..c4a6a5a698c9b --- /dev/null +++ b/sdk/core/Azure.Core.Experimental/src/ResponsePropertiesPolicy.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Azure.Core.Pipeline; + +namespace Azure.Core +{ + /// + /// + public class ResponsePropertiesPolicy : HttpPipelinePolicy + { + /// + public override void Process(HttpMessage message, ReadOnlyMemory pipeline) + { + ProcessAsync(message, pipeline, false).EnsureCompleted(); + } + + /// + public override ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory pipeline) + { + return ProcessAsync(message, pipeline, true); + } + + private static async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory pipeline, bool async) + { + message.EvaluateError(); + + if (async) + { + await ProcessNextAsync(message, pipeline).ConfigureAwait(false); + } + else + { + ProcessNext(message, pipeline); + } + } + } +} diff --git a/sdk/core/Azure.Core.Experimental/tests/PipelineTests.cs b/sdk/core/Azure.Core.Experimental/tests/PipelineTests.cs new file mode 100644 index 0000000000000..b1fa524186ab6 --- /dev/null +++ b/sdk/core/Azure.Core.Experimental/tests/PipelineTests.cs @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Azure.Core.Experimental; +using Azure.Core.Experimental.Tests; +using Azure.Core.Experimental.Tests.Models; +using Azure.Core.Pipeline; +using Azure.Core.TestFramework; +using NUnit.Framework; + +namespace Azure.Core.Tests +{ + public class PipelineTests : ClientTestBase + { + public PipelineTests(bool isAsync) : base(isAsync) + { + } + + [Test] + public async Task PipelineSetsResponseIsErrorTrue() + { + var mockTransport = new MockTransport( + new MockResponse(500)); + + var pipeline = new HttpPipeline(mockTransport, new[] { new ResponsePropertiesPolicy() }); + + Request request = pipeline.CreateRequest(); + request.Method = RequestMethod.Get; + request.Uri.Reset(new Uri("https://contoso.a.io")); + Response response = await pipeline.SendRequestAsync(request, CancellationToken.None); + + //Assert.IsTrue(response.IsError); + } + + [Test] + public async Task PipelineSetsResponseIsErrorFalse() + { + var mockTransport = new MockTransport( + new MockResponse(200)); + + var pipeline = new HttpPipeline(mockTransport); + + Request request = pipeline.CreateRequest(); + request.Method = RequestMethod.Get; + request.Uri.Reset(new Uri("https://contoso.a.io")); + Response response = await pipeline.SendRequestAsync(request, CancellationToken.None); + + //Assert.IsFalse(response.IsError); + } + + [Test] + public async Task CustomClassifierSetsResponseIsError() + { + var mockTransport = new MockTransport( + new MockResponse(404)); + + var pipeline = new HttpPipeline(mockTransport, responseClassifier: new CustomResponseClassifier()); + + Request request = pipeline.CreateRequest(); + request.Method = RequestMethod.Get; + request.Uri.Reset(new Uri("https://contoso.a.io")); + Response response = await pipeline.SendRequestAsync(request, CancellationToken.None); + + //Assert.IsFalse(response.IsError); + } + + private class CustomResponseClassifier : ResponseClassifier + { + public override bool IsRetriableResponse(HttpMessage message) + { + return message.Response.Status == 500; + } + + public override bool IsRetriableException(Exception exception) + { + return false; + } + + public override bool IsErrorResponse(HttpMessage message) + { + return IsRetriableResponse(message); + } + } + + #region Helpers + + private void SerializePet(ref Utf8JsonWriter writer, Pet pet) + { + writer.WriteStartObject(); + + writer.WritePropertyName("name"); + writer.WriteStringValue(pet.Name); + + writer.WritePropertyName("species"); + writer.WriteStringValue(pet.Species); + + writer.WriteEndObject(); + } + + #endregion + } +} diff --git a/sdk/core/Azure.Core/api/Azure.Core.net461.cs b/sdk/core/Azure.Core/api/Azure.Core.net461.cs index 04adffe51c231..4aa4d99bd6555 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net461.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net461.cs @@ -198,7 +198,6 @@ protected Response() { } public virtual System.BinaryData Content { get { throw null; } } public abstract System.IO.Stream? ContentStream { get; set; } public virtual Azure.Core.ResponseHeaders Headers { get { throw null; } } - public bool IsError { get { throw null; } protected set { } } public abstract string ReasonPhrase { get; } public abstract int Status { get; } protected internal abstract bool ContainsHeader(string name); diff --git a/sdk/core/Azure.Core/api/Azure.Core.net5.0.cs b/sdk/core/Azure.Core/api/Azure.Core.net5.0.cs index db53eb209a2d7..db985af224531 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net5.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net5.0.cs @@ -198,7 +198,6 @@ protected Response() { } public virtual System.BinaryData Content { get { throw null; } } public abstract System.IO.Stream? ContentStream { get; set; } public virtual Azure.Core.ResponseHeaders Headers { get { throw null; } } - public bool IsError { get { throw null; } protected set { } } public abstract string ReasonPhrase { get; } public abstract int Status { get; } protected internal abstract bool ContainsHeader(string name); diff --git a/sdk/core/Azure.Core/api/Azure.Core.netcoreapp2.1.cs b/sdk/core/Azure.Core/api/Azure.Core.netcoreapp2.1.cs index 04adffe51c231..4aa4d99bd6555 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.netcoreapp2.1.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.netcoreapp2.1.cs @@ -198,7 +198,6 @@ protected Response() { } public virtual System.BinaryData Content { get { throw null; } } public abstract System.IO.Stream? ContentStream { get; set; } public virtual Azure.Core.ResponseHeaders Headers { get { throw null; } } - public bool IsError { get { throw null; } protected set { } } public abstract string ReasonPhrase { get; } public abstract int Status { get; } protected internal abstract bool ContainsHeader(string name); diff --git a/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs b/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs index 04adffe51c231..4aa4d99bd6555 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs @@ -198,7 +198,6 @@ protected Response() { } public virtual System.BinaryData Content { get { throw null; } } public abstract System.IO.Stream? ContentStream { get; set; } public virtual Azure.Core.ResponseHeaders Headers { get { throw null; } } - public bool IsError { get { throw null; } protected set { } } public abstract string ReasonPhrase { get; } public abstract int Status { get; } protected internal abstract bool ContainsHeader(string name); diff --git a/sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs b/sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs index 2644759676746..c283475c0d86e 100644 --- a/sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs +++ b/sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs @@ -66,12 +66,11 @@ public HttpMessage CreateMessage() /// The to send. /// The to use. /// The representing the asynchronous operation. - public async ValueTask SendAsync(HttpMessage message, CancellationToken cancellationToken) + public ValueTask SendAsync(HttpMessage message, CancellationToken cancellationToken) { message.CancellationToken = cancellationToken; AddHttpMessageProperties(message); - await _pipeline.Span[0].ProcessAsync(message, _pipeline.Slice(1)).ConfigureAwait(false); - message.Response.EvaluateError(message); + return _pipeline.Span[0].ProcessAsync(message, _pipeline.Slice(1)); } /// @@ -84,7 +83,6 @@ public void Send(HttpMessage message, CancellationToken cancellationToken) message.CancellationToken = cancellationToken; AddHttpMessageProperties(message); _pipeline.Span[0].Process(message, _pipeline.Slice(1)); - message.Response.EvaluateError(message); } /// /// Invokes the pipeline asynchronously with the provided request. diff --git a/sdk/core/Azure.Core/src/Response.cs b/sdk/core/Azure.Core/src/Response.cs index dbbcbd992dc42..e31a6ec4752ad 100644 --- a/sdk/core/Azure.Core/src/Response.cs +++ b/sdk/core/Azure.Core/src/Response.cs @@ -111,16 +111,6 @@ public virtual BinaryData Content /// The enumerating in the response. protected internal abstract IEnumerable EnumerateHeaders(); - /// - /// Indicates whether this response is an error, according to the REST API. - /// - public bool IsError { get; protected set; } - - internal void EvaluateError(HttpMessage message) - { - IsError = message.ResponseClassifier.IsErrorResponse(message); - } - /// /// Creates a new instance of with the provided value and HTTP response. /// diff --git a/sdk/core/Azure.Core/tests/PipelineTests.cs b/sdk/core/Azure.Core/tests/PipelineTests.cs index 5bade09f7b0de..69fb3f6fc48a9 100644 --- a/sdk/core/Azure.Core/tests/PipelineTests.cs +++ b/sdk/core/Azure.Core/tests/PipelineTests.cs @@ -58,54 +58,6 @@ public void TryGetPropertyIsCaseSensitive() Assert.False(message.TryGetProperty("SomeName", out _)); } - [Test] - public async Task PipelineSetsResponseIsErrorTrue() - { - var mockTransport = new MockTransport( - new MockResponse(500)); - - var pipeline = new HttpPipeline(mockTransport); - - Request request = pipeline.CreateRequest(); - request.Method = RequestMethod.Get; - request.Uri.Reset(new Uri("https://contoso.a.io")); - Response response = await pipeline.SendRequestAsync(request, CancellationToken.None); - - Assert.IsTrue(response.IsError); - } - - [Test] - public async Task PipelineSetsResponseIsErrorFalse() - { - var mockTransport = new MockTransport( - new MockResponse(200)); - - var pipeline = new HttpPipeline(mockTransport); - - Request request = pipeline.CreateRequest(); - request.Method = RequestMethod.Get; - request.Uri.Reset(new Uri("https://contoso.a.io")); - Response response = await pipeline.SendRequestAsync(request, CancellationToken.None); - - Assert.IsFalse(response.IsError); - } - - [Test] - public async Task CustomClassifierSetsResponseIsError() - { - var mockTransport = new MockTransport( - new MockResponse(404)); - - var pipeline = new HttpPipeline(mockTransport, responseClassifier: new CustomResponseClassifier()); - - Request request = pipeline.CreateRequest(); - request.Method = RequestMethod.Get; - request.Uri.Reset(new Uri("https://contoso.a.io")); - Response response = await pipeline.SendRequestAsync(request, CancellationToken.None); - - Assert.IsFalse(response.IsError); - } - private class CustomResponseClassifier : ResponseClassifier { public override bool IsRetriableResponse(HttpMessage message) From 4e3406e42e789106e8fb4dad86f296dcdf3471cf Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Tue, 21 Sep 2021 15:11:20 -0700 Subject: [PATCH 21/25] api tweaks --- .../api/Azure.Core.Experimental.netstandard2.0.cs | 10 ++++------ .../src/HttpMessageExtensions.cs | 9 ++++++++- .../src/ResponsePropertiesPolicy.cs | 2 +- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs index 352308fd9214d..56d3d73c578d0 100644 --- a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs +++ b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs @@ -94,6 +94,10 @@ protected virtual void Dispose(bool disposing) { } protected override bool TryGetHeader(string name, out string? value) { throw null; } protected override bool TryGetHeaderValues(string name, out System.Collections.Generic.IEnumerable? values) { throw null; } } + public static partial class HttpMessageExtensions + { + public static bool ResponseIsError(this Azure.Core.HttpMessage message) { throw null; } + } [System.Diagnostics.DebuggerDisplayAttribute("{DebuggerDisplay,nq}")] public partial class JsonData : System.Dynamic.IDynamicMetaObjectProvider, System.IEquatable { @@ -183,10 +187,4 @@ public partial class ProtocolClientOptions : Azure.Core.ClientOptions { public ProtocolClientOptions() { } } - public partial class ResponsePropertiesPolicy : Azure.Core.Pipeline.HttpPipelinePolicy - { - public ResponsePropertiesPolicy() { } - public override void Process(Azure.Core.HttpMessage message, System.ReadOnlyMemory pipeline) { } - public override System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message, System.ReadOnlyMemory pipeline) { throw null; } - } } diff --git a/sdk/core/Azure.Core.Experimental/src/HttpMessageExtensions.cs b/sdk/core/Azure.Core.Experimental/src/HttpMessageExtensions.cs index 301e6b961be66..ed1458371cde0 100644 --- a/sdk/core/Azure.Core.Experimental/src/HttpMessageExtensions.cs +++ b/sdk/core/Azure.Core.Experimental/src/HttpMessageExtensions.cs @@ -5,8 +5,15 @@ namespace Azure.Core { - internal static class HttpMessageExtensions + /// + /// + public static class HttpMessageExtensions { + /// + /// Stand-in for Response.IsError during experimentation + /// + /// + /// public static bool ResponseIsError(this HttpMessage message) { if (message.TryGetProperty("ResponseIsError", out object? isError)) diff --git a/sdk/core/Azure.Core.Experimental/src/ResponsePropertiesPolicy.cs b/sdk/core/Azure.Core.Experimental/src/ResponsePropertiesPolicy.cs index c4a6a5a698c9b..e199c8f21113f 100644 --- a/sdk/core/Azure.Core.Experimental/src/ResponsePropertiesPolicy.cs +++ b/sdk/core/Azure.Core.Experimental/src/ResponsePropertiesPolicy.cs @@ -9,7 +9,7 @@ namespace Azure.Core { /// /// - public class ResponsePropertiesPolicy : HttpPipelinePolicy + internal class ResponsePropertiesPolicy : HttpPipelinePolicy { /// public override void Process(HttpMessage message, ReadOnlyMemory pipeline) From fff3b0b289a6eeb14f5bda826ece65dfa0fc4adc Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Tue, 21 Sep 2021 16:08:32 -0700 Subject: [PATCH 22/25] Add ClassfiedResponse wrapping Response with IsError --- .../src/ClassifiedResponse.cs | 88 +++++++++++++++++++ .../src/HttpMessageExtensions.cs | 33 ------- .../src/ResponsePropertiesPolicy.cs | 8 +- .../tests/PipelineTests.cs | 12 +-- 4 files changed, 101 insertions(+), 40 deletions(-) create mode 100644 sdk/core/Azure.Core.Experimental/src/ClassifiedResponse.cs delete mode 100644 sdk/core/Azure.Core.Experimental/src/HttpMessageExtensions.cs diff --git a/sdk/core/Azure.Core.Experimental/src/ClassifiedResponse.cs b/sdk/core/Azure.Core.Experimental/src/ClassifiedResponse.cs new file mode 100644 index 0000000000000..e8ed3f2dd2a28 --- /dev/null +++ b/sdk/core/Azure.Core.Experimental/src/ClassifiedResponse.cs @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Text; + +namespace Azure.Core +{ + /// + /// Wrap Response and add IsError field. + /// + public class ClassifiedResponse : Response + { + private bool _disposed; + + private Response Response { get; } + + /// + /// + public bool IsError { get; private set; } + + internal void EvaluateError(HttpMessage message) + { + IsError = message.ResponseClassifier.IsErrorResponse(message); + } + + /// + public override int Status => Response.Status; + /// + public override string ReasonPhrase => Response.ReasonPhrase; + /// + public override Stream? ContentStream { get => Response.ContentStream; set => Response.ContentStream = value; } + /// + public override string ClientRequestId { get => Response.ClientRequestId; set => Response.ClientRequestId = value; } + /// + protected override bool TryGetHeader(string name, [NotNullWhen(true)] out string? value) => Response.Headers.TryGetValue(name, out value); + /// + protected override bool TryGetHeaderValues(string name, [NotNullWhen(true)] out IEnumerable? values) => Response.Headers.TryGetValues(name, out values); + /// + protected override bool ContainsHeader(string name) => Response.Headers.Contains(name); + /// + protected override IEnumerable EnumerateHeaders() => Response.Headers; + + /// + /// Represents a result of Azure operation with a response. + /// + /// The response returned by the service. + public ClassifiedResponse(Response response) + { + Response = response; + } + + /// + /// Frees resources held by the object. + /// + public override void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Frees resources held by the object. + /// + /// true if we should dispose, otherwise false + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + if (disposing) + { + Response.Dispose(); + } + _disposed = true; + } + + private string DebuggerDisplay + { + get => $"{{Status: {Response.Status}, IsError: {IsError}}}"; + } + } +} diff --git a/sdk/core/Azure.Core.Experimental/src/HttpMessageExtensions.cs b/sdk/core/Azure.Core.Experimental/src/HttpMessageExtensions.cs deleted file mode 100644 index ed1458371cde0..0000000000000 --- a/sdk/core/Azure.Core.Experimental/src/HttpMessageExtensions.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; - -namespace Azure.Core -{ - /// - /// - public static class HttpMessageExtensions - { - /// - /// Stand-in for Response.IsError during experimentation - /// - /// - /// - public static bool ResponseIsError(this HttpMessage message) - { - if (message.TryGetProperty("ResponseIsError", out object? isError)) - { - return (bool)isError!; - } - - throw new InvalidOperationException("ResponseIsError is not set on message."); - } - - internal static void EvaluateError(this HttpMessage message) - { - bool isError = message.ResponseClassifier.IsErrorResponse(message); - message.SetProperty("ResponseIsError", isError); - } - } -} diff --git a/sdk/core/Azure.Core.Experimental/src/ResponsePropertiesPolicy.cs b/sdk/core/Azure.Core.Experimental/src/ResponsePropertiesPolicy.cs index e199c8f21113f..d90bfaf305bc6 100644 --- a/sdk/core/Azure.Core.Experimental/src/ResponsePropertiesPolicy.cs +++ b/sdk/core/Azure.Core.Experimental/src/ResponsePropertiesPolicy.cs @@ -25,8 +25,6 @@ public override ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory pipeline, bool async) { - message.EvaluateError(); - if (async) { await ProcessNextAsync(message, pipeline).ConfigureAwait(false); @@ -35,6 +33,12 @@ private static async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory< { ProcessNext(message, pipeline); } + + // In the non-experimental version of this policy, these lines reduce to + // > message.Response.EvaluateError(message); + ClassifiedResponse response = new ClassifiedResponse(message.Response); + response.EvaluateError(message); + message.Response = response; } } } diff --git a/sdk/core/Azure.Core.Experimental/tests/PipelineTests.cs b/sdk/core/Azure.Core.Experimental/tests/PipelineTests.cs index b1fa524186ab6..acf80632b4208 100644 --- a/sdk/core/Azure.Core.Experimental/tests/PipelineTests.cs +++ b/sdk/core/Azure.Core.Experimental/tests/PipelineTests.cs @@ -33,7 +33,7 @@ public async Task PipelineSetsResponseIsErrorTrue() request.Uri.Reset(new Uri("https://contoso.a.io")); Response response = await pipeline.SendRequestAsync(request, CancellationToken.None); - //Assert.IsTrue(response.IsError); + Assert.IsTrue(((ClassifiedResponse)response).IsError); } [Test] @@ -42,14 +42,14 @@ public async Task PipelineSetsResponseIsErrorFalse() var mockTransport = new MockTransport( new MockResponse(200)); - var pipeline = new HttpPipeline(mockTransport); + var pipeline = new HttpPipeline(mockTransport, new[] { new ResponsePropertiesPolicy() }); Request request = pipeline.CreateRequest(); request.Method = RequestMethod.Get; request.Uri.Reset(new Uri("https://contoso.a.io")); Response response = await pipeline.SendRequestAsync(request, CancellationToken.None); - //Assert.IsFalse(response.IsError); + Assert.IsFalse(((ClassifiedResponse)response).IsError); } [Test] @@ -58,14 +58,16 @@ public async Task CustomClassifierSetsResponseIsError() var mockTransport = new MockTransport( new MockResponse(404)); - var pipeline = new HttpPipeline(mockTransport, responseClassifier: new CustomResponseClassifier()); + var pipeline = new HttpPipeline(mockTransport, + new[] { new ResponsePropertiesPolicy() }, + new CustomResponseClassifier()); Request request = pipeline.CreateRequest(); request.Method = RequestMethod.Get; request.Uri.Reset(new Uri("https://contoso.a.io")); Response response = await pipeline.SendRequestAsync(request, CancellationToken.None); - //Assert.IsFalse(response.IsError); + Assert.IsFalse(((ClassifiedResponse)response).IsError); } private class CustomResponseClassifier : ResponseClassifier From 4478c8220735d1e9ee055ccd2dc8cf59110b159c Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Tue, 21 Sep 2021 16:10:40 -0700 Subject: [PATCH 23/25] update api listing --- .../Azure.Core.Experimental.netstandard2.0.cs | 19 +++++++++++++++---- .../src/ResponsePropertiesPolicy.cs | 4 ++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs index 56d3d73c578d0..894e709cf211a 100644 --- a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs +++ b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs @@ -30,6 +30,21 @@ public enum ResponseStatusOption } namespace Azure.Core { + public partial class ClassifiedResponse : Azure.Response + { + public ClassifiedResponse(Azure.Response response) { } + public override string ClientRequestId { get { throw null; } set { } } + public override System.IO.Stream? ContentStream { get { throw null; } set { } } + public bool IsError { get { throw null; } } + public override string ReasonPhrase { get { throw null; } } + public override int Status { get { throw null; } } + protected override bool ContainsHeader(string name) { throw null; } + public override void Dispose() { } + protected virtual void Dispose(bool disposing) { } + protected override System.Collections.Generic.IEnumerable EnumerateHeaders() { throw null; } + protected override bool TryGetHeader(string name, out string? value) { throw null; } + protected override bool TryGetHeaderValues(string name, out System.Collections.Generic.IEnumerable? values) { throw null; } + } [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] public readonly partial struct ContentType : System.IEquatable, System.IEquatable { @@ -94,10 +109,6 @@ protected virtual void Dispose(bool disposing) { } protected override bool TryGetHeader(string name, out string? value) { throw null; } protected override bool TryGetHeaderValues(string name, out System.Collections.Generic.IEnumerable? values) { throw null; } } - public static partial class HttpMessageExtensions - { - public static bool ResponseIsError(this Azure.Core.HttpMessage message) { throw null; } - } [System.Diagnostics.DebuggerDisplayAttribute("{DebuggerDisplay,nq}")] public partial class JsonData : System.Dynamic.IDynamicMetaObjectProvider, System.IEquatable { diff --git a/sdk/core/Azure.Core.Experimental/src/ResponsePropertiesPolicy.cs b/sdk/core/Azure.Core.Experimental/src/ResponsePropertiesPolicy.cs index d90bfaf305bc6..9de89f88f3758 100644 --- a/sdk/core/Azure.Core.Experimental/src/ResponsePropertiesPolicy.cs +++ b/sdk/core/Azure.Core.Experimental/src/ResponsePropertiesPolicy.cs @@ -34,8 +34,8 @@ private static async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory< ProcessNext(message, pipeline); } - // In the non-experimental version of this policy, these lines reduce to - // > message.Response.EvaluateError(message); + // In the non-experimental version of this policy, these lines reduce to: + // > message.Response.EvaluateError(message); ClassifiedResponse response = new ClassifiedResponse(message.Response); response.EvaluateError(message); message.Response = response; From b9e2dca8b027e70627b95c827daf37c44ad46785 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Tue, 21 Sep 2021 16:18:35 -0700 Subject: [PATCH 24/25] api tweaks --- .../Azure.Core.Experimental.netstandard2.0.cs | 7 +++++++ .../src/ResponseExtensions.cs | 20 +++++++++++++++++++ .../tests/PipelineTests.cs | 6 +++--- 3 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 sdk/core/Azure.Core.Experimental/src/ResponseExtensions.cs diff --git a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs index 894e709cf211a..2715e2b1231b6 100644 --- a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs +++ b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs @@ -199,3 +199,10 @@ public partial class ProtocolClientOptions : Azure.Core.ClientOptions public ProtocolClientOptions() { } } } +namespace Azure.Core.Pipeline +{ + public static partial class ResponseExtensions + { + public static bool IsError(this Azure.Response response) { throw null; } + } +} diff --git a/sdk/core/Azure.Core.Experimental/src/ResponseExtensions.cs b/sdk/core/Azure.Core.Experimental/src/ResponseExtensions.cs new file mode 100644 index 0000000000000..98302d0d7b83e --- /dev/null +++ b/sdk/core/Azure.Core.Experimental/src/ResponseExtensions.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable disable + +namespace Azure.Core.Pipeline +{ + /// + /// Extensions for experimenting with Response API. + /// + public static class ResponseExtensions + { + /// + /// This will be a property on the non-experimental Azure.Core.Response. + /// + /// + /// + public static bool IsError(this Response response) => ((ClassifiedResponse)response).IsError; + } +} diff --git a/sdk/core/Azure.Core.Experimental/tests/PipelineTests.cs b/sdk/core/Azure.Core.Experimental/tests/PipelineTests.cs index acf80632b4208..cada905614688 100644 --- a/sdk/core/Azure.Core.Experimental/tests/PipelineTests.cs +++ b/sdk/core/Azure.Core.Experimental/tests/PipelineTests.cs @@ -33,7 +33,7 @@ public async Task PipelineSetsResponseIsErrorTrue() request.Uri.Reset(new Uri("https://contoso.a.io")); Response response = await pipeline.SendRequestAsync(request, CancellationToken.None); - Assert.IsTrue(((ClassifiedResponse)response).IsError); + Assert.IsTrue(response.IsError()); } [Test] @@ -49,7 +49,7 @@ public async Task PipelineSetsResponseIsErrorFalse() request.Uri.Reset(new Uri("https://contoso.a.io")); Response response = await pipeline.SendRequestAsync(request, CancellationToken.None); - Assert.IsFalse(((ClassifiedResponse)response).IsError); + Assert.IsFalse(response.IsError()); } [Test] @@ -67,7 +67,7 @@ public async Task CustomClassifierSetsResponseIsError() request.Uri.Reset(new Uri("https://contoso.a.io")); Response response = await pipeline.SendRequestAsync(request, CancellationToken.None); - Assert.IsFalse(((ClassifiedResponse)response).IsError); + Assert.IsFalse(response.IsError()); } private class CustomResponseClassifier : ResponseClassifier From 466d558ddafd6fb7eaad278a85ab626053804c51 Mon Sep 17 00:00:00 2001 From: Anne Thompson Date: Tue, 21 Sep 2021 17:23:42 -0700 Subject: [PATCH 25/25] pr fb --- .../src/ResponseExtensions.cs | 15 ++++++++++++++- .../tests/PipelineTests.cs | 17 ----------------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/sdk/core/Azure.Core.Experimental/src/ResponseExtensions.cs b/sdk/core/Azure.Core.Experimental/src/ResponseExtensions.cs index 98302d0d7b83e..34545c5768bc8 100644 --- a/sdk/core/Azure.Core.Experimental/src/ResponseExtensions.cs +++ b/sdk/core/Azure.Core.Experimental/src/ResponseExtensions.cs @@ -3,6 +3,8 @@ #nullable disable +using System; + namespace Azure.Core.Pipeline { /// @@ -15,6 +17,17 @@ public static class ResponseExtensions /// /// /// - public static bool IsError(this Response response) => ((ClassifiedResponse)response).IsError; + public static bool IsError(this Response response) + { + var classifiedResponse = response as ClassifiedResponse; + + if (classifiedResponse == null) + { + throw new InvalidOperationException("IsError was not set on the response. " + + "Please ensure the pipeline includes ResponsePropertiesPolicy."); + } + + return classifiedResponse.IsError; + } } } diff --git a/sdk/core/Azure.Core.Experimental/tests/PipelineTests.cs b/sdk/core/Azure.Core.Experimental/tests/PipelineTests.cs index cada905614688..994e4f4211a70 100644 --- a/sdk/core/Azure.Core.Experimental/tests/PipelineTests.cs +++ b/sdk/core/Azure.Core.Experimental/tests/PipelineTests.cs @@ -87,22 +87,5 @@ public override bool IsErrorResponse(HttpMessage message) return IsRetriableResponse(message); } } - - #region Helpers - - private void SerializePet(ref Utf8JsonWriter writer, Pet pet) - { - writer.WriteStartObject(); - - writer.WritePropertyName("name"); - writer.WriteStringValue(pet.Name); - - writer.WritePropertyName("species"); - writer.WriteStringValue(pet.Species); - - writer.WriteEndObject(); - } - - #endregion } }