From 4de315b39ea525a13ef6ec712daf20e6ddb0f330 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 14 Mar 2024 15:28:59 +0800 Subject: [PATCH 01/32] Implement LRO rehydration with static method --- .../src/MockJsonModel.cs | 75 ++++++++++ sdk/core/Azure.Core/api/Azure.Core.net461.cs | 2 + sdk/core/Azure.Core/api/Azure.Core.net472.cs | 2 + sdk/core/Azure.Core/api/Azure.Core.net6.0.cs | 2 + .../api/Azure.Core.netstandard2.0.cs | 2 + sdk/core/Azure.Core/src/Azure.Core.csproj | 3 + .../src/Internal/GenericOperationSource.cs | 22 +++ .../src/Internal/RehydrationOperation.cs | 44 ++++++ .../src/Internal/RehydrationOperationOfT.cs | 53 +++++++ sdk/core/Azure.Core/src/Operation.cs | 21 +++ .../Shared/NextLinkOperationImplementation.cs | 134 ++++++++++++++++-- .../src/Shared/OperationInternal.cs | 12 +- .../src/Shared/OperationInternalBase.cs | 7 +- .../src/Shared/OperationInternalOfT.cs | 80 ++++++++++- .../Azure.Core/src/Shared/OperationPoller.cs | 2 - .../Azure.Core/tests/Azure.Core.Tests.csproj | 2 - .../NextLinkOperationImplementationTests.cs | 72 ++++++++++ sdk/core/Azure.Core/tests/OperationTests.cs | 32 +++++ .../tests/RehydrationOperationTests.cs | 63 ++++++++ .../Azure.Core/tests/RehydrationTokenTests.cs | 2 +- 20 files changed, 604 insertions(+), 28 deletions(-) create mode 100644 sdk/core/Azure.Core.TestFramework/src/MockJsonModel.cs create mode 100644 sdk/core/Azure.Core/src/Internal/GenericOperationSource.cs create mode 100644 sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs create mode 100644 sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs create mode 100644 sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs create mode 100644 sdk/core/Azure.Core/tests/RehydrationOperationTests.cs diff --git a/sdk/core/Azure.Core.TestFramework/src/MockJsonModel.cs b/sdk/core/Azure.Core.TestFramework/src/MockJsonModel.cs new file mode 100644 index 000000000000..2b2774b4b8ef --- /dev/null +++ b/sdk/core/Azure.Core.TestFramework/src/MockJsonModel.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.ClientModel.Primitives; +using System.IO; +using System.Text.Json; +using Azure.Core.Serialization; + +namespace Azure.Core.TestFramework +{ + public class MockJsonModel : IJsonModel + { + internal MockJsonModel() + { + } + + public int IntValue { get; set; } + + public string StringValue { get; set; } + + public byte[] Utf8BytesValue { get; } + + public MockJsonModel(int intValue, string stringValue) + { + IntValue = intValue; + StringValue = stringValue; + + dynamic json = BinaryData.FromString("{}").ToDynamicFromJson(JsonPropertyNames.CamelCase); + json.IntValue = IntValue; + json.StringValue = StringValue; + + MemoryStream stream = new(); + using Utf8JsonWriter writer = new Utf8JsonWriter(stream); + + writer.WriteStartObject(); + writer.WriteNumber("IntValue", IntValue); + writer.WriteString("StringValue", StringValue); + writer.WriteEndObject(); + + writer.Flush(); + Utf8BytesValue = stream.ToArray(); + } + + MockJsonModel IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) + { + dynamic json = data.ToDynamicFromJson(JsonPropertyNames.CamelCase); + return new MockJsonModel(json.IntValue, json.StringValue); + } + + MockJsonModel IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) + { + using JsonDocument doc = JsonDocument.ParseValue(ref reader); + int intValue = doc.RootElement.GetProperty("IntValue").GetInt32(); + string stringValue = doc.RootElement.GetProperty("StringValue").GetString()!; + return new MockJsonModel(intValue, stringValue); + } + + string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) + => "J"; + + BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) + { + return BinaryData.FromBytes(Utf8BytesValue); + } + + void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) + { + writer.WriteStartObject(); + writer.WriteNumber("IntValue", IntValue); + writer.WriteString("StringValue", StringValue); + writer.WriteEndObject(); + } + } +} diff --git a/sdk/core/Azure.Core/api/Azure.Core.net461.cs b/sdk/core/Azure.Core/api/Azure.Core.net461.cs index 7d6246afcef8..79ded6594ef5 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net461.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net461.cs @@ -138,6 +138,8 @@ protected Operation() { } public override int GetHashCode() { throw null; } public abstract Azure.Response GetRawResponse(); public virtual Azure.Core.RehydrationToken? GetRehydrationToken() { throw null; } + public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken? rehydrationToken, Azure.Core.ClientOptions? options = null) { throw null; } + public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken? rehydrationToken, Azure.Core.ClientOptions? options = null) where T : notnull { throw null; } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public override string? ToString() { throw null; } public abstract Azure.Response UpdateStatus(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); diff --git a/sdk/core/Azure.Core/api/Azure.Core.net472.cs b/sdk/core/Azure.Core/api/Azure.Core.net472.cs index 7d6246afcef8..79ded6594ef5 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net472.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net472.cs @@ -138,6 +138,8 @@ protected Operation() { } public override int GetHashCode() { throw null; } public abstract Azure.Response GetRawResponse(); public virtual Azure.Core.RehydrationToken? GetRehydrationToken() { throw null; } + public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken? rehydrationToken, Azure.Core.ClientOptions? options = null) { throw null; } + public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken? rehydrationToken, Azure.Core.ClientOptions? options = null) where T : notnull { throw null; } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public override string? ToString() { throw null; } public abstract Azure.Response UpdateStatus(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); 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 948e617f1186..ca69bb6b35d7 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net6.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net6.0.cs @@ -138,6 +138,8 @@ protected Operation() { } public override int GetHashCode() { throw null; } public abstract Azure.Response GetRawResponse(); public virtual Azure.Core.RehydrationToken? GetRehydrationToken() { throw null; } + public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken? rehydrationToken, Azure.Core.ClientOptions? options = null) { throw null; } + public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken? rehydrationToken, Azure.Core.ClientOptions? options = null) where T : notnull { throw null; } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public override string? ToString() { throw null; } public abstract Azure.Response UpdateStatus(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); 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 7d6246afcef8..79ded6594ef5 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs @@ -138,6 +138,8 @@ protected Operation() { } public override int GetHashCode() { throw null; } public abstract Azure.Response GetRawResponse(); public virtual Azure.Core.RehydrationToken? GetRehydrationToken() { throw null; } + public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken? rehydrationToken, Azure.Core.ClientOptions? options = null) { throw null; } + public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken? rehydrationToken, Azure.Core.ClientOptions? options = null) where T : notnull { throw null; } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public override string? ToString() { throw null; } public abstract Azure.Response UpdateStatus(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); diff --git a/sdk/core/Azure.Core/src/Azure.Core.csproj b/sdk/core/Azure.Core/src/Azure.Core.csproj index 792e1cd6d0a5..d3e4b7349a73 100644 --- a/sdk/core/Azure.Core/src/Azure.Core.csproj +++ b/sdk/core/Azure.Core/src/Azure.Core.csproj @@ -59,12 +59,15 @@ + + + diff --git a/sdk/core/Azure.Core/src/Internal/GenericOperationSource.cs b/sdk/core/Azure.Core/src/Internal/GenericOperationSource.cs new file mode 100644 index 000000000000..5d359c58d2ba --- /dev/null +++ b/sdk/core/Azure.Core/src/Internal/GenericOperationSource.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Primitives; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; + +namespace Azure.Core +{ + internal class GenericOperationSource<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] T> : IOperationSource where T : notnull + { + T IOperationSource.CreateResult(Response response, CancellationToken cancellationToken) + => CreateResult(response); + + ValueTask IOperationSource.CreateResultAsync(Response response, CancellationToken cancellationToken) + => new ValueTask(CreateResult(response)); + + private T CreateResult(Response response) + => (T)ModelReaderWriter.Read(response.Content, typeof(T))!; + } +} diff --git a/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs b/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs new file mode 100644 index 000000000000..28656d8afebe --- /dev/null +++ b/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable disable + +using System; +using System.Threading; +using System.Threading.Tasks; +using Azure.Core.Pipeline; + +namespace Azure.Core +{ + internal class RehydrationOperation : Operation + { + private readonly NextLinkOperationImplementation _nextLinkOperation; + private readonly OperationInternal _operation; + + public RehydrationOperation(HttpPipeline pipeline, RehydrationToken? rehydrationToken, ClientOptions options = null) + { + AssertNotNull(pipeline, nameof(pipeline)); + AssertNotNull(rehydrationToken, nameof(rehydrationToken)); + _nextLinkOperation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(pipeline, rehydrationToken); + _operation = new OperationInternal(_nextLinkOperation, new ClientDiagnostics(options ?? ClientOptions.Default), null, requestMethod: _nextLinkOperation.RequestMethod); + } + + public override string Id => _nextLinkOperation?.OperationId ?? null; + + public override bool HasCompleted => _operation.HasCompleted; + + public override Response GetRawResponse() => _operation.RawResponse; + + public override Response UpdateStatus(CancellationToken cancellationToken = default) => _operation.UpdateStatus(cancellationToken); + + public override ValueTask UpdateStatusAsync(CancellationToken cancellationToken = default) => _operation.UpdateStatusAsync(cancellationToken); + + private static void AssertNotNull(T value, string name) + { + if (value is null) + { + throw new ArgumentNullException(name); + } + } + } +} diff --git a/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs b/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs new file mode 100644 index 000000000000..07f538f6343f --- /dev/null +++ b/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable disable + +using System; +using System.Threading; +using System.Threading.Tasks; +using Azure.Core.Pipeline; + +namespace Azure.Core +{ +#pragma warning disable SA1649 // File name should match first type name + internal class RehydrationOperation : Operation where T: notnull +#pragma warning restore SA1649 // File name should match first type name + { + private readonly OperationInternal _operation; + private readonly NextLinkOperationImplementation _nextLinkOperation; + + public RehydrationOperation(HttpPipeline pipeline, RehydrationToken? rehydrationToken, ClientOptions options = null) + { + if (pipeline is null) + { + throw new ArgumentNullException(nameof(pipeline)); + } + + if (rehydrationToken is null) + { + throw new ArgumentNullException(nameof(rehydrationToken)); + } + + IOperationSource source = new GenericOperationSource(); + _nextLinkOperation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(pipeline, rehydrationToken); + var operation = NextLinkOperationImplementation.Create(source, _nextLinkOperation); + var clientDiagnostics = new ClientDiagnostics(options ?? ClientOptions.Default); + _operation = new OperationInternal(operation, clientDiagnostics, null); + } + + public override T Value => _operation.Value; + + public override bool HasValue => _operation.HasValue; + + public override string Id => _nextLinkOperation.OperationId ?? null; + + public override bool HasCompleted => _operation.HasCompleted; + + public override Response GetRawResponse() => _operation.RawResponse; + + public override Response UpdateStatus(CancellationToken cancellationToken = default) => _operation.UpdateStatus(cancellationToken); + + public override ValueTask UpdateStatusAsync(CancellationToken cancellationToken = default) => _operation.UpdateStatusAsync(cancellationToken); + } +} diff --git a/sdk/core/Azure.Core/src/Operation.cs b/sdk/core/Azure.Core/src/Operation.cs index 7ff18388e0f8..085bd011b352 100644 --- a/sdk/core/Azure.Core/src/Operation.cs +++ b/sdk/core/Azure.Core/src/Operation.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Azure.Core; +using Azure.Core.Pipeline; namespace Azure { @@ -16,6 +17,26 @@ namespace Azure public abstract class Operation #pragma warning restore AZC0012 // Avoid single word type names { + /// + /// Rehydrates an operation from a . + /// + /// The Http pipeline. + /// The rehydration token. + /// The client options. + /// The long-running operation. + public static Operation Rehydrate(HttpPipeline pipeline, RehydrationToken? rehydrationToken, ClientOptions? options = null) where T : notnull + => new RehydrationOperation(pipeline, rehydrationToken, options); + + /// + /// Rehydrates an operation from a . + /// + /// The Http pipeline. + /// The rehydration token. + /// The client options. + /// The long-running operation. + public static Operation Rehydrate(HttpPipeline pipeline, RehydrationToken? rehydrationToken, ClientOptions? options = null) + => new RehydrationOperation(pipeline, rehydrationToken, options); + /// /// Get a token that can be used to rehydrate the operation. /// diff --git a/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs b/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs index a61e498730ee..e69d45b60c9d 100644 --- a/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs +++ b/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs @@ -4,8 +4,9 @@ #nullable enable using System; +using System.ClientModel.Primitives; +using System.Collections.Generic; using System.Linq; -using System.IO; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -22,7 +23,6 @@ internal class NextLinkOperationImplementation : IOperation private readonly HeaderSource _headerSource; private readonly Uri _startRequestUri; private readonly OperationFinalStateVia _finalStateVia; - private readonly RequestMethod _requestMethod; private readonly HttpPipeline _pipeline; private readonly string? _apiVersion; @@ -38,7 +38,7 @@ public static IOperation Create( bool skipApiVersionOverride = false, string? apiVersionOverrideValue = null) { - string? apiVersionStr = null; + string? apiVersionStr; if (apiVersionOverrideValue is not null) { apiVersionStr = apiVersionOverrideValue; @@ -70,6 +70,60 @@ public static IOperation Create( return new OperationToOperationOfT(operationSource, operation); } + public static IOperation Create( + IOperationSource operationSource, + IOperation operation) + => new OperationToOperationOfT(operationSource, operation); + + public string? OperationId { get; } + public RequestMethod RequestMethod { get; } + + public static IOperation Create( + HttpPipeline pipeline, + RehydrationToken? rehydrationToken, + string? apiVersionOverride = null) + { + AssertNotNull(rehydrationToken, nameof(rehydrationToken)); + + var lroDetails = ModelReaderWriter.Write(rehydrationToken!, ModelReaderWriterOptions.Json).ToObjectFromJson>(); + if (!Uri.TryCreate(lroDetails["initialUri"], UriKind.Absolute, out var startRequestUri)) + { + throw new ArgumentException("Invalid initial URI from rehydration token"); + } + + if (!lroDetails.TryGetValue("nextRequestUri", out var nextRequestUri)) + { + throw new ArgumentException("Invalid next request URI from rehydration token"); + } + + RequestMethod requestMethod = new RequestMethod(lroDetails["requestMethod"]); + string lastKnownLocation = lroDetails["lastKnownLocation"]; + + OperationFinalStateVia finalStateVia; + if (Enum.IsDefined(typeof(OperationFinalStateVia), lroDetails["finalStateVia"])) + { + finalStateVia = (OperationFinalStateVia)Enum.Parse(typeof(OperationFinalStateVia), lroDetails["finalStateVia"]); + } + else + { + finalStateVia = OperationFinalStateVia.Location; + } + + HeaderSource headerSource; + if (Enum.IsDefined(typeof(HeaderSource), lroDetails["headerSource"])) + { + headerSource = (HeaderSource)Enum.Parse(typeof(HeaderSource), lroDetails["headerSource"]); + } + else + { + headerSource = HeaderSource.None; + } + + string? apiVersionStr = apiVersionOverride ?? (TryGetApiVersion(startRequestUri, out ReadOnlySpan apiVersion) ? apiVersion.ToString() : null); + + return new NextLinkOperationImplementation(pipeline, requestMethod, startRequestUri, nextRequestUri, headerSource, lastKnownLocation, finalStateVia, apiVersionStr); + } + private NextLinkOperationImplementation( HttpPipeline pipeline, RequestMethod requestMethod, @@ -80,7 +134,14 @@ private NextLinkOperationImplementation( OperationFinalStateVia finalStateVia, string? apiVersion) { - _requestMethod = requestMethod; + AssertNotNull(pipeline, nameof(pipeline)); + AssertNotNull(requestMethod, nameof(requestMethod)); + AssertNotNull(startRequestUri, nameof(startRequestUri)); + AssertNotNull(nextRequestUri, nameof(nextRequestUri)); + AssertNotNull(headerSource, nameof(headerSource)); + AssertNotNull(finalStateVia, nameof(finalStateVia)); + + RequestMethod = requestMethod; _headerSource = headerSource; _startRequestUri = startRequestUri; _nextRequestUri = nextRequestUri; @@ -88,6 +149,53 @@ private NextLinkOperationImplementation( _finalStateVia = finalStateVia; _pipeline = pipeline; _apiVersion = apiVersion; + OperationId = ParseOperationId(nextRequestUri); + } + + private static string? ParseOperationId(string nextRequestUri) + { + if (Uri.TryCreate(nextRequestUri, UriKind.Absolute, out var uri)) + { + return uri.Segments.LastOrDefault(); + } + return null; + } + + public RehydrationToken GetRehydrationToken() + => GetRehydrationToken(RequestMethod, _startRequestUri, _nextRequestUri, _headerSource.ToString(), _lastKnownLocation, _finalStateVia.ToString()); + + public static RehydrationToken GetRehydrationToken( + RequestMethod requestMethod, + Uri startRequestUri, + Response response, + OperationFinalStateVia finalStateVia, + bool skipApiVersionOverride = false, + string? apiVersionOverrideValue = null) + { + string? apiVersionStr; + if (apiVersionOverrideValue is not null) + { + apiVersionStr = apiVersionOverrideValue; + } + else + { + apiVersionStr = !skipApiVersionOverride && TryGetApiVersion(startRequestUri, out ReadOnlySpan apiVersion) ? apiVersion.ToString() : null; + } + var headerSource = GetHeaderSource(requestMethod, startRequestUri, response, apiVersionStr, out var nextRequestUri); + response.Headers.TryGetValue("Location", out var lastKnownLocation); + return GetRehydrationToken(requestMethod, startRequestUri, nextRequestUri, headerSource.ToString(), lastKnownLocation, finalStateVia.ToString()); + } + + public static RehydrationToken GetRehydrationToken( + RequestMethod requestMethod, + Uri startRequestUri, + string nextRequestUri, + string headerSource, + string? lastKnownLocation, + string finalStateVia) + { + var data = new BinaryData(new { requestMethod = requestMethod.ToString(), initialUri = startRequestUri.AbsoluteUri, nextRequestUri, headerSource, lastKnownLocation, finalStateVia }); + return ModelReaderWriter.Read(data); } public async ValueTask UpdateStateAsync(bool async, CancellationToken cancellationToken) @@ -107,7 +215,7 @@ public async ValueTask UpdateStateAsync(bool async, Cancellation ? await GetResponseAsync(async, finalUri, cancellationToken).ConfigureAwait(false) : response; - return GetOperationStateFromFinalResponse(_requestMethod, finalResponse); + return GetOperationStateFromFinalResponse(RequestMethod, finalResponse); } UpdateNextRequestUri(response.Headers); @@ -226,7 +334,7 @@ internal static bool TryGetApiVersion(Uri startRequestUri, out ReadOnlySpan= 200 and <= 204) { - if (response.ContentStream is {Length: > 0}) + if (response.ContentStream is { Length: > 0 }) { try { @@ -399,6 +507,14 @@ private static HeaderSource GetHeaderSource(RequestMethod requestMethod, Uri req return HeaderSource.None; } + private static void AssertNotNull(T value, string name) + { + if (value is null) + { + throw new ArgumentNullException(name); + } + } + private enum HeaderSource { None, diff --git a/sdk/core/Azure.Core/src/Shared/OperationInternal.cs b/sdk/core/Azure.Core/src/Shared/OperationInternal.cs index 14da62c52f59..1892fb5def0f 100644 --- a/sdk/core/Azure.Core/src/Shared/OperationInternal.cs +++ b/sdk/core/Azure.Core/src/Shared/OperationInternal.cs @@ -83,19 +83,21 @@ internal class OperationInternal : OperationInternalBase /// /// The attributes to use during diagnostic scope creation. /// The delay strategy to use. Default is . + /// The Http request method public OperationInternal(IOperation operation, ClientDiagnostics clientDiagnostics, - Response rawResponse, + Response? rawResponse, string? operationTypeName = null, IEnumerable>? scopeAttributes = null, - DelayStrategy? fallbackStrategy = null) - :base(clientDiagnostics, operationTypeName ?? operation.GetType().Name, scopeAttributes, fallbackStrategy) + DelayStrategy? fallbackStrategy = null, + RequestMethod? requestMethod = null) + : base(clientDiagnostics, operationTypeName ?? operation.GetType().Name, scopeAttributes, fallbackStrategy, requestMethod) { - _internalOperation = new OperationInternal(new OperationToOperationOfTProxy(operation), clientDiagnostics, rawResponse, operationTypeName ?? operation.GetType().Name, scopeAttributes, fallbackStrategy); + _internalOperation = new OperationInternal(new OperationToOperationOfTProxy(operation), clientDiagnostics, rawResponse, operationTypeName ?? operation.GetType().Name, scopeAttributes, fallbackStrategy, requestMethod); } private OperationInternal(OperationState finalState) - :base(finalState.RawResponse) + : base(finalState.RawResponse) { _internalOperation = finalState.HasSucceeded ? OperationInternal.Succeeded(finalState.RawResponse, default) diff --git a/sdk/core/Azure.Core/src/Shared/OperationInternalBase.cs b/sdk/core/Azure.Core/src/Shared/OperationInternalBase.cs index dad6480c22ce..54dc3f1a416f 100644 --- a/sdk/core/Azure.Core/src/Shared/OperationInternalBase.cs +++ b/sdk/core/Azure.Core/src/Shared/OperationInternalBase.cs @@ -22,8 +22,9 @@ internal abstract class OperationInternalBase private readonly string _waitForCompletionResponseScopeName; protected readonly string _updateStatusScopeName; protected readonly string _waitForCompletionScopeName; + protected readonly RequestMethod? _requestMethod; - protected OperationInternalBase(Response rawResponse) + protected OperationInternalBase(Response rawResponse, RequestMethod? requestMethod = null) { _diagnostics = new ClientDiagnostics(ClientOptions.Default); _updateStatusScopeName = string.Empty; @@ -32,9 +33,10 @@ protected OperationInternalBase(Response rawResponse) _scopeAttributes = default; _fallbackStrategy = default; _responseLock = new AsyncLockWithValue(rawResponse); + _requestMethod = requestMethod; } - protected OperationInternalBase(ClientDiagnostics clientDiagnostics, string operationTypeName, IEnumerable>? scopeAttributes = null, DelayStrategy? fallbackStrategy = null) + protected OperationInternalBase(ClientDiagnostics clientDiagnostics, string operationTypeName, IEnumerable>? scopeAttributes = null, DelayStrategy? fallbackStrategy = null, RequestMethod? requestMethod = null) { _diagnostics = clientDiagnostics; _updateStatusScopeName = $"{operationTypeName}.{nameof(UpdateStatus)}"; @@ -43,6 +45,7 @@ protected OperationInternalBase(ClientDiagnostics clientDiagnostics, string oper _scopeAttributes = scopeAttributes?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); _fallbackStrategy = fallbackStrategy; _responseLock = new AsyncLockWithValue(); + _requestMethod = requestMethod; } /// diff --git a/sdk/core/Azure.Core/src/Shared/OperationInternalOfT.cs b/sdk/core/Azure.Core/src/Shared/OperationInternalOfT.cs index 20bb1602217d..4543146f2817 100644 --- a/sdk/core/Azure.Core/src/Shared/OperationInternalOfT.cs +++ b/sdk/core/Azure.Core/src/Shared/OperationInternalOfT.cs @@ -4,7 +4,10 @@ #nullable enable using System; +using System.ClientModel.Primitives; using System.Collections.Generic; +using System.IO; +using System.Net; using System.Threading; using System.Threading.Tasks; using Azure.Core.Pipeline; @@ -53,7 +56,7 @@ internal class OperationInternal : OperationInternalBase { private readonly IOperation _operation; private readonly AsyncLockWithValue> _stateLock; - private Response _rawResponse; + private Response? _rawResponse; /// /// Initializes a new instance of the class in a final successful state. @@ -93,13 +96,15 @@ internal class OperationInternal : OperationInternalBase /// The attributes to use during diagnostic scope creation. /// The delay strategy when Retry-After header is not present. When it is present, the longer of the two delays will be used. /// Default is . + /// The Http request method. public OperationInternal(IOperation operation, ClientDiagnostics clientDiagnostics, - Response rawResponse, + Response? rawResponse, string? operationTypeName = null, IEnumerable>? scopeAttributes = null, - DelayStrategy? fallbackStrategy = null) - : base(clientDiagnostics, operationTypeName ?? operation.GetType().Name, scopeAttributes, fallbackStrategy) + DelayStrategy? fallbackStrategy = null, + RequestMethod? requetMethod = null) + : base(clientDiagnostics, operationTypeName ?? operation.GetType().Name, scopeAttributes, fallbackStrategy, requetMethod) { _operation = operation; _rawResponse = rawResponse; @@ -116,7 +121,7 @@ private OperationInternal(OperationState finalState) _stateLock = new AsyncLockWithValue>(finalState); } - public override Response RawResponse => _stateLock.TryGetValue(out var state) ? state.RawResponse : _rawResponse; + public override Response RawResponse => _stateLock.TryGetValue(out var state) ? state.RawResponse : _rawResponse ?? throw new InvalidOperationException("The operation has not completed yet."); public override bool HasCompleted => _stateLock.HasValue; @@ -265,7 +270,7 @@ protected override async ValueTask UpdateStatusAsync(bool async, Cance } asyncLock.SetValue(state); - return GetResponseFromState(state); + return GetResponseFromState(state, _requestMethod); } catch (Exception e) { @@ -274,16 +279,77 @@ protected override async ValueTask UpdateStatusAsync(bool async, Cance } } - private static Response GetResponseFromState(OperationState state) + private static Response GetResponseFromState(OperationState state, RequestMethod? requestmethod = null) { if (state.HasSucceeded) { return state.RawResponse; } + // if this is a fake delete LRO with 404, just return empty response with 204 + if (RequestMethod.Delete == requestmethod && state.RawResponse.Status == 404) + { + return new EmptyResponse(HttpStatusCode.NoContent); + } + throw state.OperationFailedException!; } + /// + /// This is only used for fake delete LRO, we just want to return an empty response with 204 to the user for this case. + /// + private sealed class EmptyResponse : Response + { + public EmptyResponse(HttpStatusCode status) + { + Status = (int)status; + ReasonPhrase = status.ToString(); + } + + public override int Status { get; } + + public override string ReasonPhrase { get; } + + public override Stream? ContentStream { get => null; set => throw new System.NotImplementedException(); } + public override string ClientRequestId { get => string.Empty; set => throw new System.NotImplementedException(); } + + public override void Dispose() + { + } + + /// +#if HAS_INTERNALS_VISIBLE_CORE + internal +#endif + protected override bool ContainsHeader(string name) => false; + + /// +#if HAS_INTERNALS_VISIBLE_CORE + internal +#endif + protected override IEnumerable EnumerateHeaders() => Array.Empty(); + + /// +#if HAS_INTERNALS_VISIBLE_CORE + internal +#endif + protected override bool TryGetHeader(string name, out string value) + { + value = string.Empty; + return false; + } + + /// +#if HAS_INTERNALS_VISIBLE_CORE + internal +#endif + protected override bool TryGetHeaderValues(string name, out IEnumerable values) + { + values = Array.Empty(); + return false; + } + } + private class FinalOperation : IOperation { public ValueTask> UpdateStateAsync(bool async, CancellationToken cancellationToken) diff --git a/sdk/core/Azure.Core/src/Shared/OperationPoller.cs b/sdk/core/Azure.Core/src/Shared/OperationPoller.cs index 4d472f8c3bf9..4cbd975ebb94 100644 --- a/sdk/core/Azure.Core/src/Shared/OperationPoller.cs +++ b/sdk/core/Azure.Core/src/Shared/OperationPoller.cs @@ -4,11 +4,9 @@ #nullable enable using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Azure.Core.Pipeline; -using Azure.Core.Shared; namespace Azure.Core { diff --git a/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj b/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj index 0faa6ea6ffe1..30aff8487a9e 100644 --- a/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj +++ b/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj @@ -41,10 +41,8 @@ - - diff --git a/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs b/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs new file mode 100644 index 000000000000..7333014f238f --- /dev/null +++ b/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Azure.Core.Pipeline; +using NUnit.Framework; + +namespace Azure.Core.Tests +{ + public class NextLinkOperationImplementationTests + { + [Test] + public void ConstructRehydrationTokenTest() + { + var requetMethod = RequestMethod.Get; + var startRequestUri = new Uri("https://test"); + var nextRequestUri = "nextRequestUri"; + var headerSource = "None"; + string lastKnownLocation = null; + var finalStateVia = OperationFinalStateVia.OperationLocation.ToString(); + var token = NextLinkOperationImplementation.GetRehydrationToken(requetMethod, startRequestUri, nextRequestUri, headerSource, lastKnownLocation, finalStateVia); + Assert.AreEqual(requetMethod, token.RequestMethod); + Assert.AreEqual(startRequestUri, token.InitialUri); + Assert.AreEqual(nextRequestUri, token.NextRequestUri); + Assert.AreEqual(headerSource, token.HeaderSource); + Assert.AreEqual(lastKnownLocation, token.LastKnownLocation); + Assert.AreEqual(finalStateVia, token.FinalStateVia); + } + + [Test] + public void ConstructNextLinkOperationTest() + { + var operationId = Guid.NewGuid().ToString(); + var requestMethod = RequestMethod.Delete; + var rehydrationToken = new RehydrationToken(null, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test", requestMethod, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); + var operation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(HttpPipelineBuilder.Build(new MockClientOptions()), rehydrationToken, null); + Assert.NotNull(operation); + Assert.AreEqual(operationId, operation.OperationId); + Assert.AreEqual(requestMethod, operation.RequestMethod); + } + + [Test] + public void GetNullOperationIdWithIvalidNextRequestUri() + { + var rehydrationToken = new RehydrationToken(null, null, "None", $"invalidNextRequestUri", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); + var operation = NextLinkOperationImplementation.Create(HttpPipelineBuilder.Build(new MockClientOptions()), rehydrationToken, null); + Assert.Null(((NextLinkOperationImplementation)operation).OperationId); + } + + [Test] + public void ThrowOnNextLinkOperationImplementationCreateWithNullRehydrationToken() + { + Assert.Throws(() => NextLinkOperationImplementation.Create(HttpPipelineBuilder.Build(new MockClientOptions()), null)); + } + + [Test] + public void ThrowOnInvalidUri() + { + Assert.Throws(() => NextLinkOperationImplementation.Create(HttpPipelineBuilder.Build(new MockClientOptions()), default(RehydrationToken))); + } + + [Test] + public void ThrowOnNextLinkOperationImplementationCreateWithNullHttpPipeline() + { + Assert.Throws(() => NextLinkOperationImplementation.Create(null, new RehydrationToken(null, null, "None", "nextRequestUri", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()), null)); + } + + private class MockClientOptions : ClientOptions + { + } + } +} diff --git a/sdk/core/Azure.Core/tests/OperationTests.cs b/sdk/core/Azure.Core/tests/OperationTests.cs index 71aa77665920..ec15ca3d7604 100644 --- a/sdk/core/Azure.Core/tests/OperationTests.cs +++ b/sdk/core/Azure.Core/tests/OperationTests.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Azure; +using Azure.Core.Pipeline; using Azure.Core.TestFramework; using Azure.Core.Tests.TestFramework; using NUnit; @@ -133,5 +134,36 @@ public void OperationId() var operation = new TestOperation(testId, TimeSpan.Zero, 0, null); Assert.AreEqual(testId, operation.Id); } + + [Test] + public void RehydrateOperation() + { + var operationId = Guid.NewGuid().ToString(); + var rehydrationToken = new RehydrationToken(null, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); + var operation = new RehydrationOperation(HttpPipelineBuilder.Build(new MockClientOptions()), rehydrationToken); + Assert.NotNull(operation); + Assert.AreEqual(operationId, operation.Id); + Assert.Throws(() => operation.GetRawResponse()); + Assert.False(operation.HasCompleted); + operation.UpdateStatus(); + } + + [Test] + public void RehydrateOperationOfT() + { + var operationId = Guid.NewGuid().ToString(); + var rehydrationToken = new RehydrationToken(null, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); + var operation = new RehydrationOperation(HttpPipelineBuilder.Build(new MockClientOptions()), rehydrationToken); + Assert.NotNull(operation); + Assert.AreEqual(operationId, operation.Id); + Assert.Throws(() => operation.GetRawResponse()); + Assert.False(operation.HasCompleted); + Assert.Throws(() => { var value = operation.Value; }); + Assert.False(operation.HasValue); + } + + private class MockClientOptions : ClientOptions + { + } } } diff --git a/sdk/core/Azure.Core/tests/RehydrationOperationTests.cs b/sdk/core/Azure.Core/tests/RehydrationOperationTests.cs new file mode 100644 index 000000000000..9a747e885daf --- /dev/null +++ b/sdk/core/Azure.Core/tests/RehydrationOperationTests.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.ClientModel.Primitives; +using Azure.Core.Pipeline; +using Azure.Core.TestFramework; +using NUnit.Framework; + +namespace Azure.Core.Tests +{ + public class RehydrationOperationTests + { + [Test] + public void ThrowOnNullArgument() + { + Assert.Throws(() => new RehydrationOperation(null, new RehydrationToken())); + } + + [Test] + public void ConstructOperationForRehydration() + { + var mockResponse = new MockResponse(200); + var mockJsonModel = new MockJsonModel(1, "a"); + mockResponse.SetContent(ModelReaderWriter.Write(mockJsonModel).ToString()); + var transport = new MockTransport(mockResponse); + var pipeline = new HttpPipeline(transport, default); + var operationId = Guid.NewGuid().ToString(); + var rehydrationToken = new RehydrationToken(null, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); + var operation = new RehydrationOperation(pipeline, rehydrationToken); + Assert.NotNull(operation); + Assert.AreEqual(operationId, operation.Id); + Assert.Throws(() => operation.GetRawResponse()); + Assert.False(operation.HasCompleted); + + operation.UpdateStatus(); + var rawResponse = operation.GetRawResponse(); + Assert.AreEqual(200, rawResponse.Status); + } + + [Test] + public void ConstructOperationOfTForRehydration() + { + var mockResponse = new MockResponse(200); + var mockJsonModel = new MockJsonModel(1, "a"); + mockResponse.SetContent(ModelReaderWriter.Write(mockJsonModel).ToString()); + var transport = new MockTransport(mockResponse); + var pipeline = new HttpPipeline(transport, default); + var operationId = Guid.NewGuid().ToString(); + var rehydrationToken = new RehydrationToken(null, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); + var operation = new RehydrationOperation(pipeline, rehydrationToken); + + operation.UpdateStatus(); + var rawResponse = operation.GetRawResponse(); + Assert.AreEqual(200, rawResponse.Status); + Assert.AreEqual(ModelReaderWriter.Write(mockJsonModel).ToString(), ModelReaderWriter.Write(operation.Value).ToString()); + } + + private class MockClientOptions : ClientOptions + { + } + } +} diff --git a/sdk/core/Azure.Core/tests/RehydrationTokenTests.cs b/sdk/core/Azure.Core/tests/RehydrationTokenTests.cs index e82743a2ecf8..f9bae237d643 100644 --- a/sdk/core/Azure.Core/tests/RehydrationTokenTests.cs +++ b/sdk/core/Azure.Core/tests/RehydrationTokenTests.cs @@ -18,7 +18,7 @@ public void ThrowOnDeserializationWithNullRehydrationToken() [Test] public void ThrowOnDeserializationWithRehydrationTokenNullRequiredMember() { - var data = BinaryData.FromString("\"requestMethod\": null}"); + var data = BinaryData.FromString("{\"requestMethod\": null}"); Assert.That(() => ModelReaderWriter.Read(data, typeof(RehydrationToken)), Throws.Exception); } From 8c361fa34ce14e7ec14ba895b6a02bafd19ddbb1 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 14 Mar 2024 15:53:47 +0800 Subject: [PATCH 02/32] update test --- sdk/core/Azure.Core/tests/OperationTests.cs | 25 +++++++++++++-- .../tests/RehydrationOperationTests.cs | 32 +++++++++++-------- 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/sdk/core/Azure.Core/tests/OperationTests.cs b/sdk/core/Azure.Core/tests/OperationTests.cs index ec15ca3d7604..36d7366219d9 100644 --- a/sdk/core/Azure.Core/tests/OperationTests.cs +++ b/sdk/core/Azure.Core/tests/OperationTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.ClientModel.Primitives; using System.Threading; using System.Threading.Tasks; using Azure; @@ -138,28 +139,46 @@ public void OperationId() [Test] public void RehydrateOperation() { + var pipeline = CreateMockHttpPipeline(out _); var operationId = Guid.NewGuid().ToString(); var rehydrationToken = new RehydrationToken(null, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); - var operation = new RehydrationOperation(HttpPipelineBuilder.Build(new MockClientOptions()), rehydrationToken); + var operation = new RehydrationOperation(pipeline, rehydrationToken); Assert.NotNull(operation); Assert.AreEqual(operationId, operation.Id); Assert.Throws(() => operation.GetRawResponse()); Assert.False(operation.HasCompleted); + operation.UpdateStatus(); + Assert.AreEqual(200, operation.GetRawResponse().Status); } [Test] - public void RehydrateOperationOfT() + public async Task RehydrateOperationOfT() { + var pipeline = CreateMockHttpPipeline(out var mockJsonModel); var operationId = Guid.NewGuid().ToString(); var rehydrationToken = new RehydrationToken(null, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); - var operation = new RehydrationOperation(HttpPipelineBuilder.Build(new MockClientOptions()), rehydrationToken); + var operation = new RehydrationOperation(pipeline, rehydrationToken); Assert.NotNull(operation); Assert.AreEqual(operationId, operation.Id); Assert.Throws(() => operation.GetRawResponse()); Assert.False(operation.HasCompleted); Assert.Throws(() => { var value = operation.Value; }); Assert.False(operation.HasValue); + + await operation.UpdateStatusAsync(); + Assert.AreEqual(200, operation.GetRawResponse().Status); + Assert.AreEqual(ModelReaderWriter.Write(mockJsonModel).ToString(), ModelReaderWriter.Write(operation.Value).ToString()); + } + + private static HttpPipeline CreateMockHttpPipeline(out MockJsonModel mockJsonModel) + { + var mockResponse = new MockResponse(200); + mockJsonModel = new MockJsonModel(1, "a"); + mockResponse.SetContent(ModelReaderWriter.Write(mockJsonModel).ToString()); + var transport = new MockTransport(mockResponse); + var pipeline = new HttpPipeline(transport, default); + return pipeline; } private class MockClientOptions : ClientOptions diff --git a/sdk/core/Azure.Core/tests/RehydrationOperationTests.cs b/sdk/core/Azure.Core/tests/RehydrationOperationTests.cs index 9a747e885daf..2d473a8d83d7 100644 --- a/sdk/core/Azure.Core/tests/RehydrationOperationTests.cs +++ b/sdk/core/Azure.Core/tests/RehydrationOperationTests.cs @@ -20,11 +20,7 @@ public void ThrowOnNullArgument() [Test] public void ConstructOperationForRehydration() { - var mockResponse = new MockResponse(200); - var mockJsonModel = new MockJsonModel(1, "a"); - mockResponse.SetContent(ModelReaderWriter.Write(mockJsonModel).ToString()); - var transport = new MockTransport(mockResponse); - var pipeline = new HttpPipeline(transport, default); + HttpPipeline pipeline = CreateMockHttpPipeline(out _); var operationId = Guid.NewGuid().ToString(); var rehydrationToken = new RehydrationToken(null, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); var operation = new RehydrationOperation(pipeline, rehydrationToken); @@ -34,28 +30,36 @@ public void ConstructOperationForRehydration() Assert.False(operation.HasCompleted); operation.UpdateStatus(); - var rawResponse = operation.GetRawResponse(); - Assert.AreEqual(200, rawResponse.Status); + Assert.AreEqual(200, operation.GetRawResponse().Status); } [Test] public void ConstructOperationOfTForRehydration() { - var mockResponse = new MockResponse(200); - var mockJsonModel = new MockJsonModel(1, "a"); - mockResponse.SetContent(ModelReaderWriter.Write(mockJsonModel).ToString()); - var transport = new MockTransport(mockResponse); - var pipeline = new HttpPipeline(transport, default); + var pipeline = CreateMockHttpPipeline(out var mockJsonModel); var operationId = Guid.NewGuid().ToString(); var rehydrationToken = new RehydrationToken(null, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); var operation = new RehydrationOperation(pipeline, rehydrationToken); + Assert.NotNull(operation); + Assert.AreEqual(operationId, operation.Id); + Assert.Throws(() => operation.GetRawResponse()); + Assert.False(operation.HasCompleted); operation.UpdateStatus(); - var rawResponse = operation.GetRawResponse(); - Assert.AreEqual(200, rawResponse.Status); + Assert.AreEqual(200, operation.GetRawResponse().Status); Assert.AreEqual(ModelReaderWriter.Write(mockJsonModel).ToString(), ModelReaderWriter.Write(operation.Value).ToString()); } + private static HttpPipeline CreateMockHttpPipeline(out MockJsonModel mockJsonModel) + { + var mockResponse = new MockResponse(200); + mockJsonModel = new MockJsonModel(1, "a"); + mockResponse.SetContent(ModelReaderWriter.Write(mockJsonModel).ToString()); + var transport = new MockTransport(mockResponse); + var pipeline = new HttpPipeline(transport, default); + return pipeline; + } + private class MockClientOptions : ClientOptions { } From 516a77d7c65be3c58736e6744c0ce24c8db05544 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 14 Mar 2024 18:00:39 +0800 Subject: [PATCH 03/32] Add type constraint. --- sdk/core/Azure.Core/src/Internal/GenericOperationSource.cs | 2 +- sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs | 3 ++- sdk/core/Azure.Core/src/Operation.cs | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/sdk/core/Azure.Core/src/Internal/GenericOperationSource.cs b/sdk/core/Azure.Core/src/Internal/GenericOperationSource.cs index 5d359c58d2ba..d9790cb41543 100644 --- a/sdk/core/Azure.Core/src/Internal/GenericOperationSource.cs +++ b/sdk/core/Azure.Core/src/Internal/GenericOperationSource.cs @@ -8,7 +8,7 @@ namespace Azure.Core { - internal class GenericOperationSource<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] T> : IOperationSource where T : notnull + internal class GenericOperationSource<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] T> : IOperationSource where T : IPersistableModel { T IOperationSource.CreateResult(Response response, CancellationToken cancellationToken) => CreateResult(response); diff --git a/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs b/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs index 07f538f6343f..d275cc617284 100644 --- a/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs +++ b/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using System.ClientModel.Primitives; using System.Threading; using System.Threading.Tasks; using Azure.Core.Pipeline; @@ -11,7 +12,7 @@ namespace Azure.Core { #pragma warning disable SA1649 // File name should match first type name - internal class RehydrationOperation : Operation where T: notnull + internal class RehydrationOperation : Operation where T : IPersistableModel #pragma warning restore SA1649 // File name should match first type name { private readonly OperationInternal _operation; diff --git a/sdk/core/Azure.Core/src/Operation.cs b/sdk/core/Azure.Core/src/Operation.cs index 085bd011b352..3e2ec44d76a5 100644 --- a/sdk/core/Azure.Core/src/Operation.cs +++ b/sdk/core/Azure.Core/src/Operation.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.ClientModel.Primitives; using System.ComponentModel; using System.Threading; using System.Threading.Tasks; @@ -24,7 +25,7 @@ public abstract class Operation /// The rehydration token. /// The client options. /// The long-running operation. - public static Operation Rehydrate(HttpPipeline pipeline, RehydrationToken? rehydrationToken, ClientOptions? options = null) where T : notnull + public static Operation Rehydrate(HttpPipeline pipeline, RehydrationToken? rehydrationToken, ClientOptions? options = null) where T : IPersistableModel => new RehydrationOperation(pipeline, rehydrationToken, options); /// From 2f6bd661f682aa2fa9d4e623e3b3021e96580b6c Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 14 Mar 2024 18:11:50 +0800 Subject: [PATCH 04/32] update API --- sdk/core/Azure.Core/api/Azure.Core.net461.cs | 2 +- sdk/core/Azure.Core/api/Azure.Core.net472.cs | 2 +- sdk/core/Azure.Core/api/Azure.Core.net6.0.cs | 2 +- sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sdk/core/Azure.Core/api/Azure.Core.net461.cs b/sdk/core/Azure.Core/api/Azure.Core.net461.cs index 79ded6594ef5..643dec8f26cc 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net461.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net461.cs @@ -139,7 +139,7 @@ protected Operation() { } public abstract Azure.Response GetRawResponse(); public virtual Azure.Core.RehydrationToken? GetRehydrationToken() { throw null; } public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken? rehydrationToken, Azure.Core.ClientOptions? options = null) { throw null; } - public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken? rehydrationToken, Azure.Core.ClientOptions? options = null) where T : notnull { throw null; } + public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken? rehydrationToken, Azure.Core.ClientOptions? options = null) where T : System.ClientModel.Primitives.IPersistableModel { throw null; } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public override string? ToString() { throw null; } public abstract Azure.Response UpdateStatus(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); diff --git a/sdk/core/Azure.Core/api/Azure.Core.net472.cs b/sdk/core/Azure.Core/api/Azure.Core.net472.cs index 79ded6594ef5..643dec8f26cc 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net472.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net472.cs @@ -139,7 +139,7 @@ protected Operation() { } public abstract Azure.Response GetRawResponse(); public virtual Azure.Core.RehydrationToken? GetRehydrationToken() { throw null; } public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken? rehydrationToken, Azure.Core.ClientOptions? options = null) { throw null; } - public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken? rehydrationToken, Azure.Core.ClientOptions? options = null) where T : notnull { throw null; } + public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken? rehydrationToken, Azure.Core.ClientOptions? options = null) where T : System.ClientModel.Primitives.IPersistableModel { throw null; } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public override string? ToString() { throw null; } public abstract Azure.Response UpdateStatus(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); 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 ca69bb6b35d7..9c4c99b7cab7 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net6.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net6.0.cs @@ -139,7 +139,7 @@ protected Operation() { } public abstract Azure.Response GetRawResponse(); public virtual Azure.Core.RehydrationToken? GetRehydrationToken() { throw null; } public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken? rehydrationToken, Azure.Core.ClientOptions? options = null) { throw null; } - public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken? rehydrationToken, Azure.Core.ClientOptions? options = null) where T : notnull { throw null; } + public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken? rehydrationToken, Azure.Core.ClientOptions? options = null) where T : System.ClientModel.Primitives.IPersistableModel { throw null; } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public override string? ToString() { throw null; } public abstract Azure.Response UpdateStatus(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); 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 79ded6594ef5..643dec8f26cc 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs @@ -139,7 +139,7 @@ protected Operation() { } public abstract Azure.Response GetRawResponse(); public virtual Azure.Core.RehydrationToken? GetRehydrationToken() { throw null; } public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken? rehydrationToken, Azure.Core.ClientOptions? options = null) { throw null; } - public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken? rehydrationToken, Azure.Core.ClientOptions? options = null) where T : notnull { throw null; } + public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken? rehydrationToken, Azure.Core.ClientOptions? options = null) where T : System.ClientModel.Primitives.IPersistableModel { throw null; } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public override string? ToString() { throw null; } public abstract Azure.Response UpdateStatus(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); From 619c6ed4605a9dcb7e2cc7936ba7aa57f084e812 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Fri, 15 Mar 2024 11:50:32 +0800 Subject: [PATCH 05/32] implement GetRehydrationToken --- .../src/Internal/RehydrationOperation.cs | 2 ++ .../src/Internal/RehydrationOperationOfT.cs | 2 ++ sdk/core/Azure.Core/src/RehydrationToken.cs | 2 +- .../Shared/NextLinkOperationImplementation.cs | 11 ++++++---- .../tests/RehydrationOperationTests.cs | 20 +++++++++++++++++++ 5 files changed, 32 insertions(+), 5 deletions(-) diff --git a/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs b/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs index 28656d8afebe..dfa7f0ef59f1 100644 --- a/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs +++ b/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs @@ -25,6 +25,8 @@ public RehydrationOperation(HttpPipeline pipeline, RehydrationToken? rehydration public override string Id => _nextLinkOperation?.OperationId ?? null; + public override RehydrationToken? GetRehydrationToken() => _nextLinkOperation?.GetRehydrationToken(); + public override bool HasCompleted => _operation.HasCompleted; public override Response GetRawResponse() => _operation.RawResponse; diff --git a/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs b/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs index d275cc617284..a4a31a16c5f8 100644 --- a/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs +++ b/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs @@ -43,6 +43,8 @@ public RehydrationOperation(HttpPipeline pipeline, RehydrationToken? rehydration public override string Id => _nextLinkOperation.OperationId ?? null; + public override RehydrationToken? GetRehydrationToken() => _nextLinkOperation?.GetRehydrationToken(); + public override bool HasCompleted => _operation.HasCompleted; public override Response GetRawResponse() => _operation.RawResponse; diff --git a/sdk/core/Azure.Core/src/RehydrationToken.cs b/sdk/core/Azure.Core/src/RehydrationToken.cs index 13addaadc64f..c3e21a3881a1 100644 --- a/sdk/core/Azure.Core/src/RehydrationToken.cs +++ b/sdk/core/Azure.Core/src/RehydrationToken.cs @@ -15,7 +15,7 @@ public readonly partial struct RehydrationToken public string? Id { get; } // Version for this contract itself since we might change the members in the future. - internal string Version { get; } = "1.0.0"; + internal string Version { get; } = NextLinkOperationImplementation.RehydartionTokenVersion; // The below members are used to re-construct . // Value of . diff --git a/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs b/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs index e69d45b60c9d..a99d0d318095 100644 --- a/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs +++ b/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs @@ -16,6 +16,7 @@ namespace Azure.Core { internal class NextLinkOperationImplementation : IOperation { + internal const string RehydartionTokenVersion = "1.0.0"; private const string ApiVersionParam = "api-version"; private static readonly string[] FailureStates = { "failed", "canceled" }; private static readonly string[] SuccessStates = { "succeeded" }; @@ -162,7 +163,7 @@ private NextLinkOperationImplementation( } public RehydrationToken GetRehydrationToken() - => GetRehydrationToken(RequestMethod, _startRequestUri, _nextRequestUri, _headerSource.ToString(), _lastKnownLocation, _finalStateVia.ToString()); + => GetRehydrationToken(RequestMethod, _startRequestUri, _nextRequestUri, _headerSource.ToString(), _lastKnownLocation, _finalStateVia.ToString(), OperationId); public static RehydrationToken GetRehydrationToken( RequestMethod requestMethod, @@ -183,7 +184,8 @@ public static RehydrationToken GetRehydrationToken( } var headerSource = GetHeaderSource(requestMethod, startRequestUri, response, apiVersionStr, out var nextRequestUri); response.Headers.TryGetValue("Location", out var lastKnownLocation); - return GetRehydrationToken(requestMethod, startRequestUri, nextRequestUri, headerSource.ToString(), lastKnownLocation, finalStateVia.ToString()); + var operationId = ParseOperationId(nextRequestUri); + return GetRehydrationToken(requestMethod, startRequestUri, nextRequestUri, headerSource.ToString(), lastKnownLocation, finalStateVia.ToString(), operationId); } public static RehydrationToken GetRehydrationToken( @@ -192,9 +194,10 @@ public static RehydrationToken GetRehydrationToken( string nextRequestUri, string headerSource, string? lastKnownLocation, - string finalStateVia) + string finalStateVia, + string? operationId = null) { - var data = new BinaryData(new { requestMethod = requestMethod.ToString(), initialUri = startRequestUri.AbsoluteUri, nextRequestUri, headerSource, lastKnownLocation, finalStateVia }); + var data = new BinaryData(new { version = RehydartionTokenVersion, id = operationId, requestMethod = requestMethod.ToString(), initialUri = startRequestUri.AbsoluteUri, nextRequestUri, headerSource, lastKnownLocation, finalStateVia }); return ModelReaderWriter.Read(data); } diff --git a/sdk/core/Azure.Core/tests/RehydrationOperationTests.cs b/sdk/core/Azure.Core/tests/RehydrationOperationTests.cs index 2d473a8d83d7..a5269667be83 100644 --- a/sdk/core/Azure.Core/tests/RehydrationOperationTests.cs +++ b/sdk/core/Azure.Core/tests/RehydrationOperationTests.cs @@ -50,6 +50,26 @@ public void ConstructOperationOfTForRehydration() Assert.AreEqual(ModelReaderWriter.Write(mockJsonModel).ToString(), ModelReaderWriter.Write(operation.Value).ToString()); } + [Test] + public void GetRehydrationToken() + { + var operationId = Guid.NewGuid().ToString(); + var rehydrationToken = new RehydrationToken(operationId, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test/", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); + var operation = new RehydrationOperation(HttpPipelineBuilder.Build(new MockClientOptions()), rehydrationToken); + var token = operation.GetRehydrationToken(); + Assert.AreEqual(ModelReaderWriter.Write(rehydrationToken).ToString(), ModelReaderWriter.Write(token).ToString()); + } + + [Test] + public void GetRehydrationTokenOfT() + { + var operationId = Guid.NewGuid().ToString(); + var rehydrationToken = new RehydrationToken(operationId, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test/", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); + var operation = new RehydrationOperation(HttpPipelineBuilder.Build(new MockClientOptions()), rehydrationToken); + var token = operation.GetRehydrationToken(); + Assert.AreEqual(ModelReaderWriter.Write(rehydrationToken).ToString(), ModelReaderWriter.Write(token).ToString()); + } + private static HttpPipeline CreateMockHttpPipeline(out MockJsonModel mockJsonModel) { var mockResponse = new MockResponse(200); From fb086232a28a7997fe12fb909e1c7097c884c864 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Sat, 16 Mar 2024 08:45:05 +0800 Subject: [PATCH 06/32] cleanup --- sdk/core/Azure.Core/tests/OperationTests.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/sdk/core/Azure.Core/tests/OperationTests.cs b/sdk/core/Azure.Core/tests/OperationTests.cs index 36d7366219d9..e867aa74ad16 100644 --- a/sdk/core/Azure.Core/tests/OperationTests.cs +++ b/sdk/core/Azure.Core/tests/OperationTests.cs @@ -5,11 +5,9 @@ using System.ClientModel.Primitives; using System.Threading; using System.Threading.Tasks; -using Azure; using Azure.Core.Pipeline; using Azure.Core.TestFramework; using Azure.Core.Tests.TestFramework; -using NUnit; using NUnit.Framework; namespace Azure.Core.Tests @@ -180,9 +178,5 @@ private static HttpPipeline CreateMockHttpPipeline(out MockJsonModel mockJsonMod var pipeline = new HttpPipeline(transport, default); return pipeline; } - - private class MockClientOptions : ClientOptions - { - } } } From 9f03f88df9515ef73c25db8c0ead375d96ee0ee4 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Sat, 16 Mar 2024 09:46:04 +0800 Subject: [PATCH 07/32] remove duplicates --- .../tests/Convenience/ClientResultTests.cs | 1 + .../tests/Message/BinaryContentTests.cs | 1 + .../TestFramework/Mocks/MockJsonModel.cs | 71 ------------------- 3 files changed, 2 insertions(+), 71 deletions(-) delete mode 100644 sdk/core/System.ClientModel/tests/TestFramework/Mocks/MockJsonModel.cs diff --git a/sdk/core/System.ClientModel/tests/Convenience/ClientResultTests.cs b/sdk/core/System.ClientModel/tests/Convenience/ClientResultTests.cs index 3f9e157e15fa..ccad66170e57 100644 --- a/sdk/core/System.ClientModel/tests/Convenience/ClientResultTests.cs +++ b/sdk/core/System.ClientModel/tests/Convenience/ClientResultTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.ClientModel.Primitives; +using Azure.Core.TestFramework; using ClientModel.Tests.Mocks; using NUnit.Framework; diff --git a/sdk/core/System.ClientModel/tests/Message/BinaryContentTests.cs b/sdk/core/System.ClientModel/tests/Message/BinaryContentTests.cs index 9604329ed5ed..77e653d37d77 100644 --- a/sdk/core/System.ClientModel/tests/Message/BinaryContentTests.cs +++ b/sdk/core/System.ClientModel/tests/Message/BinaryContentTests.cs @@ -9,6 +9,7 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using MockJsonModel = Azure.Core.TestFramework.MockJsonModel; namespace System.ClientModel.Tests.Message; diff --git a/sdk/core/System.ClientModel/tests/TestFramework/Mocks/MockJsonModel.cs b/sdk/core/System.ClientModel/tests/TestFramework/Mocks/MockJsonModel.cs deleted file mode 100644 index bdb6151ed06e..000000000000 --- a/sdk/core/System.ClientModel/tests/TestFramework/Mocks/MockJsonModel.cs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Azure; -using Azure.Core.Serialization; -using System; -using System.ClientModel.Primitives; -using System.IO; -using System.Text.Json; - -namespace ClientModel.Tests.Mocks; - -public class MockJsonModel : IJsonModel -{ - public int IntValue { get; set; } - - public string StringValue { get; set; } - - public byte[] Utf8BytesValue { get; } - - public MockJsonModel(int intValue, string stringValue) - { - IntValue = intValue; - StringValue = stringValue; - - dynamic json = BinaryData.FromString("{}").ToDynamicFromJson(JsonPropertyNames.CamelCase); - json.IntValue = IntValue; - json.StringValue = StringValue; - - MemoryStream stream = new(); - using Utf8JsonWriter writer = new Utf8JsonWriter(stream); - - writer.WriteStartObject(); - writer.WriteNumber("IntValue", IntValue); - writer.WriteString("StringValue", StringValue); - writer.WriteEndObject(); - - writer.Flush(); - Utf8BytesValue = stream.ToArray(); - } - - MockJsonModel IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) - { - dynamic json = data.ToDynamicFromJson(JsonPropertyNames.CamelCase); - return new MockJsonModel(json.IntValue, json.StringValue); - } - - MockJsonModel IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) - { - using JsonDocument doc = JsonDocument.ParseValue(ref reader); - int intValue = doc.RootElement.GetProperty("IntValue").GetInt32(); - string stringValue = doc.RootElement.GetProperty("StringValue").GetString()!; - return new MockJsonModel(intValue, stringValue); - } - - string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) - => "J"; - - BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) - { - return BinaryData.FromBytes(Utf8BytesValue); - } - - void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) - { - writer.WriteStartObject(); - writer.WriteNumber("IntValue", IntValue); - writer.WriteString("StringValue", StringValue); - writer.WriteEndObject(); - } -} From e47d85ea231da85e47066e2b16bd030d02b672f9 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 20 Mar 2024 10:22:02 +0800 Subject: [PATCH 08/32] address comments --- .../Azure.Core/src/Internal/RehydrationOperation.cs | 13 ++----------- .../src/Internal/RehydrationOperationOfT.cs | 12 ++---------- .../src/Shared/NextLinkOperationImplementation.cs | 6 +++--- .../Azure.Core/src/Shared/OperationInternalOfT.cs | 3 ++- 4 files changed, 9 insertions(+), 25 deletions(-) diff --git a/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs b/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs index dfa7f0ef59f1..c963adcb7644 100644 --- a/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs +++ b/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs @@ -3,7 +3,6 @@ #nullable disable -using System; using System.Threading; using System.Threading.Tasks; using Azure.Core.Pipeline; @@ -17,8 +16,8 @@ internal class RehydrationOperation : Operation public RehydrationOperation(HttpPipeline pipeline, RehydrationToken? rehydrationToken, ClientOptions options = null) { - AssertNotNull(pipeline, nameof(pipeline)); - AssertNotNull(rehydrationToken, nameof(rehydrationToken)); + Argument.AssertNotNull(pipeline, nameof(pipeline)); + Argument.AssertNotNull(rehydrationToken, nameof(rehydrationToken)); _nextLinkOperation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(pipeline, rehydrationToken); _operation = new OperationInternal(_nextLinkOperation, new ClientDiagnostics(options ?? ClientOptions.Default), null, requestMethod: _nextLinkOperation.RequestMethod); } @@ -34,13 +33,5 @@ public RehydrationOperation(HttpPipeline pipeline, RehydrationToken? rehydration public override Response UpdateStatus(CancellationToken cancellationToken = default) => _operation.UpdateStatus(cancellationToken); public override ValueTask UpdateStatusAsync(CancellationToken cancellationToken = default) => _operation.UpdateStatusAsync(cancellationToken); - - private static void AssertNotNull(T value, string name) - { - if (value is null) - { - throw new ArgumentNullException(name); - } - } } } diff --git a/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs b/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs index a4a31a16c5f8..e93d908e52ae 100644 --- a/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs +++ b/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs @@ -3,7 +3,6 @@ #nullable disable -using System; using System.ClientModel.Primitives; using System.Threading; using System.Threading.Tasks; @@ -20,15 +19,8 @@ internal class RehydrationOperation : Operation where T : IPersistableMode public RehydrationOperation(HttpPipeline pipeline, RehydrationToken? rehydrationToken, ClientOptions options = null) { - if (pipeline is null) - { - throw new ArgumentNullException(nameof(pipeline)); - } - - if (rehydrationToken is null) - { - throw new ArgumentNullException(nameof(rehydrationToken)); - } + Argument.AssertNotNull(pipeline, nameof(pipeline)); + Argument.AssertNotNull(rehydrationToken, nameof(rehydrationToken)); IOperationSource source = new GenericOperationSource(); _nextLinkOperation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(pipeline, rehydrationToken); diff --git a/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs b/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs index a99d0d318095..d9a59b703879 100644 --- a/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs +++ b/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs @@ -30,6 +30,9 @@ internal class NextLinkOperationImplementation : IOperation private string? _lastKnownLocation; private string _nextRequestUri; + public string? OperationId { get; } + public RequestMethod RequestMethod { get; } + public static IOperation Create( HttpPipeline pipeline, RequestMethod requestMethod, @@ -76,9 +79,6 @@ public static IOperation Create( IOperation operation) => new OperationToOperationOfT(operationSource, operation); - public string? OperationId { get; } - public RequestMethod RequestMethod { get; } - public static IOperation Create( HttpPipeline pipeline, RehydrationToken? rehydrationToken, diff --git a/sdk/core/Azure.Core/src/Shared/OperationInternalOfT.cs b/sdk/core/Azure.Core/src/Shared/OperationInternalOfT.cs index 4543146f2817..107047764160 100644 --- a/sdk/core/Azure.Core/src/Shared/OperationInternalOfT.cs +++ b/sdk/core/Azure.Core/src/Shared/OperationInternalOfT.cs @@ -4,7 +4,6 @@ #nullable enable using System; -using System.ClientModel.Primitives; using System.Collections.Generic; using System.IO; using System.Net; @@ -287,6 +286,7 @@ private static Response GetResponseFromState(OperationState state, RequestMet } // if this is a fake delete LRO with 404, just return empty response with 204 + // A fake delete LRO is a delete LRO completed with the initial call if (RequestMethod.Delete == requestmethod && state.RawResponse.Status == 404) { return new EmptyResponse(HttpStatusCode.NoContent); @@ -297,6 +297,7 @@ private static Response GetResponseFromState(OperationState state, RequestMet /// /// This is only used for fake delete LRO, we just want to return an empty response with 204 to the user for this case. + /// A fake delete LRO is a delete LRO completed with the initial call. /// private sealed class EmptyResponse : Response { From e9e3c797bfc37210d2153217d7b82b88dcbbd740 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 21 Mar 2024 10:15:22 +0800 Subject: [PATCH 09/32] Make rawResponse non-nullable --- sdk/core/Azure.Core/src/Shared/OperationInternal.cs | 2 +- sdk/core/Azure.Core/src/Shared/OperationInternalOfT.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sdk/core/Azure.Core/src/Shared/OperationInternal.cs b/sdk/core/Azure.Core/src/Shared/OperationInternal.cs index 1892fb5def0f..2391844e40e5 100644 --- a/sdk/core/Azure.Core/src/Shared/OperationInternal.cs +++ b/sdk/core/Azure.Core/src/Shared/OperationInternal.cs @@ -86,7 +86,7 @@ internal class OperationInternal : OperationInternalBase /// The Http request method public OperationInternal(IOperation operation, ClientDiagnostics clientDiagnostics, - Response? rawResponse, + Response rawResponse, string? operationTypeName = null, IEnumerable>? scopeAttributes = null, DelayStrategy? fallbackStrategy = null, diff --git a/sdk/core/Azure.Core/src/Shared/OperationInternalOfT.cs b/sdk/core/Azure.Core/src/Shared/OperationInternalOfT.cs index 107047764160..999758c6885f 100644 --- a/sdk/core/Azure.Core/src/Shared/OperationInternalOfT.cs +++ b/sdk/core/Azure.Core/src/Shared/OperationInternalOfT.cs @@ -55,7 +55,7 @@ internal class OperationInternal : OperationInternalBase { private readonly IOperation _operation; private readonly AsyncLockWithValue> _stateLock; - private Response? _rawResponse; + private Response _rawResponse; /// /// Initializes a new instance of the class in a final successful state. @@ -98,7 +98,7 @@ internal class OperationInternal : OperationInternalBase /// The Http request method. public OperationInternal(IOperation operation, ClientDiagnostics clientDiagnostics, - Response? rawResponse, + Response rawResponse, string? operationTypeName = null, IEnumerable>? scopeAttributes = null, DelayStrategy? fallbackStrategy = null, @@ -120,7 +120,7 @@ private OperationInternal(OperationState finalState) _stateLock = new AsyncLockWithValue>(finalState); } - public override Response RawResponse => _stateLock.TryGetValue(out var state) ? state.RawResponse : _rawResponse ?? throw new InvalidOperationException("The operation has not completed yet."); + public override Response RawResponse => _stateLock.TryGetValue(out var state) ? state.RawResponse : _rawResponse; public override bool HasCompleted => _stateLock.HasValue; From 70bc5c03d2632d795c5b4f2055a0a7f3073394fd Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 21 Mar 2024 13:31:07 +0800 Subject: [PATCH 10/32] UpdaateStatus during LRO rehydration to get the latest state --- .../src/Internal/RehydrationOperation.cs | 5 ++- .../src/Internal/RehydrationOperationOfT.cs | 6 ++-- .../src/Shared/OperationInternal.cs | 2 +- .../src/Shared/OperationInternalOfT.cs | 2 +- .../tests/RehydrationOperationTests.cs | 34 +++++++++++++++---- 5 files changed, 38 insertions(+), 11 deletions(-) diff --git a/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs b/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs index c963adcb7644..ec7d16f64fab 100644 --- a/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs +++ b/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs @@ -19,7 +19,10 @@ public RehydrationOperation(HttpPipeline pipeline, RehydrationToken? rehydration Argument.AssertNotNull(pipeline, nameof(pipeline)); Argument.AssertNotNull(rehydrationToken, nameof(rehydrationToken)); _nextLinkOperation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(pipeline, rehydrationToken); - _operation = new OperationInternal(_nextLinkOperation, new ClientDiagnostics(options ?? ClientOptions.Default), null, requestMethod: _nextLinkOperation.RequestMethod); + var operationState = _nextLinkOperation.UpdateStateAsync(false, default).EnsureCompleted(); + _operation = operationState.HasCompleted + ? _operation = new OperationInternal(operationState) + : new OperationInternal(_nextLinkOperation, new ClientDiagnostics(options ?? ClientOptions.Default), operationState.RawResponse, requestMethod: _nextLinkOperation.RequestMethod); } public override string Id => _nextLinkOperation?.OperationId ?? null; diff --git a/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs b/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs index e93d908e52ae..a2b1e9f161f8 100644 --- a/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs +++ b/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs @@ -25,8 +25,10 @@ public RehydrationOperation(HttpPipeline pipeline, RehydrationToken? rehydration IOperationSource source = new GenericOperationSource(); _nextLinkOperation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(pipeline, rehydrationToken); var operation = NextLinkOperationImplementation.Create(source, _nextLinkOperation); - var clientDiagnostics = new ClientDiagnostics(options ?? ClientOptions.Default); - _operation = new OperationInternal(operation, clientDiagnostics, null); + var operationState = operation.UpdateStateAsync(false, default).EnsureCompleted(); + _operation = operationState.HasCompleted + ? new OperationInternal(operationState) + : new OperationInternal(operation, new ClientDiagnostics(options ?? ClientOptions.Default), operationState.RawResponse); } public override T Value => _operation.Value; diff --git a/sdk/core/Azure.Core/src/Shared/OperationInternal.cs b/sdk/core/Azure.Core/src/Shared/OperationInternal.cs index 2391844e40e5..40c0dafbdd24 100644 --- a/sdk/core/Azure.Core/src/Shared/OperationInternal.cs +++ b/sdk/core/Azure.Core/src/Shared/OperationInternal.cs @@ -96,7 +96,7 @@ public OperationInternal(IOperation operation, _internalOperation = new OperationInternal(new OperationToOperationOfTProxy(operation), clientDiagnostics, rawResponse, operationTypeName ?? operation.GetType().Name, scopeAttributes, fallbackStrategy, requestMethod); } - private OperationInternal(OperationState finalState) + internal OperationInternal(OperationState finalState) : base(finalState.RawResponse) { _internalOperation = finalState.HasSucceeded diff --git a/sdk/core/Azure.Core/src/Shared/OperationInternalOfT.cs b/sdk/core/Azure.Core/src/Shared/OperationInternalOfT.cs index 999758c6885f..ff0802cd3e3f 100644 --- a/sdk/core/Azure.Core/src/Shared/OperationInternalOfT.cs +++ b/sdk/core/Azure.Core/src/Shared/OperationInternalOfT.cs @@ -110,7 +110,7 @@ public OperationInternal(IOperation operation, _stateLock = new AsyncLockWithValue>(); } - private OperationInternal(OperationState finalState) + internal OperationInternal(OperationState finalState) : base(finalState.RawResponse) { // FinalOperation represents operation that is in final state and can't be updated. diff --git a/sdk/core/Azure.Core/tests/RehydrationOperationTests.cs b/sdk/core/Azure.Core/tests/RehydrationOperationTests.cs index a5269667be83..718a47d53cf3 100644 --- a/sdk/core/Azure.Core/tests/RehydrationOperationTests.cs +++ b/sdk/core/Azure.Core/tests/RehydrationOperationTests.cs @@ -26,11 +26,24 @@ public void ConstructOperationForRehydration() var operation = new RehydrationOperation(pipeline, rehydrationToken); Assert.NotNull(operation); Assert.AreEqual(operationId, operation.Id); - Assert.Throws(() => operation.GetRawResponse()); - Assert.False(operation.HasCompleted); + Assert.AreEqual(200, operation.GetRawResponse().Status); + Assert.True(operation.HasCompleted); operation.UpdateStatus(); Assert.AreEqual(200, operation.GetRawResponse().Status); + Assert.True(operation.HasCompleted); + } + + [Test] + public void ConstructOperationForRehydrationWithFailure() + { + HttpPipeline pipeline = CreateMockHttpPipelineWithFailure(); + var operationId = Guid.NewGuid().ToString(); + var rehydrationToken = new RehydrationToken(null, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); + var operation = new RehydrationOperation(pipeline, rehydrationToken); + Assert.NotNull(operation); + Assert.AreEqual(500, operation.GetRawResponse().Status); + Assert.True(operation.HasCompleted); } [Test] @@ -42,12 +55,13 @@ public void ConstructOperationOfTForRehydration() var operation = new RehydrationOperation(pipeline, rehydrationToken); Assert.NotNull(operation); Assert.AreEqual(operationId, operation.Id); - Assert.Throws(() => operation.GetRawResponse()); - Assert.False(operation.HasCompleted); + Assert.AreEqual(200, operation.GetRawResponse().Status); + Assert.True(operation.HasCompleted); + Assert.True(operation.HasValue); + Assert.AreEqual(ModelReaderWriter.Write(mockJsonModel).ToString(), ModelReaderWriter.Write(operation.Value).ToString()); operation.UpdateStatus(); Assert.AreEqual(200, operation.GetRawResponse().Status); - Assert.AreEqual(ModelReaderWriter.Write(mockJsonModel).ToString(), ModelReaderWriter.Write(operation.Value).ToString()); } [Test] @@ -75,7 +89,15 @@ private static HttpPipeline CreateMockHttpPipeline(out MockJsonModel mockJsonMod var mockResponse = new MockResponse(200); mockJsonModel = new MockJsonModel(1, "a"); mockResponse.SetContent(ModelReaderWriter.Write(mockJsonModel).ToString()); - var transport = new MockTransport(mockResponse); + var transport = new MockTransport(mockResponse, mockResponse); + var pipeline = new HttpPipeline(transport, default); + return pipeline; + } + + private static HttpPipeline CreateMockHttpPipelineWithFailure() + { + var mockResponse = new MockResponse(500); + var transport = new MockTransport(mockResponse, mockResponse); var pipeline = new HttpPipeline(transport, default); return pipeline; } From c27d8636fb555c614b3444e9af71a87c1f69620e Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 21 Mar 2024 15:22:02 +0800 Subject: [PATCH 11/32] update for final get of delete LRO and add test for it --- .../src/Internal/RehydrationOperation.cs | 2 +- .../Shared/NextLinkOperationImplementation.cs | 63 ++++++++++++++++ .../src/Shared/OperationInternal.cs | 8 +- .../src/Shared/OperationInternalBase.cs | 7 +- .../src/Shared/OperationInternalOfT.cs | 73 +------------------ .../NextLinkOperationImplementationTests.cs | 28 ++++++- 6 files changed, 99 insertions(+), 82 deletions(-) diff --git a/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs b/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs index ec7d16f64fab..4144d6ed7df6 100644 --- a/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs +++ b/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs @@ -22,7 +22,7 @@ public RehydrationOperation(HttpPipeline pipeline, RehydrationToken? rehydration var operationState = _nextLinkOperation.UpdateStateAsync(false, default).EnsureCompleted(); _operation = operationState.HasCompleted ? _operation = new OperationInternal(operationState) - : new OperationInternal(_nextLinkOperation, new ClientDiagnostics(options ?? ClientOptions.Default), operationState.RawResponse, requestMethod: _nextLinkOperation.RequestMethod); + : new OperationInternal(_nextLinkOperation, new ClientDiagnostics(options ?? ClientOptions.Default), operationState.RawResponse); } public override string Id => _nextLinkOperation?.OperationId ?? null; diff --git a/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs b/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs index d9a59b703879..212d90bcd2e1 100644 --- a/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs +++ b/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs @@ -6,7 +6,9 @@ using System; using System.ClientModel.Primitives; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Net; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -385,9 +387,70 @@ private async ValueTask GetResponseAsync(bool async, string uri, Cance { _pipeline.Send(message, cancellationToken); } + + // If we are doing final get for a delete LRO with 404, just return empty response with 204 + if (message.Response.Status == 404 && RequestMethod == RequestMethod.Delete) + { + return new EmptyResponse(HttpStatusCode.NoContent); + } return message.Response; } + /// + /// This is only used for final get of the delete LRO, we just want to return an empty response with 204 to the user for this case. + /// + private sealed class EmptyResponse : Response + { + public EmptyResponse(HttpStatusCode status) + { + Status = (int)status; + ReasonPhrase = status.ToString(); + } + + public override int Status { get; } + + public override string ReasonPhrase { get; } + + public override Stream? ContentStream { get => null; set => throw new System.NotImplementedException(); } + public override string ClientRequestId { get => string.Empty; set => throw new System.NotImplementedException(); } + + public override void Dispose() + { + } + + /// +#if HAS_INTERNALS_VISIBLE_CORE + internal +#endif + protected override bool ContainsHeader(string name) => false; + + /// +#if HAS_INTERNALS_VISIBLE_CORE + internal +#endif + protected override IEnumerable EnumerateHeaders() => Array.Empty(); + + /// +#if HAS_INTERNALS_VISIBLE_CORE + internal +#endif + protected override bool TryGetHeader(string name, out string value) + { + value = string.Empty; + return false; + } + + /// +#if HAS_INTERNALS_VISIBLE_CORE + internal +#endif + protected override bool TryGetHeaderValues(string name, out IEnumerable values) + { + values = Array.Empty(); + return false; + } + } + private HttpMessage CreateRequest(string uri) { HttpMessage message = _pipeline.CreateMessage(); diff --git a/sdk/core/Azure.Core/src/Shared/OperationInternal.cs b/sdk/core/Azure.Core/src/Shared/OperationInternal.cs index 40c0dafbdd24..e728071c8c20 100644 --- a/sdk/core/Azure.Core/src/Shared/OperationInternal.cs +++ b/sdk/core/Azure.Core/src/Shared/OperationInternal.cs @@ -83,17 +83,15 @@ internal class OperationInternal : OperationInternalBase /// /// The attributes to use during diagnostic scope creation. /// The delay strategy to use. Default is . - /// The Http request method public OperationInternal(IOperation operation, ClientDiagnostics clientDiagnostics, Response rawResponse, string? operationTypeName = null, IEnumerable>? scopeAttributes = null, - DelayStrategy? fallbackStrategy = null, - RequestMethod? requestMethod = null) - : base(clientDiagnostics, operationTypeName ?? operation.GetType().Name, scopeAttributes, fallbackStrategy, requestMethod) + DelayStrategy? fallbackStrategy = null) + : base(clientDiagnostics, operationTypeName ?? operation.GetType().Name, scopeAttributes, fallbackStrategy) { - _internalOperation = new OperationInternal(new OperationToOperationOfTProxy(operation), clientDiagnostics, rawResponse, operationTypeName ?? operation.GetType().Name, scopeAttributes, fallbackStrategy, requestMethod); + _internalOperation = new OperationInternal(new OperationToOperationOfTProxy(operation), clientDiagnostics, rawResponse, operationTypeName ?? operation.GetType().Name, scopeAttributes, fallbackStrategy); } internal OperationInternal(OperationState finalState) diff --git a/sdk/core/Azure.Core/src/Shared/OperationInternalBase.cs b/sdk/core/Azure.Core/src/Shared/OperationInternalBase.cs index 54dc3f1a416f..dad6480c22ce 100644 --- a/sdk/core/Azure.Core/src/Shared/OperationInternalBase.cs +++ b/sdk/core/Azure.Core/src/Shared/OperationInternalBase.cs @@ -22,9 +22,8 @@ internal abstract class OperationInternalBase private readonly string _waitForCompletionResponseScopeName; protected readonly string _updateStatusScopeName; protected readonly string _waitForCompletionScopeName; - protected readonly RequestMethod? _requestMethod; - protected OperationInternalBase(Response rawResponse, RequestMethod? requestMethod = null) + protected OperationInternalBase(Response rawResponse) { _diagnostics = new ClientDiagnostics(ClientOptions.Default); _updateStatusScopeName = string.Empty; @@ -33,10 +32,9 @@ protected OperationInternalBase(Response rawResponse, RequestMethod? requestMeth _scopeAttributes = default; _fallbackStrategy = default; _responseLock = new AsyncLockWithValue(rawResponse); - _requestMethod = requestMethod; } - protected OperationInternalBase(ClientDiagnostics clientDiagnostics, string operationTypeName, IEnumerable>? scopeAttributes = null, DelayStrategy? fallbackStrategy = null, RequestMethod? requestMethod = null) + protected OperationInternalBase(ClientDiagnostics clientDiagnostics, string operationTypeName, IEnumerable>? scopeAttributes = null, DelayStrategy? fallbackStrategy = null) { _diagnostics = clientDiagnostics; _updateStatusScopeName = $"{operationTypeName}.{nameof(UpdateStatus)}"; @@ -45,7 +43,6 @@ protected OperationInternalBase(ClientDiagnostics clientDiagnostics, string oper _scopeAttributes = scopeAttributes?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); _fallbackStrategy = fallbackStrategy; _responseLock = new AsyncLockWithValue(); - _requestMethod = requestMethod; } /// diff --git a/sdk/core/Azure.Core/src/Shared/OperationInternalOfT.cs b/sdk/core/Azure.Core/src/Shared/OperationInternalOfT.cs index ff0802cd3e3f..c538299492ad 100644 --- a/sdk/core/Azure.Core/src/Shared/OperationInternalOfT.cs +++ b/sdk/core/Azure.Core/src/Shared/OperationInternalOfT.cs @@ -95,15 +95,13 @@ internal class OperationInternal : OperationInternalBase /// The attributes to use during diagnostic scope creation. /// The delay strategy when Retry-After header is not present. When it is present, the longer of the two delays will be used. /// Default is . - /// The Http request method. public OperationInternal(IOperation operation, ClientDiagnostics clientDiagnostics, Response rawResponse, string? operationTypeName = null, IEnumerable>? scopeAttributes = null, - DelayStrategy? fallbackStrategy = null, - RequestMethod? requetMethod = null) - : base(clientDiagnostics, operationTypeName ?? operation.GetType().Name, scopeAttributes, fallbackStrategy, requetMethod) + DelayStrategy? fallbackStrategy = null) + : base(clientDiagnostics, operationTypeName ?? operation.GetType().Name, scopeAttributes, fallbackStrategy) { _operation = operation; _rawResponse = rawResponse; @@ -269,7 +267,7 @@ protected override async ValueTask UpdateStatusAsync(bool async, Cance } asyncLock.SetValue(state); - return GetResponseFromState(state, _requestMethod); + return GetResponseFromState(state); } catch (Exception e) { @@ -278,79 +276,16 @@ protected override async ValueTask UpdateStatusAsync(bool async, Cance } } - private static Response GetResponseFromState(OperationState state, RequestMethod? requestmethod = null) + private static Response GetResponseFromState(OperationState state) { if (state.HasSucceeded) { return state.RawResponse; } - // if this is a fake delete LRO with 404, just return empty response with 204 - // A fake delete LRO is a delete LRO completed with the initial call - if (RequestMethod.Delete == requestmethod && state.RawResponse.Status == 404) - { - return new EmptyResponse(HttpStatusCode.NoContent); - } - throw state.OperationFailedException!; } - /// - /// This is only used for fake delete LRO, we just want to return an empty response with 204 to the user for this case. - /// A fake delete LRO is a delete LRO completed with the initial call. - /// - private sealed class EmptyResponse : Response - { - public EmptyResponse(HttpStatusCode status) - { - Status = (int)status; - ReasonPhrase = status.ToString(); - } - - public override int Status { get; } - - public override string ReasonPhrase { get; } - - public override Stream? ContentStream { get => null; set => throw new System.NotImplementedException(); } - public override string ClientRequestId { get => string.Empty; set => throw new System.NotImplementedException(); } - - public override void Dispose() - { - } - - /// -#if HAS_INTERNALS_VISIBLE_CORE - internal -#endif - protected override bool ContainsHeader(string name) => false; - - /// -#if HAS_INTERNALS_VISIBLE_CORE - internal -#endif - protected override IEnumerable EnumerateHeaders() => Array.Empty(); - - /// -#if HAS_INTERNALS_VISIBLE_CORE - internal -#endif - protected override bool TryGetHeader(string name, out string value) - { - value = string.Empty; - return false; - } - - /// -#if HAS_INTERNALS_VISIBLE_CORE - internal -#endif - protected override bool TryGetHeaderValues(string name, out IEnumerable values) - { - values = Array.Empty(); - return false; - } - } - private class FinalOperation : IOperation { public ValueTask> UpdateStateAsync(bool async, CancellationToken cancellationToken) diff --git a/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs b/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs index 7333014f238f..1000b0e468da 100644 --- a/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs +++ b/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs @@ -2,7 +2,10 @@ // Licensed under the MIT License. using System; +using System.ClientModel.Primitives; +using System.Threading.Tasks; using Azure.Core.Pipeline; +using Azure.Core.TestFramework; using NUnit.Framework; namespace Azure.Core.Tests @@ -10,7 +13,7 @@ namespace Azure.Core.Tests public class NextLinkOperationImplementationTests { [Test] - public void ConstructRehydrationTokenTest() + public void ConstructRehydrationToken() { var requetMethod = RequestMethod.Get; var startRequestUri = new Uri("https://test"); @@ -28,7 +31,7 @@ public void ConstructRehydrationTokenTest() } [Test] - public void ConstructNextLinkOperationTest() + public void ConstructNextLinkOperation() { var operationId = Guid.NewGuid().ToString(); var requestMethod = RequestMethod.Delete; @@ -39,6 +42,19 @@ public void ConstructNextLinkOperationTest() Assert.AreEqual(requestMethod, operation.RequestMethod); } + [Test] + public async Task FinalGetForDeleteLRO() + { + var operationId = Guid.NewGuid().ToString(); + var requestMethod = RequestMethod.Delete; + var rehydrationToken = new RehydrationToken(null, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test", requestMethod, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); + var operation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(CreateMockHttpPipeline(), rehydrationToken, null); + Assert.NotNull(operation); + + var result = await operation.UpdateStateAsync(true, default); + Assert.AreEqual(204, result.RawResponse.Status); + } + [Test] public void GetNullOperationIdWithIvalidNextRequestUri() { @@ -65,6 +81,14 @@ public void ThrowOnNextLinkOperationImplementationCreateWithNullHttpPipeline() Assert.Throws(() => NextLinkOperationImplementation.Create(null, new RehydrationToken(null, null, "None", "nextRequestUri", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()), null)); } + private static HttpPipeline CreateMockHttpPipeline() + { + var mockResponse = new MockResponse(404); + var transport = new MockTransport(mockResponse, mockResponse); + var pipeline = new HttpPipeline(transport, default); + return pipeline; + } + private class MockClientOptions : ClientOptions { } From 28435000c3a6438f9c174af71f259d044e6e5ac1 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 21 Mar 2024 15:43:15 +0800 Subject: [PATCH 12/32] fix test --- sdk/core/Azure.Core/tests/OperationTests.cs | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/sdk/core/Azure.Core/tests/OperationTests.cs b/sdk/core/Azure.Core/tests/OperationTests.cs index e867aa74ad16..bbfe4d43d46d 100644 --- a/sdk/core/Azure.Core/tests/OperationTests.cs +++ b/sdk/core/Azure.Core/tests/OperationTests.cs @@ -143,15 +143,12 @@ public void RehydrateOperation() var operation = new RehydrationOperation(pipeline, rehydrationToken); Assert.NotNull(operation); Assert.AreEqual(operationId, operation.Id); - Assert.Throws(() => operation.GetRawResponse()); - Assert.False(operation.HasCompleted); - - operation.UpdateStatus(); + Assert.True(operation.HasCompleted); Assert.AreEqual(200, operation.GetRawResponse().Status); } [Test] - public async Task RehydrateOperationOfT() + public void RehydrateOperationOfT() { var pipeline = CreateMockHttpPipeline(out var mockJsonModel); var operationId = Guid.NewGuid().ToString(); @@ -159,12 +156,8 @@ public async Task RehydrateOperationOfT() var operation = new RehydrationOperation(pipeline, rehydrationToken); Assert.NotNull(operation); Assert.AreEqual(operationId, operation.Id); - Assert.Throws(() => operation.GetRawResponse()); - Assert.False(operation.HasCompleted); - Assert.Throws(() => { var value = operation.Value; }); - Assert.False(operation.HasValue); - - await operation.UpdateStatusAsync(); + Assert.True(operation.HasCompleted); + Assert.True(operation.HasValue); Assert.AreEqual(200, operation.GetRawResponse().Status); Assert.AreEqual(ModelReaderWriter.Write(mockJsonModel).ToString(), ModelReaderWriter.Write(operation.Value).ToString()); } From 1231a36b4914728d5559c9fed951ad1bd0cbae08 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 21 Mar 2024 15:55:11 +0800 Subject: [PATCH 13/32] update changelog and test --- sdk/core/Azure.Core/CHANGELOG.md | 2 ++ sdk/core/Azure.Core/tests/OperationTests.cs | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/sdk/core/Azure.Core/CHANGELOG.md b/sdk/core/Azure.Core/CHANGELOG.md index 90e4b1135487..47090c90e46d 100644 --- a/sdk/core/Azure.Core/CHANGELOG.md +++ b/sdk/core/Azure.Core/CHANGELOG.md @@ -4,6 +4,8 @@ ### Features Added +- Add `Operation.Rehydrate` and `Operation.Rehydrate` static methods to rehydrate a long-running operation. + ### Breaking Changes ### Bugs Fixed diff --git a/sdk/core/Azure.Core/tests/OperationTests.cs b/sdk/core/Azure.Core/tests/OperationTests.cs index bbfe4d43d46d..f436a56625a9 100644 --- a/sdk/core/Azure.Core/tests/OperationTests.cs +++ b/sdk/core/Azure.Core/tests/OperationTests.cs @@ -140,7 +140,7 @@ public void RehydrateOperation() var pipeline = CreateMockHttpPipeline(out _); var operationId = Guid.NewGuid().ToString(); var rehydrationToken = new RehydrationToken(null, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); - var operation = new RehydrationOperation(pipeline, rehydrationToken); + var operation = Operation.Rehydrate(pipeline, rehydrationToken); Assert.NotNull(operation); Assert.AreEqual(operationId, operation.Id); Assert.True(operation.HasCompleted); @@ -153,7 +153,7 @@ public void RehydrateOperationOfT() var pipeline = CreateMockHttpPipeline(out var mockJsonModel); var operationId = Guid.NewGuid().ToString(); var rehydrationToken = new RehydrationToken(null, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); - var operation = new RehydrationOperation(pipeline, rehydrationToken); + var operation = Operation.Rehydrate(pipeline, rehydrationToken); Assert.NotNull(operation); Assert.AreEqual(operationId, operation.Id); Assert.True(operation.HasCompleted); From adc92770313b373c0e0ed3afd642114d21b1b967 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 21 Mar 2024 15:59:25 +0800 Subject: [PATCH 14/32] cleanup --- sdk/core/Azure.Core/src/Shared/OperationInternalOfT.cs | 2 -- .../Azure.Core/tests/NextLinkOperationImplementationTests.cs | 1 - 2 files changed, 3 deletions(-) diff --git a/sdk/core/Azure.Core/src/Shared/OperationInternalOfT.cs b/sdk/core/Azure.Core/src/Shared/OperationInternalOfT.cs index c538299492ad..ed9cd18ca381 100644 --- a/sdk/core/Azure.Core/src/Shared/OperationInternalOfT.cs +++ b/sdk/core/Azure.Core/src/Shared/OperationInternalOfT.cs @@ -5,8 +5,6 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Net; using System.Threading; using System.Threading.Tasks; using Azure.Core.Pipeline; diff --git a/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs b/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs index 1000b0e468da..670bf2806afd 100644 --- a/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs +++ b/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System; -using System.ClientModel.Primitives; using System.Threading.Tasks; using Azure.Core.Pipeline; using Azure.Core.TestFramework; From 544068baa095c734744e80e197ce228edd256deb Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Tue, 26 Mar 2024 12:09:19 +0800 Subject: [PATCH 15/32] address comments --- .../src/Internal/RehydrationOperation.cs | 8 +-- .../src/Internal/RehydrationOperationOfT.cs | 8 +-- sdk/core/Azure.Core/src/Operation.cs | 4 +- sdk/core/Azure.Core/src/RehydrationToken.cs | 2 +- .../Shared/NextLinkOperationImplementation.cs | 68 +++++++++++++------ .../NextLinkOperationImplementationTests.cs | 17 +++-- 6 files changed, 64 insertions(+), 43 deletions(-) diff --git a/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs b/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs index 4144d6ed7df6..b4fdf5d27595 100644 --- a/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs +++ b/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -#nullable disable - using System.Threading; using System.Threading.Tasks; using Azure.Core.Pipeline; @@ -14,7 +12,7 @@ internal class RehydrationOperation : Operation private readonly NextLinkOperationImplementation _nextLinkOperation; private readonly OperationInternal _operation; - public RehydrationOperation(HttpPipeline pipeline, RehydrationToken? rehydrationToken, ClientOptions options = null) + public RehydrationOperation(HttpPipeline pipeline, RehydrationToken rehydrationToken, ClientOptions? options = null) { Argument.AssertNotNull(pipeline, nameof(pipeline)); Argument.AssertNotNull(rehydrationToken, nameof(rehydrationToken)); @@ -25,9 +23,9 @@ public RehydrationOperation(HttpPipeline pipeline, RehydrationToken? rehydration : new OperationInternal(_nextLinkOperation, new ClientDiagnostics(options ?? ClientOptions.Default), operationState.RawResponse); } - public override string Id => _nextLinkOperation?.OperationId ?? null; + public override string Id => _nextLinkOperation.OperationId; - public override RehydrationToken? GetRehydrationToken() => _nextLinkOperation?.GetRehydrationToken(); + public override RehydrationToken? GetRehydrationToken() => _nextLinkOperation.GetRehydrationToken(); public override bool HasCompleted => _operation.HasCompleted; diff --git a/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs b/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs index a2b1e9f161f8..b69a479d6d2b 100644 --- a/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs +++ b/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -#nullable disable - using System.ClientModel.Primitives; using System.Threading; using System.Threading.Tasks; @@ -17,7 +15,7 @@ internal class RehydrationOperation : Operation where T : IPersistableMode private readonly OperationInternal _operation; private readonly NextLinkOperationImplementation _nextLinkOperation; - public RehydrationOperation(HttpPipeline pipeline, RehydrationToken? rehydrationToken, ClientOptions options = null) + public RehydrationOperation(HttpPipeline pipeline, RehydrationToken rehydrationToken, ClientOptions? options = null) { Argument.AssertNotNull(pipeline, nameof(pipeline)); Argument.AssertNotNull(rehydrationToken, nameof(rehydrationToken)); @@ -35,9 +33,9 @@ public RehydrationOperation(HttpPipeline pipeline, RehydrationToken? rehydration public override bool HasValue => _operation.HasValue; - public override string Id => _nextLinkOperation.OperationId ?? null; + public override string Id => _nextLinkOperation.OperationId; - public override RehydrationToken? GetRehydrationToken() => _nextLinkOperation?.GetRehydrationToken(); + public override RehydrationToken? GetRehydrationToken() => _nextLinkOperation.GetRehydrationToken(); public override bool HasCompleted => _operation.HasCompleted; diff --git a/sdk/core/Azure.Core/src/Operation.cs b/sdk/core/Azure.Core/src/Operation.cs index 3e2ec44d76a5..4bb3711c49be 100644 --- a/sdk/core/Azure.Core/src/Operation.cs +++ b/sdk/core/Azure.Core/src/Operation.cs @@ -25,7 +25,7 @@ public abstract class Operation /// The rehydration token. /// The client options. /// The long-running operation. - public static Operation Rehydrate(HttpPipeline pipeline, RehydrationToken? rehydrationToken, ClientOptions? options = null) where T : IPersistableModel + public static Operation Rehydrate(HttpPipeline pipeline, RehydrationToken rehydrationToken, ClientOptions? options = null) where T : IPersistableModel => new RehydrationOperation(pipeline, rehydrationToken, options); /// @@ -35,7 +35,7 @@ public static Operation Rehydrate(HttpPipeline pipeline, RehydrationToken? /// The rehydration token. /// The client options. /// The long-running operation. - public static Operation Rehydrate(HttpPipeline pipeline, RehydrationToken? rehydrationToken, ClientOptions? options = null) + public static Operation Rehydrate(HttpPipeline pipeline, RehydrationToken rehydrationToken, ClientOptions? options = null) => new RehydrationOperation(pipeline, rehydrationToken, options); /// diff --git a/sdk/core/Azure.Core/src/RehydrationToken.cs b/sdk/core/Azure.Core/src/RehydrationToken.cs index c3e21a3881a1..1b321fc1038a 100644 --- a/sdk/core/Azure.Core/src/RehydrationToken.cs +++ b/sdk/core/Azure.Core/src/RehydrationToken.cs @@ -15,7 +15,7 @@ public readonly partial struct RehydrationToken public string? Id { get; } // Version for this contract itself since we might change the members in the future. - internal string Version { get; } = NextLinkOperationImplementation.RehydartionTokenVersion; + internal string Version { get; } = NextLinkOperationImplementation.RehydrationTokenVersion; // The below members are used to re-construct . // Value of . diff --git a/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs b/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs index 212d90bcd2e1..9f707b112a73 100644 --- a/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs +++ b/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs @@ -18,7 +18,7 @@ namespace Azure.Core { internal class NextLinkOperationImplementation : IOperation { - internal const string RehydartionTokenVersion = "1.0.0"; + internal const string RehydrationTokenVersion = "1.0.0"; private const string ApiVersionParam = "api-version"; private static readonly string[] FailureStates = { "failed", "canceled" }; private static readonly string[] SuccessStates = { "succeeded" }; @@ -32,7 +32,7 @@ internal class NextLinkOperationImplementation : IOperation private string? _lastKnownLocation; private string _nextRequestUri; - public string? OperationId { get; } + public string OperationId { get; } public RequestMethod RequestMethod { get; } public static IOperation Create( @@ -83,50 +83,71 @@ public static IOperation Create( public static IOperation Create( HttpPipeline pipeline, - RehydrationToken? rehydrationToken, - string? apiVersionOverride = null) + RehydrationToken rehydrationToken, + bool skipApiVersionOverride = false, + string? apiVersionOverrideValue = null) { AssertNotNull(rehydrationToken, nameof(rehydrationToken)); + AssertNotNull(pipeline, nameof(pipeline)); var lroDetails = ModelReaderWriter.Write(rehydrationToken!, ModelReaderWriterOptions.Json).ToObjectFromJson>(); - if (!Uri.TryCreate(lroDetails["initialUri"], UriKind.Absolute, out var startRequestUri)) - { - throw new ArgumentException("Invalid initial URI from rehydration token"); - } - if (!lroDetails.TryGetValue("nextRequestUri", out var nextRequestUri)) + var initialUri = GetContentFromRehydrationToken(lroDetails, "initialUri"); + if (!Uri.TryCreate(initialUri, UriKind.Absolute, out var startRequestUri)) { - throw new ArgumentException("Invalid next request URI from rehydration token"); + throw new ArgumentException($"\"initialUri\" is invalid Uri from {nameof(rehydrationToken)}"); } - RequestMethod requestMethod = new RequestMethod(lroDetails["requestMethod"]); - string lastKnownLocation = lroDetails["lastKnownLocation"]; + string nextRequestUri = GetContentFromRehydrationToken(lroDetails, "nextRequestUri"); + string requestMethodStr = GetContentFromRehydrationToken(lroDetails, "requestMethod"); + RequestMethod requestMethod = new RequestMethod(requestMethodStr); + string lastKnownLocation = GetContentFromRehydrationToken(lroDetails, "lastKnownLocation"); + string finalStateViaStr = GetContentFromRehydrationToken(lroDetails, "finalStateVia"); OperationFinalStateVia finalStateVia; - if (Enum.IsDefined(typeof(OperationFinalStateVia), lroDetails["finalStateVia"])) + if (Enum.IsDefined(typeof(OperationFinalStateVia), finalStateViaStr)) { - finalStateVia = (OperationFinalStateVia)Enum.Parse(typeof(OperationFinalStateVia), lroDetails["finalStateVia"]); + finalStateVia = (OperationFinalStateVia)Enum.Parse(typeof(OperationFinalStateVia), finalStateViaStr); } else { finalStateVia = OperationFinalStateVia.Location; } + string headerSourceStr = GetContentFromRehydrationToken(lroDetails, "headerSource"); HeaderSource headerSource; - if (Enum.IsDefined(typeof(HeaderSource), lroDetails["headerSource"])) + if (Enum.IsDefined(typeof(HeaderSource), headerSourceStr)) { - headerSource = (HeaderSource)Enum.Parse(typeof(HeaderSource), lroDetails["headerSource"]); + headerSource = (HeaderSource)Enum.Parse(typeof(HeaderSource), headerSourceStr); } else { headerSource = HeaderSource.None; } - string? apiVersionStr = apiVersionOverride ?? (TryGetApiVersion(startRequestUri, out ReadOnlySpan apiVersion) ? apiVersion.ToString() : null); + string? apiVersionStr; + if (apiVersionOverrideValue is not null) + { + apiVersionStr = apiVersionOverrideValue; + } + else + { + apiVersionStr = !skipApiVersionOverride && TryGetApiVersion(startRequestUri, out ReadOnlySpan apiVersion) ? apiVersion.ToString() : null; + } return new NextLinkOperationImplementation(pipeline, requestMethod, startRequestUri, nextRequestUri, headerSource, lastKnownLocation, finalStateVia, apiVersionStr); } + private static string GetContentFromRehydrationToken(Dictionary lroDetails, string key) + { + if (!lroDetails.TryGetValue(key, out var nextRequestUri)) + { + throw new ArgumentException($"\"{key}\" is missing from rehydrationToken"); + } + + return nextRequestUri; + } + private NextLinkOperationImplementation( HttpPipeline pipeline, RequestMethod requestMethod, @@ -155,13 +176,13 @@ private NextLinkOperationImplementation( OperationId = ParseOperationId(nextRequestUri); } - private static string? ParseOperationId(string nextRequestUri) + private static string ParseOperationId(string nextRequestUri) { if (Uri.TryCreate(nextRequestUri, UriKind.Absolute, out var uri)) { - return uri.Segments.LastOrDefault(); + return uri.Segments.Last(); } - return null; + throw new ArgumentException($"{nameof(nextRequestUri)} is not a valid Uri"); } public RehydrationToken GetRehydrationToken() @@ -175,6 +196,11 @@ public static RehydrationToken GetRehydrationToken( bool skipApiVersionOverride = false, string? apiVersionOverrideValue = null) { + AssertNotNull(requestMethod, nameof(requestMethod)); + AssertNotNull(startRequestUri, nameof(startRequestUri)); + AssertNotNull(response, nameof(response)); + AssertNotNull(finalStateVia, nameof(finalStateVia)); + string? apiVersionStr; if (apiVersionOverrideValue is not null) { @@ -199,7 +225,7 @@ public static RehydrationToken GetRehydrationToken( string finalStateVia, string? operationId = null) { - var data = new BinaryData(new { version = RehydartionTokenVersion, id = operationId, requestMethod = requestMethod.ToString(), initialUri = startRequestUri.AbsoluteUri, nextRequestUri, headerSource, lastKnownLocation, finalStateVia }); + var data = new BinaryData(new { version = RehydrationTokenVersion, id = operationId, requestMethod = requestMethod.ToString(), initialUri = startRequestUri.AbsoluteUri, nextRequestUri, headerSource, lastKnownLocation, finalStateVia }); return ModelReaderWriter.Read(data); } diff --git a/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs b/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs index 670bf2806afd..a3b1330e4ef3 100644 --- a/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs +++ b/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs @@ -35,7 +35,7 @@ public void ConstructNextLinkOperation() var operationId = Guid.NewGuid().ToString(); var requestMethod = RequestMethod.Delete; var rehydrationToken = new RehydrationToken(null, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test", requestMethod, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); - var operation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(HttpPipelineBuilder.Build(new MockClientOptions()), rehydrationToken, null); + var operation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(HttpPipelineBuilder.Build(new MockClientOptions()), rehydrationToken); Assert.NotNull(operation); Assert.AreEqual(operationId, operation.OperationId); Assert.AreEqual(requestMethod, operation.RequestMethod); @@ -47,7 +47,7 @@ public async Task FinalGetForDeleteLRO() var operationId = Guid.NewGuid().ToString(); var requestMethod = RequestMethod.Delete; var rehydrationToken = new RehydrationToken(null, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test", requestMethod, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); - var operation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(CreateMockHttpPipeline(), rehydrationToken, null); + var operation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(CreateMockHttpPipeline(), rehydrationToken); Assert.NotNull(operation); var result = await operation.UpdateStateAsync(true, default); @@ -55,29 +55,28 @@ public async Task FinalGetForDeleteLRO() } [Test] - public void GetNullOperationIdWithIvalidNextRequestUri() + public void ThrowWithIvalidNextRequestUri() { var rehydrationToken = new RehydrationToken(null, null, "None", $"invalidNextRequestUri", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); - var operation = NextLinkOperationImplementation.Create(HttpPipelineBuilder.Build(new MockClientOptions()), rehydrationToken, null); - Assert.Null(((NextLinkOperationImplementation)operation).OperationId); + Assert.Throws(() =>NextLinkOperationImplementation.Create(HttpPipelineBuilder.Build(new MockClientOptions()), rehydrationToken)); } [Test] - public void ThrowOnNextLinkOperationImplementationCreateWithNullRehydrationToken() + public void ThrowWithDefaultRehydrationToken() { - Assert.Throws(() => NextLinkOperationImplementation.Create(HttpPipelineBuilder.Build(new MockClientOptions()), null)); + Assert.Throws(() => NextLinkOperationImplementation.Create(HttpPipelineBuilder.Build(new MockClientOptions()), default)); } [Test] public void ThrowOnInvalidUri() { - Assert.Throws(() => NextLinkOperationImplementation.Create(HttpPipelineBuilder.Build(new MockClientOptions()), default(RehydrationToken))); + Assert.Throws(() => NextLinkOperationImplementation.Create(HttpPipelineBuilder.Build(new MockClientOptions()), default)); } [Test] public void ThrowOnNextLinkOperationImplementationCreateWithNullHttpPipeline() { - Assert.Throws(() => NextLinkOperationImplementation.Create(null, new RehydrationToken(null, null, "None", "nextRequestUri", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()), null)); + Assert.Throws(() => NextLinkOperationImplementation.Create(null, new RehydrationToken(null, null, "None", "nextRequestUri", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()), default)); } private static HttpPipeline CreateMockHttpPipeline() From 695bbed302a49f90754934238f8b570dc6e41816 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Tue, 26 Mar 2024 14:14:02 +0800 Subject: [PATCH 16/32] Add async rehdyrate methods --- sdk/core/Azure.Core/api/Azure.Core.net461.cs | 6 +- sdk/core/Azure.Core/api/Azure.Core.net472.cs | 6 +- sdk/core/Azure.Core/api/Azure.Core.net6.0.cs | 6 +- .../api/Azure.Core.netstandard2.0.cs | 6 +- .../src/Internal/RehydrationOperation.cs | 7 +- .../src/Internal/RehydrationOperationOfT.cs | 10 +- sdk/core/Azure.Core/src/Operation.cs | 54 ++++++++- sdk/core/Azure.Core/tests/OperationTests.cs | 88 +++++++++++++- .../tests/RehydrationOperationTests.cs | 109 ------------------ 9 files changed, 158 insertions(+), 134 deletions(-) delete mode 100644 sdk/core/Azure.Core/tests/RehydrationOperationTests.cs diff --git a/sdk/core/Azure.Core/api/Azure.Core.net461.cs b/sdk/core/Azure.Core/api/Azure.Core.net461.cs index 643dec8f26cc..5a68b08cc363 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net461.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net461.cs @@ -138,8 +138,10 @@ protected Operation() { } public override int GetHashCode() { throw null; } public abstract Azure.Response GetRawResponse(); public virtual Azure.Core.RehydrationToken? GetRehydrationToken() { throw null; } - public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken? rehydrationToken, Azure.Core.ClientOptions? options = null) { throw null; } - public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken? rehydrationToken, Azure.Core.ClientOptions? options = null) where T : System.ClientModel.Primitives.IPersistableModel { throw null; } + public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken rehydrationToken, Azure.Core.ClientOptions? options = null) { throw null; } + public static System.Threading.Tasks.Task RehydrateAsync(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken rehydrationToken, Azure.Core.ClientOptions? options = null) { throw null; } + public static System.Threading.Tasks.Task> RehydrateAsync(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken rehydrationToken, Azure.Core.ClientOptions? options = null) where T : System.ClientModel.Primitives.IPersistableModel { throw null; } + public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken rehydrationToken, Azure.Core.ClientOptions? options = null) where T : System.ClientModel.Primitives.IPersistableModel { throw null; } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public override string? ToString() { throw null; } public abstract Azure.Response UpdateStatus(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); diff --git a/sdk/core/Azure.Core/api/Azure.Core.net472.cs b/sdk/core/Azure.Core/api/Azure.Core.net472.cs index 643dec8f26cc..5a68b08cc363 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net472.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net472.cs @@ -138,8 +138,10 @@ protected Operation() { } public override int GetHashCode() { throw null; } public abstract Azure.Response GetRawResponse(); public virtual Azure.Core.RehydrationToken? GetRehydrationToken() { throw null; } - public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken? rehydrationToken, Azure.Core.ClientOptions? options = null) { throw null; } - public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken? rehydrationToken, Azure.Core.ClientOptions? options = null) where T : System.ClientModel.Primitives.IPersistableModel { throw null; } + public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken rehydrationToken, Azure.Core.ClientOptions? options = null) { throw null; } + public static System.Threading.Tasks.Task RehydrateAsync(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken rehydrationToken, Azure.Core.ClientOptions? options = null) { throw null; } + public static System.Threading.Tasks.Task> RehydrateAsync(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken rehydrationToken, Azure.Core.ClientOptions? options = null) where T : System.ClientModel.Primitives.IPersistableModel { throw null; } + public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken rehydrationToken, Azure.Core.ClientOptions? options = null) where T : System.ClientModel.Primitives.IPersistableModel { throw null; } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public override string? ToString() { throw null; } public abstract Azure.Response UpdateStatus(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); 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 9c4c99b7cab7..68561668b131 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net6.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net6.0.cs @@ -138,8 +138,10 @@ protected Operation() { } public override int GetHashCode() { throw null; } public abstract Azure.Response GetRawResponse(); public virtual Azure.Core.RehydrationToken? GetRehydrationToken() { throw null; } - public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken? rehydrationToken, Azure.Core.ClientOptions? options = null) { throw null; } - public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken? rehydrationToken, Azure.Core.ClientOptions? options = null) where T : System.ClientModel.Primitives.IPersistableModel { throw null; } + public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken rehydrationToken, Azure.Core.ClientOptions? options = null) { throw null; } + public static System.Threading.Tasks.Task RehydrateAsync(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken rehydrationToken, Azure.Core.ClientOptions? options = null) { throw null; } + public static System.Threading.Tasks.Task> RehydrateAsync(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken rehydrationToken, Azure.Core.ClientOptions? options = null) where T : System.ClientModel.Primitives.IPersistableModel { throw null; } + public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken rehydrationToken, Azure.Core.ClientOptions? options = null) where T : System.ClientModel.Primitives.IPersistableModel { throw null; } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public override string? ToString() { throw null; } public abstract Azure.Response UpdateStatus(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); 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 643dec8f26cc..5a68b08cc363 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs @@ -138,8 +138,10 @@ protected Operation() { } public override int GetHashCode() { throw null; } public abstract Azure.Response GetRawResponse(); public virtual Azure.Core.RehydrationToken? GetRehydrationToken() { throw null; } - public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken? rehydrationToken, Azure.Core.ClientOptions? options = null) { throw null; } - public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken? rehydrationToken, Azure.Core.ClientOptions? options = null) where T : System.ClientModel.Primitives.IPersistableModel { throw null; } + public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken rehydrationToken, Azure.Core.ClientOptions? options = null) { throw null; } + public static System.Threading.Tasks.Task RehydrateAsync(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken rehydrationToken, Azure.Core.ClientOptions? options = null) { throw null; } + public static System.Threading.Tasks.Task> RehydrateAsync(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken rehydrationToken, Azure.Core.ClientOptions? options = null) where T : System.ClientModel.Primitives.IPersistableModel { throw null; } + public static Azure.Operation Rehydrate(Azure.Core.Pipeline.HttpPipeline pipeline, Azure.Core.RehydrationToken rehydrationToken, Azure.Core.ClientOptions? options = null) where T : System.ClientModel.Primitives.IPersistableModel { throw null; } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public override string? ToString() { throw null; } public abstract Azure.Response UpdateStatus(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); diff --git a/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs b/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs index b4fdf5d27595..6f82a9153945 100644 --- a/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs +++ b/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs @@ -12,12 +12,9 @@ internal class RehydrationOperation : Operation private readonly NextLinkOperationImplementation _nextLinkOperation; private readonly OperationInternal _operation; - public RehydrationOperation(HttpPipeline pipeline, RehydrationToken rehydrationToken, ClientOptions? options = null) + public RehydrationOperation(NextLinkOperationImplementation nextLinkOperation, OperationState operationState, ClientOptions? options = null) { - Argument.AssertNotNull(pipeline, nameof(pipeline)); - Argument.AssertNotNull(rehydrationToken, nameof(rehydrationToken)); - _nextLinkOperation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(pipeline, rehydrationToken); - var operationState = _nextLinkOperation.UpdateStateAsync(false, default).EnsureCompleted(); + _nextLinkOperation = nextLinkOperation; _operation = operationState.HasCompleted ? _operation = new OperationInternal(operationState) : new OperationInternal(_nextLinkOperation, new ClientDiagnostics(options ?? ClientOptions.Default), operationState.RawResponse); diff --git a/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs b/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs index b69a479d6d2b..933957f63dd1 100644 --- a/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs +++ b/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs @@ -15,15 +15,9 @@ internal class RehydrationOperation : Operation where T : IPersistableMode private readonly OperationInternal _operation; private readonly NextLinkOperationImplementation _nextLinkOperation; - public RehydrationOperation(HttpPipeline pipeline, RehydrationToken rehydrationToken, ClientOptions? options = null) + public RehydrationOperation(NextLinkOperationImplementation nextLinkOperation, OperationState operationState, IOperation operation, ClientOptions? options = null) { - Argument.AssertNotNull(pipeline, nameof(pipeline)); - Argument.AssertNotNull(rehydrationToken, nameof(rehydrationToken)); - - IOperationSource source = new GenericOperationSource(); - _nextLinkOperation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(pipeline, rehydrationToken); - var operation = NextLinkOperationImplementation.Create(source, _nextLinkOperation); - var operationState = operation.UpdateStateAsync(false, default).EnsureCompleted(); + _nextLinkOperation = nextLinkOperation; _operation = operationState.HasCompleted ? new OperationInternal(operationState) : new OperationInternal(operation, new ClientDiagnostics(options ?? ClientOptions.Default), operationState.RawResponse); diff --git a/sdk/core/Azure.Core/src/Operation.cs b/sdk/core/Azure.Core/src/Operation.cs index 4bb3711c49be..6e67b68591d2 100644 --- a/sdk/core/Azure.Core/src/Operation.cs +++ b/sdk/core/Azure.Core/src/Operation.cs @@ -26,7 +26,16 @@ public abstract class Operation /// The client options. /// The long-running operation. public static Operation Rehydrate(HttpPipeline pipeline, RehydrationToken rehydrationToken, ClientOptions? options = null) where T : IPersistableModel - => new RehydrationOperation(pipeline, rehydrationToken, options); + { + Argument.AssertNotNull(pipeline, nameof(pipeline)); + Argument.AssertNotNull(rehydrationToken, nameof(rehydrationToken)); + + IOperationSource source = new GenericOperationSource(); + var nextLinkOperation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(pipeline, rehydrationToken); + var operation = NextLinkOperationImplementation.Create(source, nextLinkOperation); + var operationState = operation.UpdateStateAsync(false, default).EnsureCompleted(); + return new RehydrationOperation(nextLinkOperation, operationState, operation, options); + } /// /// Rehydrates an operation from a . @@ -36,7 +45,48 @@ public static Operation Rehydrate(HttpPipeline pipeline, RehydrationToken /// The client options. /// The long-running operation. public static Operation Rehydrate(HttpPipeline pipeline, RehydrationToken rehydrationToken, ClientOptions? options = null) - => new RehydrationOperation(pipeline, rehydrationToken, options); + { + Argument.AssertNotNull(pipeline, nameof(pipeline)); + Argument.AssertNotNull(rehydrationToken, nameof(rehydrationToken)); + var nextLinkOperation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(pipeline, rehydrationToken); + var operationState = nextLinkOperation.UpdateStateAsync(false, default).EnsureCompleted(); + return new RehydrationOperation(nextLinkOperation, operationState); + } + + /// + /// Rehydrates an operation from a . + /// + /// The Http pipeline. + /// The rehydration token. + /// The client options. + /// The long-running operation. + public static async Task> RehydrateAsync(HttpPipeline pipeline, RehydrationToken rehydrationToken, ClientOptions? options = null) where T : IPersistableModel + { + Argument.AssertNotNull(pipeline, nameof(pipeline)); + Argument.AssertNotNull(rehydrationToken, nameof(rehydrationToken)); + + IOperationSource source = new GenericOperationSource(); + var nextLinkOperation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(pipeline, rehydrationToken); + var operation = NextLinkOperationImplementation.Create(source, nextLinkOperation); + var operationState = await operation.UpdateStateAsync(true, default).ConfigureAwait(false); + return new RehydrationOperation(nextLinkOperation, operationState, operation, options); + } + + /// + /// Rehydrates an operation from a . + /// + /// The Http pipeline. + /// The rehydration token. + /// The client options. + /// The long-running operation. + public static async Task RehydrateAsync(HttpPipeline pipeline, RehydrationToken rehydrationToken, ClientOptions? options = null) + { + Argument.AssertNotNull(pipeline, nameof(pipeline)); + Argument.AssertNotNull(rehydrationToken, nameof(rehydrationToken)); + var nextLinkOperation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(pipeline, rehydrationToken); + var operationState = await nextLinkOperation.UpdateStateAsync(true, default).ConfigureAwait(false); + return new RehydrationOperation(nextLinkOperation, operationState); + } /// /// Get a token that can be used to rehydrate the operation. diff --git a/sdk/core/Azure.Core/tests/OperationTests.cs b/sdk/core/Azure.Core/tests/OperationTests.cs index f436a56625a9..3d1e2de41529 100644 --- a/sdk/core/Azure.Core/tests/OperationTests.cs +++ b/sdk/core/Azure.Core/tests/OperationTests.cs @@ -135,7 +135,19 @@ public void OperationId() } [Test] - public void RehydrateOperation() + public void ThrowOnNullArgumentWhileRehydrateAsync() + { + Assert.ThrowsAsync(() => Operation.RehydrateAsync(null, new RehydrationToken())); + } + + [Test] + public void ThrowOnNullArgumentWhileRehydrate() + { + Assert.Throws(() => Operation.Rehydrate(null, new RehydrationToken())); + } + + [Test] + public void Rehydrate() { var pipeline = CreateMockHttpPipeline(out _); var operationId = Guid.NewGuid().ToString(); @@ -148,7 +160,20 @@ public void RehydrateOperation() } [Test] - public void RehydrateOperationOfT() + public async Task RehydrateAsync() + { + var pipeline = CreateMockHttpPipeline(out _); + var operationId = Guid.NewGuid().ToString(); + var rehydrationToken = new RehydrationToken(null, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); + var operation = await Operation.RehydrateAsync(pipeline, rehydrationToken); + Assert.NotNull(operation); + Assert.AreEqual(operationId, operation.Id); + Assert.True(operation.HasCompleted); + Assert.AreEqual(200, operation.GetRawResponse().Status); + } + + [Test] + public void RehydrateOfT() { var pipeline = CreateMockHttpPipeline(out var mockJsonModel); var operationId = Guid.NewGuid().ToString(); @@ -162,6 +187,53 @@ public void RehydrateOperationOfT() Assert.AreEqual(ModelReaderWriter.Write(mockJsonModel).ToString(), ModelReaderWriter.Write(operation.Value).ToString()); } + [Test] + public async Task RehydrateAsyncOfT() + { + var pipeline = CreateMockHttpPipeline(out var mockJsonModel); + var operationId = Guid.NewGuid().ToString(); + var rehydrationToken = new RehydrationToken(null, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); + var operation = await Operation.RehydrateAsync(pipeline, rehydrationToken); + Assert.NotNull(operation); + Assert.AreEqual(operationId, operation.Id); + Assert.True(operation.HasCompleted); + Assert.True(operation.HasValue); + Assert.AreEqual(200, operation.GetRawResponse().Status); + Assert.AreEqual(ModelReaderWriter.Write(mockJsonModel).ToString(), ModelReaderWriter.Write(operation.Value).ToString()); + } + + [Test] + public async Task ConstructOperationForRehydrationWithFailure() + { + HttpPipeline pipeline = CreateMockHttpPipelineWithFailure(); + var operationId = Guid.NewGuid().ToString(); + var rehydrationToken = new RehydrationToken(null, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); + var operation = await Operation.RehydrateAsync(pipeline, rehydrationToken); + Assert.NotNull(operation); + Assert.AreEqual(500, operation.GetRawResponse().Status); + Assert.True(operation.HasCompleted); + } + + [Test] + public async Task GetRehydrationTokenAsync() + { + var operationId = Guid.NewGuid().ToString(); + var rehydrationToken = new RehydrationToken(operationId, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test/", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); + var operation = await Operation.RehydrateAsync(HttpPipelineBuilder.Build(new MockClientOptions()), rehydrationToken); + var token = operation.GetRehydrationToken(); + Assert.AreEqual(ModelReaderWriter.Write(rehydrationToken).ToString(), ModelReaderWriter.Write(token).ToString()); + } + + [Test] + public async Task GetRehydrationTokenOfTAsync() + { + var operationId = Guid.NewGuid().ToString(); + var rehydrationToken = new RehydrationToken(operationId, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test/", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); + var operation = await Operation.RehydrateAsync(HttpPipelineBuilder.Build(new MockClientOptions()), rehydrationToken); + var token = operation.GetRehydrationToken(); + Assert.AreEqual(ModelReaderWriter.Write(rehydrationToken).ToString(), ModelReaderWriter.Write(token).ToString()); + } + private static HttpPipeline CreateMockHttpPipeline(out MockJsonModel mockJsonModel) { var mockResponse = new MockResponse(200); @@ -171,5 +243,17 @@ private static HttpPipeline CreateMockHttpPipeline(out MockJsonModel mockJsonMod var pipeline = new HttpPipeline(transport, default); return pipeline; } + + private static HttpPipeline CreateMockHttpPipelineWithFailure() + { + var mockResponse = new MockResponse(500); + var transport = new MockTransport(mockResponse, mockResponse); + var pipeline = new HttpPipeline(transport, default); + return pipeline; + } + + private class MockClientOptions : ClientOptions + { + } } } diff --git a/sdk/core/Azure.Core/tests/RehydrationOperationTests.cs b/sdk/core/Azure.Core/tests/RehydrationOperationTests.cs deleted file mode 100644 index 718a47d53cf3..000000000000 --- a/sdk/core/Azure.Core/tests/RehydrationOperationTests.cs +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.ClientModel.Primitives; -using Azure.Core.Pipeline; -using Azure.Core.TestFramework; -using NUnit.Framework; - -namespace Azure.Core.Tests -{ - public class RehydrationOperationTests - { - [Test] - public void ThrowOnNullArgument() - { - Assert.Throws(() => new RehydrationOperation(null, new RehydrationToken())); - } - - [Test] - public void ConstructOperationForRehydration() - { - HttpPipeline pipeline = CreateMockHttpPipeline(out _); - var operationId = Guid.NewGuid().ToString(); - var rehydrationToken = new RehydrationToken(null, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); - var operation = new RehydrationOperation(pipeline, rehydrationToken); - Assert.NotNull(operation); - Assert.AreEqual(operationId, operation.Id); - Assert.AreEqual(200, operation.GetRawResponse().Status); - Assert.True(operation.HasCompleted); - - operation.UpdateStatus(); - Assert.AreEqual(200, operation.GetRawResponse().Status); - Assert.True(operation.HasCompleted); - } - - [Test] - public void ConstructOperationForRehydrationWithFailure() - { - HttpPipeline pipeline = CreateMockHttpPipelineWithFailure(); - var operationId = Guid.NewGuid().ToString(); - var rehydrationToken = new RehydrationToken(null, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); - var operation = new RehydrationOperation(pipeline, rehydrationToken); - Assert.NotNull(operation); - Assert.AreEqual(500, operation.GetRawResponse().Status); - Assert.True(operation.HasCompleted); - } - - [Test] - public void ConstructOperationOfTForRehydration() - { - var pipeline = CreateMockHttpPipeline(out var mockJsonModel); - var operationId = Guid.NewGuid().ToString(); - var rehydrationToken = new RehydrationToken(null, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); - var operation = new RehydrationOperation(pipeline, rehydrationToken); - Assert.NotNull(operation); - Assert.AreEqual(operationId, operation.Id); - Assert.AreEqual(200, operation.GetRawResponse().Status); - Assert.True(operation.HasCompleted); - Assert.True(operation.HasValue); - Assert.AreEqual(ModelReaderWriter.Write(mockJsonModel).ToString(), ModelReaderWriter.Write(operation.Value).ToString()); - - operation.UpdateStatus(); - Assert.AreEqual(200, operation.GetRawResponse().Status); - } - - [Test] - public void GetRehydrationToken() - { - var operationId = Guid.NewGuid().ToString(); - var rehydrationToken = new RehydrationToken(operationId, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test/", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); - var operation = new RehydrationOperation(HttpPipelineBuilder.Build(new MockClientOptions()), rehydrationToken); - var token = operation.GetRehydrationToken(); - Assert.AreEqual(ModelReaderWriter.Write(rehydrationToken).ToString(), ModelReaderWriter.Write(token).ToString()); - } - - [Test] - public void GetRehydrationTokenOfT() - { - var operationId = Guid.NewGuid().ToString(); - var rehydrationToken = new RehydrationToken(operationId, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test/", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); - var operation = new RehydrationOperation(HttpPipelineBuilder.Build(new MockClientOptions()), rehydrationToken); - var token = operation.GetRehydrationToken(); - Assert.AreEqual(ModelReaderWriter.Write(rehydrationToken).ToString(), ModelReaderWriter.Write(token).ToString()); - } - - private static HttpPipeline CreateMockHttpPipeline(out MockJsonModel mockJsonModel) - { - var mockResponse = new MockResponse(200); - mockJsonModel = new MockJsonModel(1, "a"); - mockResponse.SetContent(ModelReaderWriter.Write(mockJsonModel).ToString()); - var transport = new MockTransport(mockResponse, mockResponse); - var pipeline = new HttpPipeline(transport, default); - return pipeline; - } - - private static HttpPipeline CreateMockHttpPipelineWithFailure() - { - var mockResponse = new MockResponse(500); - var transport = new MockTransport(mockResponse, mockResponse); - var pipeline = new HttpPipeline(transport, default); - return pipeline; - } - - private class MockClientOptions : ClientOptions - { - } - } -} From 3b040be775bf21be882107a4626b3065f47d4f3a Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Tue, 26 Mar 2024 14:57:22 +0800 Subject: [PATCH 17/32] update --- .../src/Shared/NextLinkOperationImplementation.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs b/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs index 9f707b112a73..6962bdfe4043 100644 --- a/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs +++ b/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs @@ -182,7 +182,7 @@ private static string ParseOperationId(string nextRequestUri) { return uri.Segments.Last(); } - throw new ArgumentException($"{nameof(nextRequestUri)} is not a valid Uri"); + throw new ArgumentException($"\"{nameof(nextRequestUri)}\":{nextRequestUri} is not a valid Uri"); } public RehydrationToken GetRehydrationToken() @@ -417,7 +417,7 @@ private async ValueTask GetResponseAsync(bool async, string uri, Cance // If we are doing final get for a delete LRO with 404, just return empty response with 204 if (message.Response.Status == 404 && RequestMethod == RequestMethod.Delete) { - return new EmptyResponse(HttpStatusCode.NoContent); + return new EmptyResponse(HttpStatusCode.NoContent, message.Response.ClientRequestId); } return message.Response; } @@ -427,18 +427,19 @@ private async ValueTask GetResponseAsync(bool async, string uri, Cance /// private sealed class EmptyResponse : Response { - public EmptyResponse(HttpStatusCode status) + public EmptyResponse(HttpStatusCode status, string clientRequestId) { Status = (int)status; ReasonPhrase = status.ToString(); + ClientRequestId = clientRequestId; } public override int Status { get; } public override string ReasonPhrase { get; } - public override Stream? ContentStream { get => null; set => throw new System.NotImplementedException(); } - public override string ClientRequestId { get => string.Empty; set => throw new System.NotImplementedException(); } + public override Stream? ContentStream { get => null; set => throw new InvalidOperationException("Should not set ContentStream for an empty response."); } + public override string ClientRequestId { get; set; } public override void Dispose() { From e86c766f181cb204569b3a5f3cec2e32e308aa3f Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Tue, 26 Mar 2024 17:51:04 +0800 Subject: [PATCH 18/32] fix for imcomplete nextRequesturi --- .../src/Shared/NextLinkOperationImplementation.cs | 14 +++++++++----- .../tests/NextLinkOperationImplementationTests.cs | 13 +++++++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs b/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs index 6962bdfe4043..4346dde386e4 100644 --- a/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs +++ b/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs @@ -173,14 +173,18 @@ private NextLinkOperationImplementation( _finalStateVia = finalStateVia; _pipeline = pipeline; _apiVersion = apiVersion; - OperationId = ParseOperationId(nextRequestUri); + OperationId = ParseOperationId(startRequestUri, nextRequestUri); } - private static string ParseOperationId(string nextRequestUri) + private static string ParseOperationId(Uri startRequestUri, string nextRequestUri) { - if (Uri.TryCreate(nextRequestUri, UriKind.Absolute, out var uri)) + if (Uri.TryCreate(nextRequestUri, UriKind.Absolute, out var nextLink) && nextLink.Scheme != "file") { - return uri.Segments.Last(); + return nextLink.Segments.Last(); + } + else + { + return new Uri(startRequestUri, nextRequestUri).Segments.Last(); } throw new ArgumentException($"\"{nameof(nextRequestUri)}\":{nextRequestUri} is not a valid Uri"); } @@ -212,7 +216,7 @@ public static RehydrationToken GetRehydrationToken( } var headerSource = GetHeaderSource(requestMethod, startRequestUri, response, apiVersionStr, out var nextRequestUri); response.Headers.TryGetValue("Location", out var lastKnownLocation); - var operationId = ParseOperationId(nextRequestUri); + var operationId = ParseOperationId(startRequestUri, nextRequestUri); return GetRehydrationToken(requestMethod, startRequestUri, nextRequestUri, headerSource.ToString(), lastKnownLocation, finalStateVia.ToString(), operationId); } diff --git a/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs b/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs index a3b1330e4ef3..5299931a5b8b 100644 --- a/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs +++ b/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs @@ -29,6 +29,19 @@ public void ConstructRehydrationToken() Assert.AreEqual(finalStateVia, token.FinalStateVia); } + [Test] + public void CreateWithIncompleteNextRequestUri() + { + var pipeline = CreateMockHttpPipeline(); + var operationId = Guid.NewGuid().ToString(); + var requestMethod = RequestMethod.Delete; + var rehydrationToken = new RehydrationToken(null, null, "None", $"/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test", requestMethod, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); + var operation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(pipeline, rehydrationToken); + Assert.NotNull(operation); + Assert.AreEqual(operationId, operation.OperationId); + Assert.AreEqual(requestMethod, operation.RequestMethod); + } + [Test] public void ConstructNextLinkOperation() { From 6e2eb4a965f3d33fc115de40186cb318bbd81677 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Tue, 26 Mar 2024 18:14:52 +0800 Subject: [PATCH 19/32] remove unneeded test --- .../tests/NextLinkOperationImplementationTests.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs b/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs index 5299931a5b8b..f3f97e8ee4e9 100644 --- a/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs +++ b/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs @@ -67,13 +67,6 @@ public async Task FinalGetForDeleteLRO() Assert.AreEqual(204, result.RawResponse.Status); } - [Test] - public void ThrowWithIvalidNextRequestUri() - { - var rehydrationToken = new RehydrationToken(null, null, "None", $"invalidNextRequestUri", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); - Assert.Throws(() =>NextLinkOperationImplementation.Create(HttpPipelineBuilder.Build(new MockClientOptions()), rehydrationToken)); - } - [Test] public void ThrowWithDefaultRehydrationToken() { From 71e65b854fabf3c71c22fa7b10c9c8db4d8e52c8 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 27 Mar 2024 11:46:22 +0800 Subject: [PATCH 20/32] Address comments --- sdk/core/Azure.Core/src/Operation.cs | 1 + .../Shared/NextLinkOperationImplementation.cs | 21 ++++--------------- .../NextLinkOperationImplementationTests.cs | 8 +++---- 3 files changed, 9 insertions(+), 21 deletions(-) diff --git a/sdk/core/Azure.Core/src/Operation.cs b/sdk/core/Azure.Core/src/Operation.cs index 6e67b68591d2..8ce4a1712e78 100644 --- a/sdk/core/Azure.Core/src/Operation.cs +++ b/sdk/core/Azure.Core/src/Operation.cs @@ -83,6 +83,7 @@ public static async Task RehydrateAsync(HttpPipeline pipeline, Rehydr { Argument.AssertNotNull(pipeline, nameof(pipeline)); Argument.AssertNotNull(rehydrationToken, nameof(rehydrationToken)); + var nextLinkOperation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(pipeline, rehydrationToken); var operationState = await nextLinkOperation.UpdateStateAsync(true, default).ConfigureAwait(false); return new RehydrationOperation(nextLinkOperation, operationState); diff --git a/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs b/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs index 4346dde386e4..7ed62c0f8cf6 100644 --- a/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs +++ b/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs @@ -44,7 +44,7 @@ public static IOperation Create( bool skipApiVersionOverride = false, string? apiVersionOverrideValue = null) { - string? apiVersionStr; + string? apiVersionStr = null; if (apiVersionOverrideValue is not null) { apiVersionStr = apiVersionOverrideValue; @@ -83,9 +83,7 @@ public static IOperation Create( public static IOperation Create( HttpPipeline pipeline, - RehydrationToken rehydrationToken, - bool skipApiVersionOverride = false, - string? apiVersionOverrideValue = null) + RehydrationToken rehydrationToken) { AssertNotNull(rehydrationToken, nameof(rehydrationToken)); AssertNotNull(pipeline, nameof(pipeline)); @@ -95,7 +93,7 @@ public static IOperation Create( var initialUri = GetContentFromRehydrationToken(lroDetails, "initialUri"); if (!Uri.TryCreate(initialUri, UriKind.Absolute, out var startRequestUri)) { - throw new ArgumentException($"\"initialUri\" is invalid Uri from {nameof(rehydrationToken)}"); + throw new ArgumentException($"\"initialUri\" property on \"rehydrationToken\" is an invalid Uri", nameof(rehydrationToken)); } string nextRequestUri = GetContentFromRehydrationToken(lroDetails, "nextRequestUri"); @@ -125,17 +123,7 @@ public static IOperation Create( headerSource = HeaderSource.None; } - string? apiVersionStr; - if (apiVersionOverrideValue is not null) - { - apiVersionStr = apiVersionOverrideValue; - } - else - { - apiVersionStr = !skipApiVersionOverride && TryGetApiVersion(startRequestUri, out ReadOnlySpan apiVersion) ? apiVersion.ToString() : null; - } - - return new NextLinkOperationImplementation(pipeline, requestMethod, startRequestUri, nextRequestUri, headerSource, lastKnownLocation, finalStateVia, apiVersionStr); + return new NextLinkOperationImplementation(pipeline, requestMethod, startRequestUri, nextRequestUri, headerSource, lastKnownLocation, finalStateVia, null); } private static string GetContentFromRehydrationToken(Dictionary lroDetails, string key) @@ -186,7 +174,6 @@ private static string ParseOperationId(Uri startRequestUri, string nextRequestUr { return new Uri(startRequestUri, nextRequestUri).Segments.Last(); } - throw new ArgumentException($"\"{nameof(nextRequestUri)}\":{nextRequestUri} is not a valid Uri"); } public RehydrationToken GetRehydrationToken() diff --git a/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs b/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs index f3f97e8ee4e9..9d9fc94988ce 100644 --- a/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs +++ b/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs @@ -35,7 +35,7 @@ public void CreateWithIncompleteNextRequestUri() var pipeline = CreateMockHttpPipeline(); var operationId = Guid.NewGuid().ToString(); var requestMethod = RequestMethod.Delete; - var rehydrationToken = new RehydrationToken(null, null, "None", $"/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test", requestMethod, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); + var rehydrationToken = new RehydrationToken(null, null, "None", $"/operations/{operationId}?api-version=xxx", "https://test", requestMethod, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); var operation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(pipeline, rehydrationToken); Assert.NotNull(operation); Assert.AreEqual(operationId, operation.OperationId); @@ -47,7 +47,7 @@ public void ConstructNextLinkOperation() { var operationId = Guid.NewGuid().ToString(); var requestMethod = RequestMethod.Delete; - var rehydrationToken = new RehydrationToken(null, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test", requestMethod, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); + var rehydrationToken = new RehydrationToken(null, null, "None", $"https://test.com/operations/{operationId}?api-version=2019-12-01", "https://test", requestMethod, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); var operation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(HttpPipelineBuilder.Build(new MockClientOptions()), rehydrationToken); Assert.NotNull(operation); Assert.AreEqual(operationId, operation.OperationId); @@ -59,7 +59,7 @@ public async Task FinalGetForDeleteLRO() { var operationId = Guid.NewGuid().ToString(); var requestMethod = RequestMethod.Delete; - var rehydrationToken = new RehydrationToken(null, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test", requestMethod, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); + var rehydrationToken = new RehydrationToken(null, null, "None", $"https://test.com/operations/{operationId}?api-version=2019-12-01", "https://test", requestMethod, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); var operation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(CreateMockHttpPipeline(), rehydrationToken); Assert.NotNull(operation); @@ -82,7 +82,7 @@ public void ThrowOnInvalidUri() [Test] public void ThrowOnNextLinkOperationImplementationCreateWithNullHttpPipeline() { - Assert.Throws(() => NextLinkOperationImplementation.Create(null, new RehydrationToken(null, null, "None", "nextRequestUri", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()), default)); + Assert.Throws(() => NextLinkOperationImplementation.Create(null, new RehydrationToken(null, null, "None", "nextRequestUri", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()))); } private static HttpPipeline CreateMockHttpPipeline() From ebadd2ceae2eded98fd8c6d7b3c594be565bc144 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 27 Mar 2024 12:04:16 +0800 Subject: [PATCH 21/32] make OpetaionId nullable --- .../Azure.Core/src/Internal/RehydrationOperation.cs | 2 +- .../src/Internal/RehydrationOperationOfT.cs | 2 +- sdk/core/Azure.Core/src/Operation.cs | 1 + .../src/Shared/NextLinkOperationImplementation.cs | 11 +++++++---- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs b/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs index 6f82a9153945..56e6e67b4190 100644 --- a/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs +++ b/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs @@ -20,7 +20,7 @@ public RehydrationOperation(NextLinkOperationImplementation nextLinkOperation, O : new OperationInternal(_nextLinkOperation, new ClientDiagnostics(options ?? ClientOptions.Default), operationState.RawResponse); } - public override string Id => _nextLinkOperation.OperationId; + public override string Id => _nextLinkOperation.OperationId ?? string.Empty; public override RehydrationToken? GetRehydrationToken() => _nextLinkOperation.GetRehydrationToken(); diff --git a/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs b/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs index 933957f63dd1..01144f3c4562 100644 --- a/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs +++ b/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs @@ -27,7 +27,7 @@ public RehydrationOperation(NextLinkOperationImplementation nextLinkOperation, O public override bool HasValue => _operation.HasValue; - public override string Id => _nextLinkOperation.OperationId; + public override string Id => _nextLinkOperation.OperationId ?? string.Empty; public override RehydrationToken? GetRehydrationToken() => _nextLinkOperation.GetRehydrationToken(); diff --git a/sdk/core/Azure.Core/src/Operation.cs b/sdk/core/Azure.Core/src/Operation.cs index 8ce4a1712e78..02df8f3ccc40 100644 --- a/sdk/core/Azure.Core/src/Operation.cs +++ b/sdk/core/Azure.Core/src/Operation.cs @@ -48,6 +48,7 @@ public static Operation Rehydrate(HttpPipeline pipeline, RehydrationToken rehydr { Argument.AssertNotNull(pipeline, nameof(pipeline)); Argument.AssertNotNull(rehydrationToken, nameof(rehydrationToken)); + var nextLinkOperation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(pipeline, rehydrationToken); var operationState = nextLinkOperation.UpdateStateAsync(false, default).EnsureCompleted(); return new RehydrationOperation(nextLinkOperation, operationState); diff --git a/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs b/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs index 7ed62c0f8cf6..d97e03049b57 100644 --- a/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs +++ b/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs @@ -32,7 +32,8 @@ internal class NextLinkOperationImplementation : IOperation private string? _lastKnownLocation; private string _nextRequestUri; - public string OperationId { get; } + // We can only get OperationId if the operation is in progress when the nextRequestUri contains it + public string? OperationId { get; private set; } public RequestMethod RequestMethod { get; } public static IOperation Create( @@ -164,7 +165,7 @@ private NextLinkOperationImplementation( OperationId = ParseOperationId(startRequestUri, nextRequestUri); } - private static string ParseOperationId(Uri startRequestUri, string nextRequestUri) + private string ParseOperationId(Uri startRequestUri, string nextRequestUri) { if (Uri.TryCreate(nextRequestUri, UriKind.Absolute, out var nextLink) && nextLink.Scheme != "file") { @@ -203,8 +204,7 @@ public static RehydrationToken GetRehydrationToken( } var headerSource = GetHeaderSource(requestMethod, startRequestUri, response, apiVersionStr, out var nextRequestUri); response.Headers.TryGetValue("Location", out var lastKnownLocation); - var operationId = ParseOperationId(startRequestUri, nextRequestUri); - return GetRehydrationToken(requestMethod, startRequestUri, nextRequestUri, headerSource.ToString(), lastKnownLocation, finalStateVia.ToString(), operationId); + return GetRehydrationToken(requestMethod, startRequestUri, nextRequestUri, headerSource.ToString(), lastKnownLocation, finalStateVia.ToString(), null); } public static RehydrationToken GetRehydrationToken( @@ -269,12 +269,15 @@ private void UpdateNextRequestUri(ResponseHeaders headers) { case HeaderSource.OperationLocation when headers.TryGetValue("Operation-Location", out string? operationLocation): _nextRequestUri = AppendOrReplaceApiVersion(operationLocation, _apiVersion); + OperationId = ParseOperationId(_startRequestUri, _nextRequestUri); return; case HeaderSource.AzureAsyncOperation when headers.TryGetValue("Azure-AsyncOperation", out string? azureAsyncOperation): _nextRequestUri = AppendOrReplaceApiVersion(azureAsyncOperation, _apiVersion); + OperationId = ParseOperationId(_startRequestUri, _nextRequestUri); return; case HeaderSource.Location when hasLocation: _nextRequestUri = AppendOrReplaceApiVersion(location!, _apiVersion); + OperationId = ParseOperationId(_startRequestUri, _nextRequestUri); return; } } From 812b8eabee8f12bef0e4c23b72f63dfea5bec7e8 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 27 Mar 2024 15:24:07 +0800 Subject: [PATCH 22/32] update tests --- .../Shared/NextLinkOperationImplementation.cs | 7 +- .../NextLinkOperationImplementationTests.cs | 19 ++--- sdk/core/Azure.Core/tests/OperationTests.cs | 70 +++++++++---------- 3 files changed, 50 insertions(+), 46 deletions(-) diff --git a/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs b/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs index d97e03049b57..a106dfc51c0f 100644 --- a/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs +++ b/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs @@ -124,7 +124,7 @@ public static IOperation Create( headerSource = HeaderSource.None; } - return new NextLinkOperationImplementation(pipeline, requestMethod, startRequestUri, nextRequestUri, headerSource, lastKnownLocation, finalStateVia, null); + return new NextLinkOperationImplementation(pipeline, requestMethod, startRequestUri, nextRequestUri, headerSource, lastKnownLocation, finalStateVia, null, rehydrationToken.Id); } private static string GetContentFromRehydrationToken(Dictionary lroDetails, string key) @@ -145,7 +145,8 @@ private NextLinkOperationImplementation( HeaderSource headerSource, string? lastKnownLocation, OperationFinalStateVia finalStateVia, - string? apiVersion) + string? apiVersion, + string? operationId = null) { AssertNotNull(pipeline, nameof(pipeline)); AssertNotNull(requestMethod, nameof(requestMethod)); @@ -162,7 +163,7 @@ private NextLinkOperationImplementation( _finalStateVia = finalStateVia; _pipeline = pipeline; _apiVersion = apiVersion; - OperationId = ParseOperationId(startRequestUri, nextRequestUri); + OperationId = operationId; } private string ParseOperationId(Uri startRequestUri, string nextRequestUri) diff --git a/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs b/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs index 9d9fc94988ce..0017a1b32218 100644 --- a/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs +++ b/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Net; using System.Threading.Tasks; using Azure.Core.Pipeline; using Azure.Core.TestFramework; @@ -32,13 +33,15 @@ public void ConstructRehydrationToken() [Test] public void CreateWithIncompleteNextRequestUri() { - var pipeline = CreateMockHttpPipeline(); + var pipeline = CreateMockHttpPipeline(HttpStatusCode.Accepted); + var resourceId = Guid.NewGuid().ToString(); var operationId = Guid.NewGuid().ToString(); - var requestMethod = RequestMethod.Delete; - var rehydrationToken = new RehydrationToken(null, null, "None", $"/operations/{operationId}?api-version=xxx", "https://test", requestMethod, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); + var requestMethod = RequestMethod.Put; + var lastKnowLocation = $"/operations/{operationId}?api-version=xxx"; + var rehydrationToken = new RehydrationToken(null, null, "None", $"/resource/{resourceId}?api-version=xxx", "https://test", requestMethod, lastKnowLocation, OperationFinalStateVia.AzureAsyncOperation.ToString()); var operation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(pipeline, rehydrationToken); Assert.NotNull(operation); - Assert.AreEqual(operationId, operation.OperationId); + Assert.Null(operation.OperationId); Assert.AreEqual(requestMethod, operation.RequestMethod); } @@ -50,7 +53,7 @@ public void ConstructNextLinkOperation() var rehydrationToken = new RehydrationToken(null, null, "None", $"https://test.com/operations/{operationId}?api-version=2019-12-01", "https://test", requestMethod, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); var operation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(HttpPipelineBuilder.Build(new MockClientOptions()), rehydrationToken); Assert.NotNull(operation); - Assert.AreEqual(operationId, operation.OperationId); + Assert.Null(operation.OperationId); Assert.AreEqual(requestMethod, operation.RequestMethod); } @@ -60,7 +63,7 @@ public async Task FinalGetForDeleteLRO() var operationId = Guid.NewGuid().ToString(); var requestMethod = RequestMethod.Delete; var rehydrationToken = new RehydrationToken(null, null, "None", $"https://test.com/operations/{operationId}?api-version=2019-12-01", "https://test", requestMethod, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); - var operation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(CreateMockHttpPipeline(), rehydrationToken); + var operation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(CreateMockHttpPipeline(HttpStatusCode.NotFound), rehydrationToken); Assert.NotNull(operation); var result = await operation.UpdateStateAsync(true, default); @@ -85,9 +88,9 @@ public void ThrowOnNextLinkOperationImplementationCreateWithNullHttpPipeline() Assert.Throws(() => NextLinkOperationImplementation.Create(null, new RehydrationToken(null, null, "None", "nextRequestUri", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()))); } - private static HttpPipeline CreateMockHttpPipeline() + private static HttpPipeline CreateMockHttpPipeline(HttpStatusCode statusCode) { - var mockResponse = new MockResponse(404); + var mockResponse = new MockResponse((int)statusCode); var transport = new MockTransport(mockResponse, mockResponse); var pipeline = new HttpPipeline(transport, default); return pipeline; diff --git a/sdk/core/Azure.Core/tests/OperationTests.cs b/sdk/core/Azure.Core/tests/OperationTests.cs index 3d1e2de41529..6bbc3dd285a0 100644 --- a/sdk/core/Azure.Core/tests/OperationTests.cs +++ b/sdk/core/Azure.Core/tests/OperationTests.cs @@ -3,6 +3,7 @@ using System; using System.ClientModel.Primitives; +using System.Net; using System.Threading; using System.Threading.Tasks; using Azure.Core.Pipeline; @@ -149,65 +150,62 @@ public void ThrowOnNullArgumentWhileRehydrate() [Test] public void Rehydrate() { - var pipeline = CreateMockHttpPipeline(out _); + var pipeline = CreateMockHttpPipeline(HttpStatusCode.Accepted, out _); var operationId = Guid.NewGuid().ToString(); - var rehydrationToken = new RehydrationToken(null, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); + var rehydrationToken = new RehydrationToken(operationId, null, "Location", "test", "https://test", RequestMethod.Put, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); var operation = Operation.Rehydrate(pipeline, rehydrationToken); Assert.NotNull(operation); Assert.AreEqual(operationId, operation.Id); - Assert.True(operation.HasCompleted); - Assert.AreEqual(200, operation.GetRawResponse().Status); + Assert.False(operation.HasCompleted); + Assert.AreEqual((int)HttpStatusCode.Accepted,operation.GetRawResponse().Status); } [Test] public async Task RehydrateAsync() { - var pipeline = CreateMockHttpPipeline(out _); + var pipeline = CreateMockHttpPipeline(HttpStatusCode.Accepted, out _); var operationId = Guid.NewGuid().ToString(); - var rehydrationToken = new RehydrationToken(null, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); + var rehydrationToken = new RehydrationToken(operationId, null, "Location", "test", "https://test", RequestMethod.Put, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); var operation = await Operation.RehydrateAsync(pipeline, rehydrationToken); Assert.NotNull(operation); Assert.AreEqual(operationId, operation.Id); - Assert.True(operation.HasCompleted); - Assert.AreEqual(200, operation.GetRawResponse().Status); + Assert.False(operation.HasCompleted); + Assert.AreEqual((int)HttpStatusCode.Accepted, operation.GetRawResponse().Status); } [Test] public void RehydrateOfT() { - var pipeline = CreateMockHttpPipeline(out var mockJsonModel); + var pipeline = CreateMockHttpPipeline(HttpStatusCode.OK, out var mockJsonModel); var operationId = Guid.NewGuid().ToString(); - var rehydrationToken = new RehydrationToken(null, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); + var rehydrationToken = new RehydrationToken(operationId, null, "None", "test", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); var operation = Operation.Rehydrate(pipeline, rehydrationToken); Assert.NotNull(operation); Assert.AreEqual(operationId, operation.Id); Assert.True(operation.HasCompleted); Assert.True(operation.HasValue); - Assert.AreEqual(200, operation.GetRawResponse().Status); + Assert.AreEqual((int)HttpStatusCode.OK, operation.GetRawResponse().Status); Assert.AreEqual(ModelReaderWriter.Write(mockJsonModel).ToString(), ModelReaderWriter.Write(operation.Value).ToString()); } [Test] public async Task RehydrateAsyncOfT() { - var pipeline = CreateMockHttpPipeline(out var mockJsonModel); - var operationId = Guid.NewGuid().ToString(); - var rehydrationToken = new RehydrationToken(null, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); + var pipeline = CreateMockHttpPipeline(HttpStatusCode.OK, out var mockJsonModel); + var rehydrationToken = new RehydrationToken(null, null, "None", "test", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); var operation = await Operation.RehydrateAsync(pipeline, rehydrationToken); Assert.NotNull(operation); - Assert.AreEqual(operationId, operation.Id); Assert.True(operation.HasCompleted); Assert.True(operation.HasValue); - Assert.AreEqual(200, operation.GetRawResponse().Status); + Assert.AreEqual((int)HttpStatusCode.OK, operation.GetRawResponse().Status); Assert.AreEqual(ModelReaderWriter.Write(mockJsonModel).ToString(), ModelReaderWriter.Write(operation.Value).ToString()); } [Test] public async Task ConstructOperationForRehydrationWithFailure() { - HttpPipeline pipeline = CreateMockHttpPipelineWithFailure(); - var operationId = Guid.NewGuid().ToString(); - var rehydrationToken = new RehydrationToken(null, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); + HttpPipeline pipeline = CreateMockHttpPipeline(HttpStatusCode.InternalServerError, out _); + var rehydrationToken = new RehydrationToken(null, null, "None", "test", "https://test", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); var operation = await Operation.RehydrateAsync(pipeline, rehydrationToken); Assert.NotNull(operation); Assert.AreEqual(500, operation.GetRawResponse().Status); @@ -217,9 +215,9 @@ public async Task ConstructOperationForRehydrationWithFailure() [Test] public async Task GetRehydrationTokenAsync() { - var operationId = Guid.NewGuid().ToString(); - var rehydrationToken = new RehydrationToken(operationId, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test/", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); - var operation = await Operation.RehydrateAsync(HttpPipelineBuilder.Build(new MockClientOptions()), rehydrationToken); + var pipeline = CreateMockHttpPipeline(HttpStatusCode.OK, out _); + var rehydrationToken = new RehydrationToken(null, null, "None", "test", "https://test/", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); + var operation = await Operation.RehydrateAsync(pipeline, rehydrationToken); var token = operation.GetRehydrationToken(); Assert.AreEqual(ModelReaderWriter.Write(rehydrationToken).ToString(), ModelReaderWriter.Write(token).ToString()); } @@ -227,16 +225,26 @@ public async Task GetRehydrationTokenAsync() [Test] public async Task GetRehydrationTokenOfTAsync() { - var operationId = Guid.NewGuid().ToString(); - var rehydrationToken = new RehydrationToken(operationId, null, "None", $"https://management.azure.com/subscriptions/subscription-id/providers/Microsoft.Compute/locations/region/operations/{operationId}?api-version=2019-12-01", "https://test/", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); - var operation = await Operation.RehydrateAsync(HttpPipelineBuilder.Build(new MockClientOptions()), rehydrationToken); + var pipeline = CreateMockHttpPipeline(HttpStatusCode.OK, out _); + var rehydrationToken = new RehydrationToken(null, null, "None", "test", "https://test/", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); + var operation = await Operation.RehydrateAsync(pipeline, rehydrationToken); var token = operation.GetRehydrationToken(); Assert.AreEqual(ModelReaderWriter.Write(rehydrationToken).ToString(), ModelReaderWriter.Write(token).ToString()); } - private static HttpPipeline CreateMockHttpPipeline(out MockJsonModel mockJsonModel) + //private static HttpPipeline CreateMockHttpPipeline(out MockJsonModel mockJsonModel) + //{ + // var mockResponse = new MockResponse(202); + // mockJsonModel = new MockJsonModel(1, "a"); + // mockResponse.SetContent(ModelReaderWriter.Write(mockJsonModel).ToString()); + // var transport = new MockTransport(mockResponse); + // var pipeline = new HttpPipeline(transport, default); + // return pipeline; + //} + + private static HttpPipeline CreateMockHttpPipeline(HttpStatusCode statusCode, out MockJsonModel mockJsonModel) { - var mockResponse = new MockResponse(200); + var mockResponse = new MockResponse((int)statusCode); mockJsonModel = new MockJsonModel(1, "a"); mockResponse.SetContent(ModelReaderWriter.Write(mockJsonModel).ToString()); var transport = new MockTransport(mockResponse); @@ -244,14 +252,6 @@ private static HttpPipeline CreateMockHttpPipeline(out MockJsonModel mockJsonMod return pipeline; } - private static HttpPipeline CreateMockHttpPipelineWithFailure() - { - var mockResponse = new MockResponse(500); - var transport = new MockTransport(mockResponse, mockResponse); - var pipeline = new HttpPipeline(transport, default); - return pipeline; - } - private class MockClientOptions : ClientOptions { } From b4668a841c8cc03f132e79a4a23d4d7357811fed Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 27 Mar 2024 15:41:25 +0800 Subject: [PATCH 23/32] Add comments --- .../Azure.Core/src/Shared/NextLinkOperationImplementation.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs b/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs index a106dfc51c0f..e41f187a2545 100644 --- a/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs +++ b/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs @@ -32,7 +32,9 @@ internal class NextLinkOperationImplementation : IOperation private string? _lastKnownLocation; private string _nextRequestUri; - // We can only get OperationId if the operation is in progress when the nextRequestUri contains it + // We can only get OperationId when + // - The operation is still in progress and nextRequestUri contains it + // - During rehydration, rehydrationToken.Id is the operation id public string? OperationId { get; private set; } public RequestMethod RequestMethod { get; } @@ -89,6 +91,7 @@ public static IOperation Create( AssertNotNull(rehydrationToken, nameof(rehydrationToken)); AssertNotNull(pipeline, nameof(pipeline)); + // TODO: Once we remove NextLinkOperationImplementation from internal shared and make it internal to Azure.Core only, we can access the internal members from RehydrationToken directly var lroDetails = ModelReaderWriter.Write(rehydrationToken!, ModelReaderWriterOptions.Json).ToObjectFromJson>(); var initialUri = GetContentFromRehydrationToken(lroDetails, "initialUri"); From cac95fc1c0179063b8ea9c50bd7b683c6027f92b Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 27 Mar 2024 15:53:10 +0800 Subject: [PATCH 24/32] update --- .../src/Shared/NextLinkOperationImplementation.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs b/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs index e41f187a2545..6f7b44e213cf 100644 --- a/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs +++ b/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs @@ -61,7 +61,12 @@ public static IOperation Create( { return new CompletedOperation(failureState ?? GetOperationStateFromFinalResponse(requestMethod, response)); } - response.Headers.TryGetValue("Location", out var lastKnownLocation); + + string? lastKnownLocation; + if (!response.Headers.TryGetValue("Location", out lastKnownLocation)) + { + lastKnownLocation = null; + } return new NextLinkOperationImplementation(pipeline, requestMethod, startRequestUri, nextRequestUri, headerSource, lastKnownLocation, finalStateVia, apiVersionStr); } @@ -206,8 +211,13 @@ public static RehydrationToken GetRehydrationToken( { apiVersionStr = !skipApiVersionOverride && TryGetApiVersion(startRequestUri, out ReadOnlySpan apiVersion) ? apiVersion.ToString() : null; } + var headerSource = GetHeaderSource(requestMethod, startRequestUri, response, apiVersionStr, out var nextRequestUri); - response.Headers.TryGetValue("Location", out var lastKnownLocation); + string? lastKnownLocation; + if (!response.Headers.TryGetValue("Location", out lastKnownLocation)) + { + lastKnownLocation = null; + } return GetRehydrationToken(requestMethod, startRequestUri, nextRequestUri, headerSource.ToString(), lastKnownLocation, finalStateVia.ToString(), null); } From f9f0ca81d2733b193c84fd9a8084bf84dc4b0289 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 27 Mar 2024 16:42:07 +0800 Subject: [PATCH 25/32] cleanup --- sdk/core/Azure.Core/tests/OperationTests.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/sdk/core/Azure.Core/tests/OperationTests.cs b/sdk/core/Azure.Core/tests/OperationTests.cs index 6bbc3dd285a0..58d85653a8d7 100644 --- a/sdk/core/Azure.Core/tests/OperationTests.cs +++ b/sdk/core/Azure.Core/tests/OperationTests.cs @@ -232,16 +232,6 @@ public async Task GetRehydrationTokenOfTAsync() Assert.AreEqual(ModelReaderWriter.Write(rehydrationToken).ToString(), ModelReaderWriter.Write(token).ToString()); } - //private static HttpPipeline CreateMockHttpPipeline(out MockJsonModel mockJsonModel) - //{ - // var mockResponse = new MockResponse(202); - // mockJsonModel = new MockJsonModel(1, "a"); - // mockResponse.SetContent(ModelReaderWriter.Write(mockJsonModel).ToString()); - // var transport = new MockTransport(mockResponse); - // var pipeline = new HttpPipeline(transport, default); - // return pipeline; - //} - private static HttpPipeline CreateMockHttpPipeline(HttpStatusCode statusCode, out MockJsonModel mockJsonModel) { var mockResponse = new MockResponse((int)statusCode); From 69a4c381e8c2c6bf21af746d1666c21a91938a59 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Fri, 29 Mar 2024 10:11:26 +0800 Subject: [PATCH 26/32] add parameter name for async while calling UpdateStateAsync --- sdk/core/Azure.Core/src/Operation.cs | 8 ++++---- .../tests/NextLinkOperationImplementationTests.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sdk/core/Azure.Core/src/Operation.cs b/sdk/core/Azure.Core/src/Operation.cs index 02df8f3ccc40..f79ecf8304aa 100644 --- a/sdk/core/Azure.Core/src/Operation.cs +++ b/sdk/core/Azure.Core/src/Operation.cs @@ -33,7 +33,7 @@ public static Operation Rehydrate(HttpPipeline pipeline, RehydrationToken IOperationSource source = new GenericOperationSource(); var nextLinkOperation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(pipeline, rehydrationToken); var operation = NextLinkOperationImplementation.Create(source, nextLinkOperation); - var operationState = operation.UpdateStateAsync(false, default).EnsureCompleted(); + var operationState = operation.UpdateStateAsync(async: false, default).EnsureCompleted(); return new RehydrationOperation(nextLinkOperation, operationState, operation, options); } @@ -50,7 +50,7 @@ public static Operation Rehydrate(HttpPipeline pipeline, RehydrationToken rehydr Argument.AssertNotNull(rehydrationToken, nameof(rehydrationToken)); var nextLinkOperation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(pipeline, rehydrationToken); - var operationState = nextLinkOperation.UpdateStateAsync(false, default).EnsureCompleted(); + var operationState = nextLinkOperation.UpdateStateAsync(async: false, default).EnsureCompleted(); return new RehydrationOperation(nextLinkOperation, operationState); } @@ -69,7 +69,7 @@ public static async Task> RehydrateAsync(HttpPipeline pipeline, IOperationSource source = new GenericOperationSource(); var nextLinkOperation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(pipeline, rehydrationToken); var operation = NextLinkOperationImplementation.Create(source, nextLinkOperation); - var operationState = await operation.UpdateStateAsync(true, default).ConfigureAwait(false); + var operationState = await operation.UpdateStateAsync(async: true, default).ConfigureAwait(false); return new RehydrationOperation(nextLinkOperation, operationState, operation, options); } @@ -86,7 +86,7 @@ public static async Task RehydrateAsync(HttpPipeline pipeline, Rehydr Argument.AssertNotNull(rehydrationToken, nameof(rehydrationToken)); var nextLinkOperation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(pipeline, rehydrationToken); - var operationState = await nextLinkOperation.UpdateStateAsync(true, default).ConfigureAwait(false); + var operationState = await nextLinkOperation.UpdateStateAsync(async: true, default).ConfigureAwait(false); return new RehydrationOperation(nextLinkOperation, operationState); } diff --git a/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs b/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs index 0017a1b32218..10b8c722f3aa 100644 --- a/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs +++ b/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs @@ -66,7 +66,7 @@ public async Task FinalGetForDeleteLRO() var operation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(CreateMockHttpPipeline(HttpStatusCode.NotFound), rehydrationToken); Assert.NotNull(operation); - var result = await operation.UpdateStateAsync(true, default); + var result = await operation.UpdateStateAsync(async: true, default); Assert.AreEqual(204, result.RawResponse.Status); } From 0887d04c3e89a938a5eb3657f8025390dcb2481e Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Fri, 29 Mar 2024 11:46:29 +0800 Subject: [PATCH 27/32] remove apiversion for rehydration --- .../Shared/NextLinkOperationImplementation.cs | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs b/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs index b4cc74d756f7..55d7fc504e30 100644 --- a/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs +++ b/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs @@ -193,26 +193,14 @@ public static RehydrationToken GetRehydrationToken( RequestMethod requestMethod, Uri startRequestUri, Response response, - OperationFinalStateVia finalStateVia, - bool skipApiVersionOverride = false, - string? apiVersionOverrideValue = null) + OperationFinalStateVia finalStateVia) { AssertNotNull(requestMethod, nameof(requestMethod)); AssertNotNull(startRequestUri, nameof(startRequestUri)); AssertNotNull(response, nameof(response)); AssertNotNull(finalStateVia, nameof(finalStateVia)); - string? apiVersionStr; - if (apiVersionOverrideValue is not null) - { - apiVersionStr = apiVersionOverrideValue; - } - else - { - apiVersionStr = !skipApiVersionOverride && TryGetApiVersion(startRequestUri, out ReadOnlySpan apiVersion) ? apiVersion.ToString() : null; - } - - var headerSource = GetHeaderSource(requestMethod, startRequestUri, response, apiVersionStr, out var nextRequestUri); + var headerSource = GetHeaderSource(requestMethod, startRequestUri, response, null, out var nextRequestUri); string? lastKnownLocation; if (!response.Headers.TryGetValue("Location", out lastKnownLocation)) { From 9a06d859f76538a4468551f764fa2ec692990535 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Sun, 7 Apr 2024 11:40:56 +0800 Subject: [PATCH 28/32] Make operation id non-nullable and return NOT_SET instead of null --- sdk/core/Azure.Core/src/Operation.cs | 1 + sdk/core/Azure.Core/src/RehydrationToken.Serialization.cs | 6 ++++-- sdk/core/Azure.Core/src/RehydrationToken.cs | 5 +++-- .../src/Shared/NextLinkOperationImplementation.cs | 8 ++++++-- .../tests/NextLinkOperationImplementationTests.cs | 4 ++-- sdk/core/Azure.Core/tests/OperationTests.cs | 4 ++-- 6 files changed, 18 insertions(+), 10 deletions(-) diff --git a/sdk/core/Azure.Core/src/Operation.cs b/sdk/core/Azure.Core/src/Operation.cs index f79ecf8304aa..60208de3713b 100644 --- a/sdk/core/Azure.Core/src/Operation.cs +++ b/sdk/core/Azure.Core/src/Operation.cs @@ -98,6 +98,7 @@ public static async Task RehydrateAsync(HttpPipeline pipeline, Rehydr /// /// Gets an ID representing the operation that can be used to poll for /// the status of the long-running operation. + /// There are cases that operation id is not available, we return "NOT_SET" to not break the existing user experience. /// public abstract string Id { get; } diff --git a/sdk/core/Azure.Core/src/RehydrationToken.Serialization.cs b/sdk/core/Azure.Core/src/RehydrationToken.Serialization.cs index 5553a190bac1..50e16fca3bde 100644 --- a/sdk/core/Azure.Core/src/RehydrationToken.Serialization.cs +++ b/sdk/core/Azure.Core/src/RehydrationToken.Serialization.cs @@ -23,7 +23,7 @@ internal RehydrationToken DeserializeRehydrationToken(JsonElement element, Model throw new InvalidOperationException("Cannot deserialize a null value to a non-nullable RehydrationToken"); } - string? id = null; + string id = NextLinkOperationImplementation.NotSet; string version = string.Empty; string headerSource = string.Empty; string nextRequestUri = string.Empty; @@ -40,7 +40,9 @@ internal RehydrationToken DeserializeRehydrationToken(JsonElement element, Model { continue; } - id = property.Value.GetString(); + var idString = property.Value.GetString(); + Debug.Assert(idString is not null); + id = idString!; } if (property.NameEquals("version"u8)) { diff --git a/sdk/core/Azure.Core/src/RehydrationToken.cs b/sdk/core/Azure.Core/src/RehydrationToken.cs index 1b321fc1038a..2c4637c66ae3 100644 --- a/sdk/core/Azure.Core/src/RehydrationToken.cs +++ b/sdk/core/Azure.Core/src/RehydrationToken.cs @@ -11,8 +11,9 @@ public readonly partial struct RehydrationToken /// /// Gets an ID representing the operation that can be used to poll for /// the status of the long-running operation. + /// There are cases that operation id is not available, we return "NOT_SET" to not break the existing user experience. /// - public string? Id { get; } + public string Id { get; } = NextLinkOperationImplementation.NotSet; // Version for this contract itself since we might change the members in the future. internal string Version { get; } = NextLinkOperationImplementation.RehydrationTokenVersion; @@ -36,7 +37,7 @@ public readonly partial struct RehydrationToken // The final state of the operation, could be azure-async-operation, location, original-uri or operation-location. internal string FinalStateVia { get; } - internal RehydrationToken(string? id, string? version, string headerSource, string nextRequestUri, string initialUri, RequestMethod requestMethod, string? lastKnownLocation, string finalStateVia) + internal RehydrationToken(string id, string? version, string headerSource, string nextRequestUri, string initialUri, RequestMethod requestMethod, string? lastKnownLocation, string finalStateVia) { Id = id; if (version is not null) diff --git a/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs b/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs index 55d7fc504e30..c27bd3da48d7 100644 --- a/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs +++ b/sdk/core/Azure.Core/src/Shared/NextLinkOperationImplementation.cs @@ -18,6 +18,7 @@ namespace Azure.Core { internal class NextLinkOperationImplementation : IOperation { + internal const string NotSet = "NOT_SET"; internal const string RehydrationTokenVersion = "1.0.0"; private const string ApiVersionParam = "api-version"; private static readonly string[] FailureStates = { "failed", "canceled" }; @@ -35,7 +36,7 @@ internal class NextLinkOperationImplementation : IOperation // We can only get OperationId when // - The operation is still in progress and nextRequestUri contains it // - During rehydration, rehydrationToken.Id is the operation id - public string? OperationId { get; private set; } + public string OperationId { get; private set; } = NotSet; public RequestMethod RequestMethod { get; } public static IOperation Create( @@ -171,7 +172,10 @@ private NextLinkOperationImplementation( _finalStateVia = finalStateVia; _pipeline = pipeline; _apiVersion = apiVersion; - OperationId = operationId; + if (operationId is not null) + { + OperationId = operationId; + } } private string ParseOperationId(Uri startRequestUri, string nextRequestUri) diff --git a/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs b/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs index 10b8c722f3aa..f16aa76bd611 100644 --- a/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs +++ b/sdk/core/Azure.Core/tests/NextLinkOperationImplementationTests.cs @@ -41,7 +41,7 @@ public void CreateWithIncompleteNextRequestUri() var rehydrationToken = new RehydrationToken(null, null, "None", $"/resource/{resourceId}?api-version=xxx", "https://test", requestMethod, lastKnowLocation, OperationFinalStateVia.AzureAsyncOperation.ToString()); var operation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(pipeline, rehydrationToken); Assert.NotNull(operation); - Assert.Null(operation.OperationId); + Assert.AreEqual(NextLinkOperationImplementation.NotSet, operation.OperationId); Assert.AreEqual(requestMethod, operation.RequestMethod); } @@ -53,7 +53,7 @@ public void ConstructNextLinkOperation() var rehydrationToken = new RehydrationToken(null, null, "None", $"https://test.com/operations/{operationId}?api-version=2019-12-01", "https://test", requestMethod, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); var operation = (NextLinkOperationImplementation)NextLinkOperationImplementation.Create(HttpPipelineBuilder.Build(new MockClientOptions()), rehydrationToken); Assert.NotNull(operation); - Assert.Null(operation.OperationId); + Assert.AreEqual(NextLinkOperationImplementation.NotSet, operation.OperationId); Assert.AreEqual(requestMethod, operation.RequestMethod); } diff --git a/sdk/core/Azure.Core/tests/OperationTests.cs b/sdk/core/Azure.Core/tests/OperationTests.cs index 58d85653a8d7..a94f75e75de7 100644 --- a/sdk/core/Azure.Core/tests/OperationTests.cs +++ b/sdk/core/Azure.Core/tests/OperationTests.cs @@ -216,7 +216,7 @@ public async Task ConstructOperationForRehydrationWithFailure() public async Task GetRehydrationTokenAsync() { var pipeline = CreateMockHttpPipeline(HttpStatusCode.OK, out _); - var rehydrationToken = new RehydrationToken(null, null, "None", "test", "https://test/", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); + var rehydrationToken = new RehydrationToken(NextLinkOperationImplementation.NotSet, null, "None", "test", "https://test/", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); var operation = await Operation.RehydrateAsync(pipeline, rehydrationToken); var token = operation.GetRehydrationToken(); Assert.AreEqual(ModelReaderWriter.Write(rehydrationToken).ToString(), ModelReaderWriter.Write(token).ToString()); @@ -226,7 +226,7 @@ public async Task GetRehydrationTokenAsync() public async Task GetRehydrationTokenOfTAsync() { var pipeline = CreateMockHttpPipeline(HttpStatusCode.OK, out _); - var rehydrationToken = new RehydrationToken(null, null, "None", "test", "https://test/", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); + var rehydrationToken = new RehydrationToken(NextLinkOperationImplementation.NotSet, null, "None", "test", "https://test/", RequestMethod.Delete, null, OperationFinalStateVia.AzureAsyncOperation.ToString()); var operation = await Operation.RehydrateAsync(pipeline, rehydrationToken); var token = operation.GetRehydrationToken(); Assert.AreEqual(ModelReaderWriter.Write(rehydrationToken).ToString(), ModelReaderWriter.Write(token).ToString()); From f19fe64e12d35bc34bf90f2adcb3fd23cf4f48b7 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Sun, 7 Apr 2024 11:55:33 +0800 Subject: [PATCH 29/32] export API --- sdk/core/Azure.Core/api/Azure.Core.net461.cs | 2 +- sdk/core/Azure.Core/api/Azure.Core.net472.cs | 2 +- sdk/core/Azure.Core/api/Azure.Core.net6.0.cs | 2 +- sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sdk/core/Azure.Core/api/Azure.Core.net461.cs b/sdk/core/Azure.Core/api/Azure.Core.net461.cs index 5a68b08cc363..5b1df2f22f6d 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net461.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net461.cs @@ -534,7 +534,7 @@ public static partial class MultipartResponse { private readonly object _dummy; private readonly int _dummyPrimitive; - public string? Id { get { throw null; } } + public string Id { get { throw null; } } Azure.Core.RehydrationToken System.ClientModel.Primitives.IJsonModel.Create(ref System.Text.Json.Utf8JsonReader reader, System.ClientModel.Primitives.ModelReaderWriterOptions options) { throw null; } void System.ClientModel.Primitives.IJsonModel.Write(System.Text.Json.Utf8JsonWriter writer, System.ClientModel.Primitives.ModelReaderWriterOptions options) { } object System.ClientModel.Primitives.IJsonModel.Create(ref System.Text.Json.Utf8JsonReader reader, System.ClientModel.Primitives.ModelReaderWriterOptions options) { 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 5a68b08cc363..5b1df2f22f6d 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net472.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net472.cs @@ -534,7 +534,7 @@ public static partial class MultipartResponse { private readonly object _dummy; private readonly int _dummyPrimitive; - public string? Id { get { throw null; } } + public string Id { get { throw null; } } Azure.Core.RehydrationToken System.ClientModel.Primitives.IJsonModel.Create(ref System.Text.Json.Utf8JsonReader reader, System.ClientModel.Primitives.ModelReaderWriterOptions options) { throw null; } void System.ClientModel.Primitives.IJsonModel.Write(System.Text.Json.Utf8JsonWriter writer, System.ClientModel.Primitives.ModelReaderWriterOptions options) { } object System.ClientModel.Primitives.IJsonModel.Create(ref System.Text.Json.Utf8JsonReader reader, System.ClientModel.Primitives.ModelReaderWriterOptions options) { 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 68561668b131..0e573c0ab8ee 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net6.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net6.0.cs @@ -534,7 +534,7 @@ public static partial class MultipartResponse { private readonly object _dummy; private readonly int _dummyPrimitive; - public string? Id { get { throw null; } } + public string Id { get { throw null; } } Azure.Core.RehydrationToken System.ClientModel.Primitives.IJsonModel.Create(ref System.Text.Json.Utf8JsonReader reader, System.ClientModel.Primitives.ModelReaderWriterOptions options) { throw null; } void System.ClientModel.Primitives.IJsonModel.Write(System.Text.Json.Utf8JsonWriter writer, System.ClientModel.Primitives.ModelReaderWriterOptions options) { } object System.ClientModel.Primitives.IJsonModel.Create(ref System.Text.Json.Utf8JsonReader reader, System.ClientModel.Primitives.ModelReaderWriterOptions options) { 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 5a68b08cc363..5b1df2f22f6d 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs @@ -534,7 +534,7 @@ public static partial class MultipartResponse { private readonly object _dummy; private readonly int _dummyPrimitive; - public string? Id { get { throw null; } } + public string Id { get { throw null; } } Azure.Core.RehydrationToken System.ClientModel.Primitives.IJsonModel.Create(ref System.Text.Json.Utf8JsonReader reader, System.ClientModel.Primitives.ModelReaderWriterOptions options) { throw null; } void System.ClientModel.Primitives.IJsonModel.Write(System.Text.Json.Utf8JsonWriter writer, System.ClientModel.Primitives.ModelReaderWriterOptions options) { } object System.ClientModel.Primitives.IJsonModel.Create(ref System.Text.Json.Utf8JsonReader reader, System.ClientModel.Primitives.ModelReaderWriterOptions options) { throw null; } From 1745223ebe39c6690762017635bca555254d4a8f Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Sun, 7 Apr 2024 13:39:49 +0800 Subject: [PATCH 30/32] cleanup --- sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs | 2 +- sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs b/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs index 56e6e67b4190..6f82a9153945 100644 --- a/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs +++ b/sdk/core/Azure.Core/src/Internal/RehydrationOperation.cs @@ -20,7 +20,7 @@ public RehydrationOperation(NextLinkOperationImplementation nextLinkOperation, O : new OperationInternal(_nextLinkOperation, new ClientDiagnostics(options ?? ClientOptions.Default), operationState.RawResponse); } - public override string Id => _nextLinkOperation.OperationId ?? string.Empty; + public override string Id => _nextLinkOperation.OperationId; public override RehydrationToken? GetRehydrationToken() => _nextLinkOperation.GetRehydrationToken(); diff --git a/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs b/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs index 01144f3c4562..933957f63dd1 100644 --- a/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs +++ b/sdk/core/Azure.Core/src/Internal/RehydrationOperationOfT.cs @@ -27,7 +27,7 @@ public RehydrationOperation(NextLinkOperationImplementation nextLinkOperation, O public override bool HasValue => _operation.HasValue; - public override string Id => _nextLinkOperation.OperationId ?? string.Empty; + public override string Id => _nextLinkOperation.OperationId; public override RehydrationToken? GetRehydrationToken() => _nextLinkOperation.GetRehydrationToken(); From 5545d541d0cbd40840f29ad18823a0b67aa3309c Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Mon, 8 Apr 2024 11:23:28 +0800 Subject: [PATCH 31/32] update description --- sdk/core/Azure.Core/src/Operation.cs | 2 +- sdk/core/Azure.Core/src/RehydrationToken.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/core/Azure.Core/src/Operation.cs b/sdk/core/Azure.Core/src/Operation.cs index 60208de3713b..e6b2ef48ff6c 100644 --- a/sdk/core/Azure.Core/src/Operation.cs +++ b/sdk/core/Azure.Core/src/Operation.cs @@ -98,7 +98,7 @@ public static async Task RehydrateAsync(HttpPipeline pipeline, Rehydr /// /// Gets an ID representing the operation that can be used to poll for /// the status of the long-running operation. - /// There are cases that operation id is not available, we return "NOT_SET" to not break the existing user experience. + /// There are cases that operation id is not available, we return "NOT_SET" for unavailable operation id. /// public abstract string Id { get; } diff --git a/sdk/core/Azure.Core/src/RehydrationToken.cs b/sdk/core/Azure.Core/src/RehydrationToken.cs index 2c4637c66ae3..d53e14181f08 100644 --- a/sdk/core/Azure.Core/src/RehydrationToken.cs +++ b/sdk/core/Azure.Core/src/RehydrationToken.cs @@ -11,7 +11,7 @@ public readonly partial struct RehydrationToken /// /// Gets an ID representing the operation that can be used to poll for /// the status of the long-running operation. - /// There are cases that operation id is not available, we return "NOT_SET" to not break the existing user experience. + /// There are cases that operation id is not available, we return "NOT_SET" for unavailable operation id. /// public string Id { get; } = NextLinkOperationImplementation.NotSet; From b3daf2e46261c20716d6e98f1a9df6b8da99001f Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 10 Apr 2024 11:37:56 +0800 Subject: [PATCH 32/32] avoid cast --- sdk/core/Azure.Core/src/Internal/GenericOperationSource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/core/Azure.Core/src/Internal/GenericOperationSource.cs b/sdk/core/Azure.Core/src/Internal/GenericOperationSource.cs index d9790cb41543..f400c7f6e341 100644 --- a/sdk/core/Azure.Core/src/Internal/GenericOperationSource.cs +++ b/sdk/core/Azure.Core/src/Internal/GenericOperationSource.cs @@ -17,6 +17,6 @@ ValueTask IOperationSource.CreateResultAsync(Response response, Cancellati => new ValueTask(CreateResult(response)); private T CreateResult(Response response) - => (T)ModelReaderWriter.Read(response.Content, typeof(T))!; + => ModelReaderWriter.Read(response.Content)!; } }