diff --git a/sdk/core/Azure.Core/api/Azure.Core.net461.cs b/sdk/core/Azure.Core/api/Azure.Core.net461.cs index 2c09e715a7c8..0dabcb13f0b2 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net461.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net461.cs @@ -115,16 +115,17 @@ public MatchConditions() { } public Azure.ETag? IfMatch { get { throw null; } set { } } public Azure.ETag? IfNoneMatch { get { throw null; } set { } } } - public abstract partial class NullableResponse + public abstract partial class NullableResponse : System.ClientModel.ClientResult { - protected NullableResponse() { } - public abstract bool HasValue { get; } - public abstract T? Value { get; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + protected NullableResponse() : base (default(T), default(System.ClientModel.Primitives.PipelineResponse)) { } + protected NullableResponse(T? value, Azure.Response response) : base (default(T), default(System.ClientModel.Primitives.PipelineResponse)) { } + public virtual bool HasValue { get { throw null; } } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public override bool Equals(object? obj) { throw null; } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public override int GetHashCode() { throw null; } - public abstract Azure.Response GetRawResponse(); + public virtual new Azure.Response GetRawResponse() { throw null; } public override string ToString() { throw null; } } public abstract partial class Operation @@ -238,18 +239,16 @@ public RequestFailedException(string message, System.Exception? innerException) public override void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { } public Azure.Response? GetRawResponse() { throw null; } } - public abstract partial class Response : System.IDisposable + public abstract partial class Response : System.ClientModel.Primitives.PipelineResponse { protected Response() { } public abstract string ClientRequestId { get; set; } - 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 virtual bool IsError { get { throw null; } } - public abstract string ReasonPhrase { get; } - public abstract int Status { get; } + public override System.BinaryData Content { get { throw null; } } + public virtual new Azure.Core.ResponseHeaders Headers { get { throw null; } } + protected override System.ClientModel.Primitives.PipelineResponseHeaders HeadersCore { get { throw null; } } + public override System.BinaryData BufferContent(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public override System.Threading.Tasks.ValueTask BufferContentAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } protected internal abstract bool ContainsHeader(string name); - public abstract void Dispose(); protected internal abstract System.Collections.Generic.IEnumerable EnumerateHeaders(); public static Azure.Response FromValue(T value, Azure.Response response) { throw null; } public override string ToString() { throw null; } @@ -265,7 +264,9 @@ public ResponseError(string? code, string? message) { } } public abstract partial class Response : Azure.NullableResponse { + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] protected Response() { } + protected Response(T value, Azure.Response response) { } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public override bool HasValue { get { throw null; } } public override T Value { get { throw null; } } diff --git a/sdk/core/Azure.Core/api/Azure.Core.net472.cs b/sdk/core/Azure.Core/api/Azure.Core.net472.cs index 2c09e715a7c8..0dabcb13f0b2 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net472.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net472.cs @@ -115,16 +115,17 @@ public MatchConditions() { } public Azure.ETag? IfMatch { get { throw null; } set { } } public Azure.ETag? IfNoneMatch { get { throw null; } set { } } } - public abstract partial class NullableResponse + public abstract partial class NullableResponse : System.ClientModel.ClientResult { - protected NullableResponse() { } - public abstract bool HasValue { get; } - public abstract T? Value { get; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + protected NullableResponse() : base (default(T), default(System.ClientModel.Primitives.PipelineResponse)) { } + protected NullableResponse(T? value, Azure.Response response) : base (default(T), default(System.ClientModel.Primitives.PipelineResponse)) { } + public virtual bool HasValue { get { throw null; } } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public override bool Equals(object? obj) { throw null; } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public override int GetHashCode() { throw null; } - public abstract Azure.Response GetRawResponse(); + public virtual new Azure.Response GetRawResponse() { throw null; } public override string ToString() { throw null; } } public abstract partial class Operation @@ -238,18 +239,16 @@ public RequestFailedException(string message, System.Exception? innerException) public override void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { } public Azure.Response? GetRawResponse() { throw null; } } - public abstract partial class Response : System.IDisposable + public abstract partial class Response : System.ClientModel.Primitives.PipelineResponse { protected Response() { } public abstract string ClientRequestId { get; set; } - 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 virtual bool IsError { get { throw null; } } - public abstract string ReasonPhrase { get; } - public abstract int Status { get; } + public override System.BinaryData Content { get { throw null; } } + public virtual new Azure.Core.ResponseHeaders Headers { get { throw null; } } + protected override System.ClientModel.Primitives.PipelineResponseHeaders HeadersCore { get { throw null; } } + public override System.BinaryData BufferContent(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public override System.Threading.Tasks.ValueTask BufferContentAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } protected internal abstract bool ContainsHeader(string name); - public abstract void Dispose(); protected internal abstract System.Collections.Generic.IEnumerable EnumerateHeaders(); public static Azure.Response FromValue(T value, Azure.Response response) { throw null; } public override string ToString() { throw null; } @@ -265,7 +264,9 @@ public ResponseError(string? code, string? message) { } } public abstract partial class Response : Azure.NullableResponse { + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] protected Response() { } + protected Response(T value, Azure.Response response) { } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public override bool HasValue { get { throw null; } } public override T Value { get { throw null; } } diff --git a/sdk/core/Azure.Core/api/Azure.Core.net6.0.cs b/sdk/core/Azure.Core/api/Azure.Core.net6.0.cs index 283d772f4260..95022c479e4c 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net6.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net6.0.cs @@ -115,16 +115,17 @@ public MatchConditions() { } public Azure.ETag? IfMatch { get { throw null; } set { } } public Azure.ETag? IfNoneMatch { get { throw null; } set { } } } - public abstract partial class NullableResponse + public abstract partial class NullableResponse : System.ClientModel.ClientResult { - protected NullableResponse() { } - public abstract bool HasValue { get; } - public abstract T? Value { get; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + protected NullableResponse() : base (default(T), default(System.ClientModel.Primitives.PipelineResponse)) { } + protected NullableResponse(T? value, Azure.Response response) : base (default(T), default(System.ClientModel.Primitives.PipelineResponse)) { } + public virtual bool HasValue { get { throw null; } } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public override bool Equals(object? obj) { throw null; } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public override int GetHashCode() { throw null; } - public abstract Azure.Response GetRawResponse(); + public virtual new Azure.Response GetRawResponse() { throw null; } public override string ToString() { throw null; } } public abstract partial class Operation @@ -238,18 +239,16 @@ public RequestFailedException(string message, System.Exception? innerException) public override void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { } public Azure.Response? GetRawResponse() { throw null; } } - public abstract partial class Response : System.IDisposable + public abstract partial class Response : System.ClientModel.Primitives.PipelineResponse { protected Response() { } public abstract string ClientRequestId { get; set; } - 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 virtual bool IsError { get { throw null; } } - public abstract string ReasonPhrase { get; } - public abstract int Status { get; } + public override System.BinaryData Content { get { throw null; } } + public virtual new Azure.Core.ResponseHeaders Headers { get { throw null; } } + protected override System.ClientModel.Primitives.PipelineResponseHeaders HeadersCore { get { throw null; } } + public override System.BinaryData BufferContent(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public override System.Threading.Tasks.ValueTask BufferContentAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } protected internal abstract bool ContainsHeader(string name); - public abstract void Dispose(); protected internal abstract System.Collections.Generic.IEnumerable EnumerateHeaders(); public static Azure.Response FromValue(T value, Azure.Response response) { throw null; } public override string ToString() { throw null; } @@ -265,7 +264,9 @@ public ResponseError(string? code, string? message) { } } public abstract partial class Response : Azure.NullableResponse { + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] protected Response() { } + protected Response(T value, Azure.Response response) { } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public override bool HasValue { get { throw null; } } public override T Value { get { throw null; } } 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 2c09e715a7c8..0dabcb13f0b2 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs @@ -115,16 +115,17 @@ public MatchConditions() { } public Azure.ETag? IfMatch { get { throw null; } set { } } public Azure.ETag? IfNoneMatch { get { throw null; } set { } } } - public abstract partial class NullableResponse + public abstract partial class NullableResponse : System.ClientModel.ClientResult { - protected NullableResponse() { } - public abstract bool HasValue { get; } - public abstract T? Value { get; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + protected NullableResponse() : base (default(T), default(System.ClientModel.Primitives.PipelineResponse)) { } + protected NullableResponse(T? value, Azure.Response response) : base (default(T), default(System.ClientModel.Primitives.PipelineResponse)) { } + public virtual bool HasValue { get { throw null; } } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public override bool Equals(object? obj) { throw null; } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public override int GetHashCode() { throw null; } - public abstract Azure.Response GetRawResponse(); + public virtual new Azure.Response GetRawResponse() { throw null; } public override string ToString() { throw null; } } public abstract partial class Operation @@ -238,18 +239,16 @@ public RequestFailedException(string message, System.Exception? innerException) public override void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { } public Azure.Response? GetRawResponse() { throw null; } } - public abstract partial class Response : System.IDisposable + public abstract partial class Response : System.ClientModel.Primitives.PipelineResponse { protected Response() { } public abstract string ClientRequestId { get; set; } - 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 virtual bool IsError { get { throw null; } } - public abstract string ReasonPhrase { get; } - public abstract int Status { get; } + public override System.BinaryData Content { get { throw null; } } + public virtual new Azure.Core.ResponseHeaders Headers { get { throw null; } } + protected override System.ClientModel.Primitives.PipelineResponseHeaders HeadersCore { get { throw null; } } + public override System.BinaryData BufferContent(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public override System.Threading.Tasks.ValueTask BufferContentAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } protected internal abstract bool ContainsHeader(string name); - public abstract void Dispose(); protected internal abstract System.Collections.Generic.IEnumerable EnumerateHeaders(); public static Azure.Response FromValue(T value, Azure.Response response) { throw null; } public override string ToString() { throw null; } @@ -265,7 +264,9 @@ public ResponseError(string? code, string? message) { } } public abstract partial class Response : Azure.NullableResponse { + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] protected Response() { } + protected Response(T value, Azure.Response response) { } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public override bool HasValue { get { throw null; } } public override T Value { get { throw null; } } diff --git a/sdk/core/Azure.Core/src/HttpMessage.cs b/sdk/core/Azure.Core/src/HttpMessage.cs index 06e0fa598077..aa7960c3839c 100644 --- a/sdk/core/Azure.Core/src/HttpMessage.cs +++ b/sdk/core/Azure.Core/src/HttpMessage.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.ClientModel.Primitives; using System.Collections.Generic; using System.IO; using System.Threading; @@ -177,7 +178,7 @@ public void SetProperty(Type type, object value) => _propertyBag.Set((ulong)type.TypeHandle.Value, value); /// - /// Returns the response content stream and releases it ownership to the caller. After calling this methods using or would result in exception. + /// Returns the response content stream and releases it ownership to the caller. After calling this methods using or would result in exception. /// /// The content stream or null if response didn't have any. public Stream? ExtractResponseContent() diff --git a/sdk/core/Azure.Core/src/Internal/ValueResponse.cs b/sdk/core/Azure.Core/src/Internal/ValueResponse.cs deleted file mode 100644 index b6db85a79089..000000000000 --- a/sdk/core/Azure.Core/src/Internal/ValueResponse.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Azure -{ - internal class ValueResponse : Response - { - private readonly Response _response; - - public ValueResponse(Response response, T value) - { - _response = response; - Value = value; - } - - public override T Value { get; } - - public override Response GetRawResponse() => _response; - } -} diff --git a/sdk/core/Azure.Core/src/NullableResponseOfT.cs b/sdk/core/Azure.Core/src/NullableResponseOfT.cs index d14cd7ac07ee..53871e8e4bed 100644 --- a/sdk/core/Azure.Core/src/NullableResponseOfT.cs +++ b/sdk/core/Azure.Core/src/NullableResponseOfT.cs @@ -1,7 +1,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Azure.Core; namespace Azure { @@ -10,26 +19,58 @@ namespace Azure /// /// The type of returned value. #pragma warning disable SA1649 // File name should match first type name - public abstract class NullableResponse + public abstract class NullableResponse : ClientResult #pragma warning restore SA1649 // File name should match first type name { private const string NoValue = ""; + // This property is used to enable passing a value to the base type + // that validates Response is not null. We don't expect this to be + // used, so it is instantiated lazily. + private static DefaultResponse? _defaultRawResponse; + private static DefaultResponse DefaultRawResponse + => _defaultRawResponse ??= new(); + + /// + /// Creates an instance of with no + /// value or . It is not intended for this + /// constructor to be called, as it will create an instance of + /// with a null , + /// which is not the intended usage of this type. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected NullableResponse() : base(default, DefaultRawResponse) + { + // Added for back-compat with GA APIs. Any type that derives from + // NullableResponse must provide an implementation for + // GetRawResponse that replaces DefaultResponse with the Response + // populated on HttpMessage during the call to pipeline.Send. + } + /// - /// Gets a value indicating whether the current instance has a valid value of its underlying type. + /// Creates an instance of from the + /// provided and . /// - public abstract bool HasValue { get; } + /// The value to return from + /// on the created instance. + /// The to return from + /// on the created instance. + protected NullableResponse(T? value, Response response) + : base(value, ReplaceWithDefaultIfNull(response)) + { + } /// - /// Gets the value returned by the service. Accessing this property will throw if is false. + /// Gets a value indicating whether the current instance has a non-null value. /// - public abstract T? Value { get; } + public virtual bool HasValue => Value != null; /// /// Returns the HTTP response returned by the service. /// /// The HTTP response returned by the service. - public abstract Response GetRawResponse(); + public new virtual Response GetRawResponse() + => (Response)base.GetRawResponse(); /// [EditorBrowsable(EditorBrowsableState.Never)] @@ -40,6 +81,87 @@ public abstract class NullableResponse public override int GetHashCode() => base.GetHashCode(); /// - public override string ToString() => $"Status: {GetRawResponse()?.Status}, Value: {(HasValue ? Value : NoValue)}"; + public override string ToString() + => $"Status: {GetRawResponse()?.Status}, Value: {(HasValue ? Value : NoValue)}"; + + private static Response ReplaceWithDefaultIfNull(Response? response) + => response ?? DefaultRawResponse; + + // This type exists because of the following reasons: + // 1. The base ClientResult constructor requires a non-null instance + // of PipelineResponse. + // 2. NullableResponse was GA'ed with a protected parameterless + // default constructor before inheriting from the base + // ClientResult class. + // 3. The new implementation of the default constructor must pass an + // instance of this dummy type to the base constructor. + // + // Because the intent of NullableResponse and Response is to + // always return a Response value from GetRawResponse, it is incorrect + // for types derived from them to return null from GetRawResponse, and + // instances of this type should only be created when the derived type + // has been implemented incorrectly. If an instance of this type is + // returned from GetRawResponse, callers who access its properties will + // get a NotSupportedException. + private class DefaultResponse : Response + { + private readonly string ExceptionMessage = "Types derived from abstract NullableResponse or Response must provide an implementation of the virtual GetRawResponse method that returns a non-null Response value."; + + public override string ClientRequestId + { + get => throw new NotSupportedException(ExceptionMessage); + set => throw new NotSupportedException(ExceptionMessage); + } + + public override int Status + => throw new NotSupportedException(ExceptionMessage); + + public override string ReasonPhrase + => throw new NotSupportedException(ExceptionMessage); + + public override Stream? ContentStream + { + get => throw new NotSupportedException(ExceptionMessage); + set => throw new NotSupportedException(ExceptionMessage); + } + + protected override PipelineResponseHeaders HeadersCore + => throw new NotSupportedException(ExceptionMessage); + + public override void Dispose() + { + throw new NotSupportedException(ExceptionMessage); + } + + protected internal override bool ContainsHeader(string name) + { + throw new NotSupportedException(ExceptionMessage); + } + + protected internal override IEnumerable EnumerateHeaders() + { + throw new NotSupportedException(ExceptionMessage); + } + + protected internal override bool TryGetHeader(string name, [NotNullWhen(true)] out string? value) + { + throw new NotSupportedException(ExceptionMessage); + } + + protected internal override bool TryGetHeaderValues(string name, [NotNullWhen(true)] out IEnumerable? values) + { + throw new NotSupportedException(ExceptionMessage); + } + + public override BinaryData BufferContent(CancellationToken cancellationToken = default) + { + throw new NotSupportedException(ExceptionMessage); + } + + public override ValueTask BufferContentAsync(CancellationToken cancellationToken = default) + { + throw new NotSupportedException(ExceptionMessage); + } + } } } diff --git a/sdk/core/Azure.Core/src/Pipeline/Internal/HttpPipelineTransportPolicy.cs b/sdk/core/Azure.Core/src/Pipeline/Internal/HttpPipelineTransportPolicy.cs index fd4ae71e2d2e..49f5dfce21f9 100644 --- a/sdk/core/Azure.Core/src/Pipeline/Internal/HttpPipelineTransportPolicy.cs +++ b/sdk/core/Azure.Core/src/Pipeline/Internal/HttpPipelineTransportPolicy.cs @@ -28,7 +28,7 @@ public override async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory message.Response.RequestFailedDetailsParser = _errorParser; message.Response.Sanitizer = _sanitizer; - message.Response.IsError = message.ResponseClassifier.IsErrorResponse(message); + message.Response.SetIsError(message.ResponseClassifier.IsErrorResponse(message)); } public override void Process(HttpMessage message, ReadOnlyMemory pipeline) @@ -39,7 +39,7 @@ public override void Process(HttpMessage message, ReadOnlyMemory /// Parses the error details from the provided . /// - /// The to parse. The will already be buffered. + /// The to parse. The + /// will already be buffered. /// The describing the parsed error details. /// Data to be applied to the property. /// true if successful, otherwise false. diff --git a/sdk/core/Azure.Core/src/Pipeline/RetryPolicy.cs b/sdk/core/Azure.Core/src/Pipeline/RetryPolicy.cs index ea6fb6c633cb..1c644f82ac36 100644 --- a/sdk/core/Azure.Core/src/Pipeline/RetryPolicy.cs +++ b/sdk/core/Azure.Core/src/Pipeline/RetryPolicy.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.ClientModel.Primitives; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.ExceptionServices; @@ -177,7 +178,7 @@ internal virtual void Wait(TimeSpan time, CancellationToken cancellationToken) /// /// This method can be overriden to control whether a request should be retried. It will be called for any response where - /// is true, or if an exception is thrown from any subsequent pipeline policies or the transport. + /// is true, or if an exception is thrown from any subsequent pipeline policies or the transport. /// This method will only be called for sync methods. /// /// The message containing the request and response. @@ -187,7 +188,7 @@ internal virtual void Wait(TimeSpan time, CancellationToken cancellationToken) /// /// This method can be overriden to control whether a request should be retried. It will be called for any response where - /// is true, or if an exception is thrown from any subsequent pipeline policies or the transport. + /// is true, or if an exception is thrown from any subsequent pipeline policies or the transport. /// This method will only be called for async methods. /// /// The message containing the request and response. diff --git a/sdk/core/Azure.Core/src/Response.cs b/sdk/core/Azure.Core/src/Response.cs index b602db8621c6..9e4e405768a2 100644 --- a/sdk/core/Azure.Core/src/Response.cs +++ b/sdk/core/Azure.Core/src/Response.cs @@ -2,10 +2,15 @@ // Licensed under the MIT License. using System; +using System.ClientModel.Primitives; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Threading; +using System.Threading.Tasks; using Azure.Core; +using Azure.Core.Buffers; +using Azure.Core.Pipeline; namespace Azure { @@ -13,23 +18,11 @@ namespace Azure /// Represents the HTTP response from the service. /// #pragma warning disable AZC0012 // Avoid single word type names - public abstract class Response : IDisposable + public abstract class Response : PipelineResponse #pragma warning restore AZC0012 // Avoid single word type names { - /// - /// Gets the HTTP status code. - /// - public abstract int Status { get; } - - /// - /// Gets the HTTP reason phrase. - /// - public abstract string ReasonPhrase { get; } - - /// - /// Gets the contents of HTTP response. Returns null for responses without content. - /// - public abstract Stream? ContentStream { get; set; } + // TODO(matell): The .NET Framework team plans to add BinaryData.Empty in dotnet/runtime#49670, and we can use it then. + private static readonly BinaryData s_EmptyBinaryData = new(Array.Empty()); /// /// Gets the client request id that was sent to the server as x-ms-client-request-id headers. @@ -39,59 +32,40 @@ public abstract class Response : IDisposable /// /// Get the HTTP response headers. /// - public virtual ResponseHeaders Headers => new ResponseHeaders(this); - - // TODO(matell): The .NET Framework team plans to add BinaryData.Empty in dotnet/runtime#49670, and we can use it then. - private static readonly BinaryData s_EmptyBinaryData = new BinaryData(Array.Empty()); + public new virtual ResponseHeaders Headers => new ResponseHeaders(this); /// /// Gets the contents of HTTP response, if it is available. /// /// - /// Throws when is not a . + /// Throws when response content + /// has not been buffered by the pipeline. /// - public virtual BinaryData Content + public override BinaryData Content { get { - if (ContentStream == null) - { - return s_EmptyBinaryData; - } - - MemoryStream? memoryContent = ContentStream as MemoryStream; - - if (memoryContent == null) + if (ContentStream is null || ContentStream is MemoryStream) { - throw new InvalidOperationException($"The response is not fully buffered."); + return BufferContent(); } - if (memoryContent.TryGetBuffer(out ArraySegment segment)) - { - return new BinaryData(segment.AsMemory()); - } - else - { - return new BinaryData(memoryContent.ToArray()); - } + throw new InvalidOperationException($"The response is not buffered."); } } - /// - /// Frees resources held by this instance. - /// - public abstract void Dispose(); + /// + protected override PipelineResponseHeaders HeadersCore + => new ResponseHeadersAdapter(Headers); - /// - /// Indicates whether the status code of the returned response is considered - /// an error code. - /// - public virtual bool IsError { get; internal set; } + internal void SetIsError(bool value) => IsErrorCore = value; internal HttpMessageSanitizer Sanitizer { get; set; } = HttpMessageSanitizer.Default; internal RequestFailedDetailsParser? RequestFailedDetailsParser { get; set; } + #region Abstract header methods + /// /// Returns header value if the header is stored in the collection. If header has multiple values they are going to be joined with a comma. /// @@ -121,6 +95,88 @@ public virtual BinaryData Content /// The enumerating in the response. protected internal abstract IEnumerable EnumerateHeaders(); + #endregion + + #region BufferContent implementation + + /// + public override BinaryData BufferContent(CancellationToken cancellationToken = default) + => BufferContentSyncOrAsync(cancellationToken, async: false).EnsureCompleted(); + + /// + public override async ValueTask BufferContentAsync(CancellationToken cancellationToken = default) + => await BufferContentSyncOrAsync(cancellationToken, async: true).ConfigureAwait(false); + + /// + /// Provide a default implementation of the abstract + /// method inherited from + /// . This is used by any types derived + /// from that don't override the BufferContent + /// methods. It is intended that any high-performance implementation + /// will override these methods instead of using the default + /// implementation. + /// + private async ValueTask BufferContentSyncOrAsync(CancellationToken cancellationToken, bool async) + { + // We can tell the content has been buffered and not overwritten by + // a call to the abstract ContentStream setter if ContentStream is + // an instance our private BufferContentStream type. + + if (ContentStream is BufferedContentStream bufferedContent) + { + return bufferedContent.Content; + } + + if (ContentStream is null) + { + return s_EmptyBinaryData; + } + + if (ContentStream is MemoryStream memoryStream) + { + return BufferedContentStream.FromBuffer(memoryStream); + } + + BufferedContentStream bufferStream = new(); + Stream? contentStream = ContentStream; + + if (async) + { + await contentStream.CopyToAsync(bufferStream, cancellationToken).ConfigureAwait(false); +#if NET6_0_OR_GREATER + await contentStream.DisposeAsync().ConfigureAwait(false); +#else + contentStream.Dispose(); +#endif + } + else + { + contentStream.CopyTo(bufferStream, cancellationToken); + contentStream.Dispose(); + } + + bufferStream.Position = 0; + ContentStream = bufferStream; + return bufferStream.Content; + } + + /// + /// Private Stream type to facilitate detecting whether abstract + /// ContentStream setter was called in order to invalidate cached + /// content returned from Content property. + /// + private class BufferedContentStream : MemoryStream + { + public static BinaryData FromBuffer(MemoryStream stream) + => stream.TryGetBuffer(out ArraySegment segment) ? + new BinaryData(segment.AsMemory()) : + new BinaryData(stream.ToArray()); + + public BinaryData Content => FromBuffer(this); + } + + #endregion + /// /// Creates a new instance of with the provided value and HTTP response. /// @@ -129,9 +185,7 @@ public virtual BinaryData Content /// The HTTP response. /// A new instance of with the provided value and HTTP response. public static Response FromValue(T value, Response response) - { - return new ValueResponse(response, value); - } + => new AzureCoreResponse(value, response); /// /// Returns the string representation of this . @@ -154,5 +208,51 @@ internal static void DisposeStreamIfNotBuffered(ref Stream? stream) stream = null; } } + + /// + /// Internal implementation of abstract . + /// + private class AzureCoreResponse : Response + { + public AzureCoreResponse(T value, Response response) + : base(value, response) { } + } + + /// + /// This adapter adapts the Azure.Core + /// type to the System.ClientModel + /// interface, so that can implement the + /// property inherited from + /// . + /// + private class ResponseHeadersAdapter : PipelineResponseHeaders + { + /// + /// Headers on the Azure.Core Response type to adapt to. + /// + private readonly ResponseHeaders _headers; + + public ResponseHeadersAdapter(ResponseHeaders headers) + { + _headers = headers; + } + + public override bool TryGetValue(string name, out string? value) + => _headers.TryGetValue(name, out value); + + public override bool TryGetValues(string name, out IEnumerable? values) + => _headers.TryGetValues(name, out values); + + public override IEnumerator> GetEnumerator() + => GetHeaderValues().GetEnumerator(); + + private IEnumerable> GetHeaderValues() + { + foreach (HttpHeader header in _headers) + { + yield return new KeyValuePair(header.Name, header.Value); + } + } + } } } diff --git a/sdk/core/Azure.Core/src/ResponseOfT.cs b/sdk/core/Azure.Core/src/ResponseOfT.cs index 0dbded789a53..2772247b9088 100644 --- a/sdk/core/Azure.Core/src/ResponseOfT.cs +++ b/sdk/core/Azure.Core/src/ResponseOfT.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.ClientModel; using System.ComponentModel; using System.Diagnostics; @@ -18,12 +19,40 @@ public abstract class Response : NullableResponse #pragma warning restore AZC0012 // Avoid single word type names #pragma warning restore SA1649 // File name should match first type name { + /// + /// Creates an instance of with no value + /// or . It is not intended for this constructor + /// to be called, as it will create an instance of + /// with null and + /// , neither of which is intended usage of this + /// type. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected Response() : base() + { + // Added for back-compat with GA APIs. Any type that derives from + // Response must provide an implementation for GetRawResponse that + // replaces DefaultResponse with the Response populated on HttpMessage + // during the call to pipeline.Send. + } + + /// + /// Creates an instance of from the provided + /// and . + /// + /// The value to return from + /// on the created instance. + /// The to return from + /// on the created + /// instance. + protected Response(T value, Response response) : base(value, response) { } + /// [EditorBrowsable(EditorBrowsableState.Never)] public override bool HasValue => true; /// - public override T Value => Value; + public override T Value => base.Value!; /// /// Returns the value of this object. diff --git a/sdk/core/Azure.Core/tests/NullableResponseOfTTests.cs b/sdk/core/Azure.Core/tests/NullableResponseOfTTests.cs index f1b83631ef7a..23a55986007b 100644 --- a/sdk/core/Azure.Core/tests/NullableResponseOfTTests.cs +++ b/sdk/core/Azure.Core/tests/NullableResponseOfTTests.cs @@ -26,6 +26,23 @@ public void DoesNotThrowWhenValueIsAccessedWithValue() Assert.That(target.ToString(), Does.Contain("test")); } + [Test] + public void ThrowsWhenValueIsAccessedWithNoValue_2_0() + { + var target = new TestClientResultNullableResponse(new MockResponse(200)); + Assert.Throws(() => { var val = target.Value; }); + Assert.That(target.ToString(), Does.Contain("")); + } + + [Test] + public void DoesNotThrowWhenValueIsAccessedWithValue_2_0() + { + var target = new TestClientResultNullableResponse(new MockResponse(200), "test"); + Assert.AreEqual("test", target.Value); + Assert.That(target.ToString(), Does.Not.Contain("")); + Assert.That(target.ToString(), Does.Contain("test")); + } + private class TestValueResponse : NullableResponse { private readonly bool _hasValue; @@ -45,11 +62,35 @@ public TestValueResponse(Response response) _value = default; _hasValue = false; } + public override bool HasValue => _hasValue; public override T Value => _hasValue ? _value : throw new Exception("has no value"); public override Response GetRawResponse() => _response; } + + // In contrast to TestValueResponse above, this derived type calls + // through to the protected constructors that take value and response. + private class TestClientResultNullableResponse : NullableResponse + { + private readonly bool _hasValue; + + public TestClientResultNullableResponse(Response response, T value) + : base(value, response) + { + _hasValue = true; + } + + public TestClientResultNullableResponse(Response response) + : base(default, response) + { + _hasValue = false; + } + + public override bool HasValue => _hasValue; + + public override T Value => _hasValue ? base.Value : throw new Exception("has no value"); + } } } diff --git a/sdk/core/Azure.Core/tests/ResponseTests.cs b/sdk/core/Azure.Core/tests/ResponseTests.cs index 32d5bbd3c43f..30a9dc8ed442 100644 --- a/sdk/core/Azure.Core/tests/ResponseTests.cs +++ b/sdk/core/Azure.Core/tests/ResponseTests.cs @@ -3,15 +3,26 @@ using System; using System.IO; -using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Azure.Core.Pipeline; using Azure.Core.TestFramework; -using NUnit.Framework; using Moq; +using NUnit.Framework; namespace Azure.Core.Tests { + [TestFixture(true)] + [TestFixture(false)] public class ResponseTests { + private readonly bool _isAsync; + + public ResponseTests(bool isAsync) + { + _isAsync = isAsync; + } + [Test] public void ValueIsAccessible() { @@ -170,6 +181,108 @@ public void CanMoqIsError() Assert.IsTrue(response.Object.IsError); } + [Test] + public void ResponseBuffersContentFromContentStream() + { + BinaryData mockContent = BinaryData.FromString("Mock content"); + MemoryStream mockContentStream = new(mockContent.ToArray()); + + MockResponse response = new(200); + response.ContentStream = mockContentStream; + + Assert.AreEqual(mockContent.ToString(), response.Content.ToString()); + } + + [Test] + public void BufferedResponseContentEmptyWhenNoResponseContent() + { + MockResponse response = new(200); + + Assert.AreEqual(0, response.Content.ToMemory().Length); + } + + [Test] + public void BufferedResponseContentAvailableAfterResponseDisposed() + { + BinaryData mockContent = BinaryData.FromString("Mock content"); + MemoryStream mockContentStream = new(mockContent.ToArray()); + + MockResponse response = new(200); + response.ContentStream = mockContentStream; + + Assert.AreEqual(mockContent.ToString(), response.Content.ToString()); + + response.Dispose(); + + Assert.AreEqual(mockContent.ToString(), response.Content.ToString()); + Assert.AreEqual(mockContent.ToString(), BinaryData.FromStream(response.ContentStream).ToString()); + } + + [Test] + public void UnbufferedResponseContentThrows() + { + BinaryData mockContent = BinaryData.FromString("Mock content"); + MockNetworkStream mockContentStream = new(mockContent.ToArray()); + + MockResponse response = new(200); + response.ContentStream = mockContentStream; + + Assert.Throws(() => { var content = response.Content; }); + } + + [Test] + public async Task BufferContentReturnsContentIfBuffered() + { + BinaryData mockContent = BinaryData.FromString("Mock content"); + MemoryStream mockContentStream = new(mockContent.ToArray()); + + MockResponse response = new(200); + response.ContentStream = mockContentStream; + + BinaryData content = await BufferContentAsync(response); + + Assert.AreEqual(response.Content.ToArray(), content.ToArray()); + } + + [Test] + public async Task BufferContentReturnsEmptyWhenNoResponseContent() + { + MockResponse response = new(200); + + BinaryData content = await BufferContentAsync(response); + + Assert.AreEqual(response.Content.ToArray(), content.ToArray()); + } + + [Test] + public async Task CachedResponseContentReplacedWhenContentStreamReplaced() + { + BinaryData mockContent = BinaryData.FromString("Mock content"); + MemoryStream mockContentStream = new(mockContent.ToArray()); + + MockResponse response = new(200); + response.ContentStream = mockContentStream; + + BinaryData content = await BufferContentAsync(response); + + Assert.AreEqual(response.Content.ToArray(), content.ToArray()); + + // Replace content stream + response.ContentStream = new MemoryStream(Encoding.UTF8.GetBytes("Mock content - 2")); + + Assert.AreEqual("Mock content - 2", response.Content.ToString()); + Assert.AreEqual("Mock content - 2", BinaryData.FromStream(response.ContentStream!).ToString()); + } + + #region Helpers + + private async Task BufferContentAsync(Response response) + { + return _isAsync ? + await response.BufferContentAsync() : + response.BufferContent(); + } + internal class TestPayload { public string Name { get; } @@ -222,5 +335,37 @@ public override void Write(byte[] buffer, int offset, int count) throw new System.NotImplementedException(); } } + + private class MockNetworkStream : ReadOnlyStream + { + private readonly MemoryStream _stream; + + public MockNetworkStream(byte[] content) + { + _stream = new MemoryStream(content); + } + + public override bool CanRead => true; + + public override bool CanSeek => false; + + public override long Length => _stream.Length; + + public override long Position + { + get => _stream.Position; + set => throw new NotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + => _stream.Read(buffer, offset, count); + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + } + + #endregion } } diff --git a/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj b/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj index 56b663e4306d..fbd3910a553f 100644 --- a/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj +++ b/sdk/tables/Azure.Data.Tables/src/Azure.Data.Tables.csproj @@ -12,7 +12,7 @@ true - + diff --git a/sdk/tables/Azure.Data.Tables/src/TableClient.cs b/sdk/tables/Azure.Data.Tables/src/TableClient.cs index 5bc5d7ad0f8e..908bc2ef4dd6 100644 --- a/sdk/tables/Azure.Data.Tables/src/TableClient.cs +++ b/sdk/tables/Azure.Data.Tables/src/TableClient.cs @@ -2,9 +2,8 @@ // Licensed under the MIT License. using System; +using System.ClientModel.Primitives; using System.Collections.Generic; -using System.Data; -using System.Diagnostics; using System.Linq; using System.Linq.Expressions; using System.Net; @@ -478,7 +477,7 @@ public virtual async Task> CreateAsync(CancellationToken can /// /// A controlling the request lifetime. /// The server returned an error. See for details returned from the server. - /// A containing properties of the table. If the table already exists, then is 409. The can be accessed via the GetRawResponse() method. + /// A containing properties of the table. If the table already exists, then is 409. The can be accessed via the GetRawResponse() method. public virtual Response CreateIfNotExists(CancellationToken cancellationToken = default) { using DiagnosticScope scope = _diagnostics.CreateScope($"{nameof(TableClient)}.{nameof(CreateIfNotExists)}"); @@ -517,7 +516,7 @@ public virtual Response CreateIfNotExists(CancellationToken cancellat /// /// A controlling the request lifetime. /// The server returned an error. See for details returned from the server. - /// A containing properties of the table. If the table already exists, then is 409. The can be accessed via the GetRawResponse() method. + /// A containing properties of the table. If the table already exists, then is 409. The can be accessed via the GetRawResponse() method. public virtual async Task> CreateIfNotExistsAsync(CancellationToken cancellationToken = default) { using DiagnosticScope scope = _diagnostics.CreateScope($"{nameof(TableClient)}.{nameof(CreateIfNotExists)}"); diff --git a/sdk/tables/Azure.Data.Tables/src/TableServiceClient.cs b/sdk/tables/Azure.Data.Tables/src/TableServiceClient.cs index 77ff4f7e890c..cd177ad4e2a1 100644 --- a/sdk/tables/Azure.Data.Tables/src/TableServiceClient.cs +++ b/sdk/tables/Azure.Data.Tables/src/TableServiceClient.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.ClientModel.Primitives; using System.Collections.Generic; using System.Linq.Expressions; using System.Net; @@ -685,7 +686,7 @@ public virtual async Task> CreateTableAsync(string tableName /// /// The name of the table to create. /// A controlling the request lifetime. - /// A containing properties of the table. If the table already exists, then is 409. The can be accessed via the GetRawResponse() method. + /// A containing properties of the table. If the table already exists, then is 409. The can be accessed via the GetRawResponse() method. public virtual Response CreateTableIfNotExists(string tableName, CancellationToken cancellationToken = default) { Argument.AssertNotNull(tableName, nameof(tableName)); @@ -724,7 +725,7 @@ public virtual Response CreateTableIfNotExists(string tableName, Canc /// /// The name of the table to create. /// A controlling the request lifetime. - /// A containing properties of the table. If the table already exists, then is 409. The can be accessed via the GetRawResponse() method. + /// A containing properties of the table. If the table already exists, then is 409. The can be accessed via the GetRawResponse() method. public virtual async Task> CreateTableIfNotExistsAsync(string tableName, CancellationToken cancellationToken = default) { Argument.AssertNotNull(tableName, nameof(tableName)); diff --git a/sdk/tables/Azure.Data.Tables/src/TablesRequestFailedDetailsParser.cs b/sdk/tables/Azure.Data.Tables/src/TablesRequestFailedDetailsParser.cs index 3642b101977f..16b643761d4e 100644 --- a/sdk/tables/Azure.Data.Tables/src/TablesRequestFailedDetailsParser.cs +++ b/sdk/tables/Azure.Data.Tables/src/TablesRequestFailedDetailsParser.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Globalization; -using System.IO; using System.Text.Json; using Azure.Core; @@ -15,10 +14,6 @@ public override bool TryParse(Response response, out ResponseError error, out ID { error = null; data = null; - if (response.ContentStream == null || !(response.ContentStream is MemoryStream)) - { - return false; - } try {