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 a57d83d869b6f..b8ee6c1467b4e 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 @@ -19,6 +19,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 { @@ -173,3 +188,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/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/ResponseExtensions.cs b/sdk/core/Azure.Core.Experimental/src/ResponseExtensions.cs new file mode 100644 index 0000000000000..34545c5768bc8 --- /dev/null +++ b/sdk/core/Azure.Core.Experimental/src/ResponseExtensions.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable disable + +using System; + +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) + { + 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/src/ResponsePropertiesPolicy.cs b/sdk/core/Azure.Core.Experimental/src/ResponsePropertiesPolicy.cs new file mode 100644 index 0000000000000..9de89f88f3758 --- /dev/null +++ b/sdk/core/Azure.Core.Experimental/src/ResponsePropertiesPolicy.cs @@ -0,0 +1,44 @@ +// 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 +{ + /// + /// + internal 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) + { + if (async) + { + await ProcessNextAsync(message, pipeline).ConfigureAwait(false); + } + else + { + 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 new file mode 100644 index 0000000000000..994e4f4211a70 --- /dev/null +++ b/sdk/core/Azure.Core.Experimental/tests/PipelineTests.cs @@ -0,0 +1,91 @@ +// 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, 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()); + } + + [Test] + public async Task CustomClassifierSetsResponseIsError() + { + var mockTransport = new MockTransport( + new MockResponse(404)); + + 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()); + } + + 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); + } + } + } +}