diff --git a/ClientRuntimes/CSharp/Microsoft.Rest.ClientRuntime.Azure.Tests/LongRunningOperationsTest.cs b/ClientRuntimes/CSharp/Microsoft.Rest.ClientRuntime.Azure.Tests/LongRunningOperationsTest.cs index 1d0bb2069f1b..014446a6b9eb 100644 --- a/ClientRuntimes/CSharp/Microsoft.Rest.ClientRuntime.Azure.Tests/LongRunningOperationsTest.cs +++ b/ClientRuntimes/CSharp/Microsoft.Rest.ClientRuntime.Azure.Tests/LongRunningOperationsTest.cs @@ -774,6 +774,12 @@ private IEnumerable MockPutOperaionWithoutProvisioningState }; yield return response1; + + var response2 = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{ \"properties\": { }, \"id\": \"100\", \"name\": \"foo\" }") + }; + yield return response2; } private IEnumerable MockPutOperaionWitNonResource() diff --git a/ClientRuntimes/CSharp/Microsoft.Rest.ClientRuntime.Azure/AzureClientExtensions.cs b/ClientRuntimes/CSharp/Microsoft.Rest.ClientRuntime.Azure/AzureClientExtensions.cs index 422f074bd302..6661d5e1895f 100644 --- a/ClientRuntimes/CSharp/Microsoft.Rest.ClientRuntime.Azure/AzureClientExtensions.cs +++ b/ClientRuntimes/CSharp/Microsoft.Rest.ClientRuntime.Azure/AzureClientExtensions.cs @@ -19,7 +19,7 @@ namespace Microsoft.Rest.Azure public static class AzureClientExtensions { /// - /// Gets operation result for PUT and PATCH operations. + /// Gets operation result for long running operations. /// /// Type of the resource body /// IAzureClient @@ -27,11 +27,11 @@ public static class AzureClientExtensions /// Headers that will be added to request /// Cancellation token /// Response with created resource - public static async Task> GetPutOrPatchOperationResultAsync( + public static async Task> GetLongRunningOperationResultAsync( this IAzureClient client, AzureOperationResponse response, Dictionary> customHeaders, - CancellationToken cancellationToken) where TBody : class + CancellationToken cancellationToken) where TBody : class { if (response == null) { @@ -44,7 +44,7 @@ public static class AzureClientExtensions RequestId = response.RequestId, Response = response.Response }; - var longRunningResponse = await GetPutOrPatchOperationResultAsync(client, headerlessResponse, customHeaders, cancellationToken); + var longRunningResponse = await GetLongRunningOperationResultAsync(client, headerlessResponse, customHeaders, cancellationToken); return new AzureOperationResponse { Body = longRunningResponse.Body, @@ -55,7 +55,7 @@ public static class AzureClientExtensions } /// - /// Gets operation result for PUT and PATCH operations. + /// Gets operation result for long running operations. /// /// Type of the resource body /// Type of the resource header @@ -64,7 +64,7 @@ public static class AzureClientExtensions /// Headers that will be added to request /// Cancellation token /// Response with created resource - public static async Task> GetPutOrPatchOperationResultAsync( + public static async Task> GetLongRunningOperationResultAsync( this IAzureClient client, AzureOperationResponse response, Dictionary> customHeaders, @@ -74,11 +74,29 @@ public static class AzureClientExtensions { throw new ValidationException(ValidationRules.CannotBeNull, "response"); } - if (response.Response.StatusCode != HttpStatusCode.OK && - response.Response.StatusCode != HttpStatusCode.Accepted && - response.Response.StatusCode != HttpStatusCode.Created) + + if (response.Response == null) + { + throw new ValidationException(ValidationRules.CannotBeNull, "response.Response"); + } + + if (response.Request == null) + { + throw new ValidationException(ValidationRules.CannotBeNull, "response.Request"); + } + + if (response.Request.Method == null) + { + throw new ValidationException(ValidationRules.CannotBeNull, "response.Request.Method"); + } + + var initialRequestMethod = response.Request.Method; + if (CheckResponseStatusCodeFailed(response)) { - throw new CloudException(string.Format(Resources.UnexpectedPollingStatus, response.Response.StatusCode)); + throw new CloudException(string.Format( + Resources.UnexpectedPollingStatus, + response.Response.StatusCode, + initialRequestMethod)); } var pollingState = new PollingState(response, client.LongRunningOperationRetryTimeout); @@ -92,24 +110,31 @@ public static class AzureClientExtensions if (!string.IsNullOrEmpty(pollingState.AzureAsyncOperationHeaderLink)) { - await UpdateStateFromAzureAsyncOperationHeader(client, pollingState, customHeaders, false, cancellationToken); + await UpdateStateFromAzureAsyncOperationHeader(client, pollingState, customHeaders, cancellationToken); } else if (!string.IsNullOrEmpty(pollingState.LocationHeaderLink)) { - await UpdateStateFromLocationHeaderOnPut(client, pollingState, customHeaders, cancellationToken); + await UpdateStateFromLocationHeader(client, pollingState, customHeaders, cancellationToken, initialRequestMethod); } - else + else if (initialRequestMethod == HttpMethod.Put) { - await UpdateStateFromGetResourceOperation(client, pollingState, getOperationUrl, + await UpdateStateFromGetResourceOperation(client, pollingState, getOperationUrl, customHeaders, cancellationToken); } + else + { + throw new CloudException("Location header is missing from long running operation."); + } } - if (AzureAsyncOperation.SuccessStatus.Equals(pollingState.Status, StringComparison.OrdinalIgnoreCase) && - pollingState.Resource == null) + if (AzureAsyncOperation.SuccessStatus.Equals(pollingState.Status, StringComparison.OrdinalIgnoreCase)) { - await UpdateStateFromGetResourceOperation(client, pollingState, getOperationUrl, - customHeaders, cancellationToken); + if ((!string.IsNullOrEmpty(pollingState.AzureAsyncOperationHeaderLink) || pollingState.Resource == null) && + (initialRequestMethod == HttpMethod.Put || initialRequestMethod == new HttpMethod("PATCH"))) + { + await UpdateStateFromGetResourceOperation(client, pollingState, getOperationUrl, customHeaders, + cancellationToken); + } } // Check if operation failed @@ -123,14 +148,14 @@ public static class AzureClientExtensions } /// - /// Gets operation result for PUT and PATCH operations. + /// Gets operation result for PUT and PATCH operations. (Deprecated, please use GetLongRunningOperationResultAsync) /// /// IAzureClient /// Response from the begin operation /// Headers that will be added to request /// Cancellation token /// Operation response - public static async Task GetPutOrPatchOperationResultAsync( + public static async Task GetLongRunningOperationResultAsync( this IAzureClient client, AzureOperationResponse response, Dictionary> customHeaders, @@ -143,7 +168,7 @@ public static class AzureClientExtensions RequestId = response.RequestId }; - var azureOperationResponse = await client.GetPutOrPatchOperationResultAsync( + var azureOperationResponse = await client.GetLongRunningOperationResultAsync( newResponse, customHeaders, cancellationToken); return new AzureOperationResponse @@ -155,35 +180,35 @@ public static class AzureClientExtensions } /// - /// Gets operation result for DELETE and POST operations. + /// Gets operation result for long running operations. /// - /// Type of the resource body + /// Type of the resource headers /// IAzureClient /// Response from the begin operation /// Headers that will be added to request /// Cancellation token /// Operation response - public static async Task> GetPostOrDeleteOperationResultAsync( + public static async Task> GetLongRunningOperationResultAsync( this IAzureClient client, - AzureOperationResponse response, + AzureOperationHeaderResponse response, Dictionary> customHeaders, - CancellationToken cancellationToken) where TBody : class + CancellationToken cancellationToken) where THeader : class { if (response == null) { throw new ArgumentNullException("response"); } - var headerlessResponse = new AzureOperationResponse + var headerlessResponse = new AzureOperationResponse { - Body = response.Body, + Headers = response.Headers, Request = response.Request, RequestId = response.RequestId, Response = response.Response }; - var longRunningResponse = await GetPostOrDeleteOperationResultAsync(client, headerlessResponse, customHeaders, cancellationToken); - return new AzureOperationResponse + var longRunningResponse = await GetLongRunningOperationResultAsync(client, headerlessResponse, customHeaders, cancellationToken); + return new AzureOperationHeaderResponse { - Body = longRunningResponse.Body, + Headers = longRunningResponse.Headers, Request = longRunningResponse.Request, RequestId = longRunningResponse.RequestId, Response = longRunningResponse.Response @@ -191,7 +216,81 @@ public static class AzureClientExtensions } /// - /// Gets operation result for DELETE and POST operations. + /// Gets operation result for PUT and PATCH operations. (Deprecated, please use GetLongRunningOperationResultAsync) + /// + /// Type of the resource body + /// IAzureClient + /// Response from the begin operation + /// Headers that will be added to request + /// Cancellation token + /// Response with created resource + public static async Task> GetPutOrPatchOperationResultAsync( + this IAzureClient client, + AzureOperationResponse response, + Dictionary> customHeaders, + CancellationToken cancellationToken) where TBody : class + { + return await client.GetLongRunningOperationResultAsync( + response, customHeaders, cancellationToken); + } + + /// + /// Gets operation result for PUT and PATCH operations. (Deprecated, please use GetLongRunningOperationResultAsync) + /// + /// Type of the resource body + /// Type of the resource header + /// IAzureClient + /// Response from the begin operation + /// Headers that will be added to request + /// Cancellation token + /// Response with created resource + public static async Task> GetPutOrPatchOperationResultAsync( + this IAzureClient client, + AzureOperationResponse response, + Dictionary> customHeaders, + CancellationToken cancellationToken) where TBody : class where THeader : class + { + return await GetLongRunningOperationResultAsync(client, response, customHeaders, cancellationToken); + } + + /// + /// Gets operation result for PUT and PATCH operations. (Deprecated, please use GetLongRunningOperationResultAsync) + /// + /// IAzureClient + /// Response from the begin operation + /// Headers that will be added to request + /// Cancellation token + /// Operation response + public static async Task GetPutOrPatchOperationResultAsync( + this IAzureClient client, + AzureOperationResponse response, + Dictionary> customHeaders, + CancellationToken cancellationToken) + { + return await client.GetLongRunningOperationResultAsync( + response, customHeaders, cancellationToken); + } + + /// + /// Gets operation result for DELETE and POST operations. (Deprecated, please use GetLongRunningOperationResultAsync) + /// + /// Type of the resource body + /// IAzureClient + /// Response from the begin operation + /// Headers that will be added to request + /// Cancellation token + /// Operation response + public static async Task> GetPostOrDeleteOperationResultAsync( + this IAzureClient client, + AzureOperationResponse response, + Dictionary> customHeaders, + CancellationToken cancellationToken) where TBody : class + { + return await GetLongRunningOperationResultAsync(client, response, customHeaders, cancellationToken); + } + + /// + /// Gets operation result for DELETE and POST operations. (Deprecated, please use GetLongRunningOperationResultAsync) /// /// Type of the resource headers /// IAzureClient @@ -205,29 +304,11 @@ public static class AzureClientExtensions Dictionary> customHeaders, CancellationToken cancellationToken) where THeader : class { - if (response == null) - { - throw new ArgumentNullException("response"); - } - var headerlessResponse = new AzureOperationResponse - { - Headers = response.Headers, - Request = response.Request, - RequestId = response.RequestId, - Response = response.Response - }; - var longRunningResponse = await GetPostOrDeleteOperationResultAsync(client, headerlessResponse, customHeaders, cancellationToken); - return new AzureOperationHeaderResponse - { - Headers = longRunningResponse.Headers, - Request = longRunningResponse.Request, - RequestId = longRunningResponse.RequestId, - Response = longRunningResponse.Response - }; + return await GetLongRunningOperationResultAsync(client, response, customHeaders, cancellationToken); } /// - /// Gets operation result for DELETE and POST operations. + /// Gets operation result for DELETE and POST operations. (Deprecated, please use GetLongRunningOperationResultAsync) /// /// Type of the resource body /// Type of the resource header @@ -242,57 +323,11 @@ public static class AzureClientExtensions Dictionary> customHeaders, CancellationToken cancellationToken) where TBody : class where THeader : class { - if (response == null) - { - throw new ValidationException(ValidationRules.CannotBeNull, "response"); - } - - if (response.Response == null) - { - throw new ValidationException(ValidationRules.CannotBeNull, "response.Response"); - } - - if (response.Response.StatusCode != HttpStatusCode.OK && - response.Response.StatusCode != HttpStatusCode.Accepted && - response.Response.StatusCode != HttpStatusCode.NoContent) - { - throw new CloudException(string.Format(Resources.UnexpectedPollingStatus, response.Response.StatusCode)); - } - - var pollingState = new PollingState(response, client.LongRunningOperationRetryTimeout); - - // Check provisioning state - while (!AzureAsyncOperation.TerminalStatuses.Any(s => s.Equals(pollingState.Status, - StringComparison.OrdinalIgnoreCase))) - { - await Task.Delay(pollingState.DelayInMilliseconds, cancellationToken).ConfigureAwait(false); - - if (!string.IsNullOrEmpty(pollingState.AzureAsyncOperationHeaderLink)) - { - await UpdateStateFromAzureAsyncOperationHeader(client, pollingState, customHeaders,true, cancellationToken); - } - else if (!string.IsNullOrEmpty(pollingState.LocationHeaderLink)) - { - await UpdateStateFromLocationHeaderOnPostOrDelete(client, pollingState, customHeaders, cancellationToken); - } - else - { - throw new CloudException(Resources.NoHeader); - } - } - - // Check if operation failed - if (AzureAsyncOperation.FailedStatuses.Any( - s => s.Equals(pollingState.Status, StringComparison.OrdinalIgnoreCase))) - { - throw pollingState.CloudException; - } - - return pollingState.AzureOperationResponse; + return await GetLongRunningOperationResultAsync(client, response, customHeaders, cancellationToken); } /// - /// Gets operation result for DELETE and POST operations. + /// Gets operation result for DELETE and POST operations. (Deprecated, please use GetLongRunningOperationResultAsync) /// /// IAzureClient /// Response from the begin operation @@ -305,22 +340,21 @@ public static class AzureClientExtensions Dictionary> customHeaders, CancellationToken cancellationToken) { - var newResponse = new AzureOperationResponse - { - Request = response.Request, - Response = response.Response, - RequestId = response.RequestId - }; - - var azureOperationResponse = await client.GetPostOrDeleteOperationResultAsync( - newResponse, customHeaders, cancellationToken); + return await client.GetLongRunningOperationResultAsync(response, customHeaders, cancellationToken); + } - return new AzureOperationResponse + private static bool CheckResponseStatusCodeFailed( + AzureOperationResponse initialResponse) + { + var statusCode = initialResponse.Response.StatusCode; + var method = initialResponse.Request.Method; + if (statusCode == HttpStatusCode.OK || statusCode == HttpStatusCode.Accepted || + (statusCode == HttpStatusCode.Created && method == HttpMethod.Put) || + (statusCode == HttpStatusCode.NoContent && (method == HttpMethod.Delete || method == HttpMethod.Post))) { - Request = azureOperationResponse.Request, - Response = azureOperationResponse.Response, - RequestId = azureOperationResponse.RequestId - }; + return false; + } + return true; } /// @@ -375,7 +409,7 @@ public static class AzureClientExtensions } /// - /// Updates PollingState from Location header on Put operations. + /// Updates PollingState from Location header. /// /// Type of the resource body. /// Type of the resource header. @@ -383,12 +417,14 @@ public static class AzureClientExtensions /// Current polling state. /// Headers that will be added to request /// Cancellation token + /// Http method of the initial long running operation request /// Task. - private static async Task UpdateStateFromLocationHeaderOnPut( + private static async Task UpdateStateFromLocationHeader( IAzureClient client, PollingState pollingState, Dictionary> customHeaders, - CancellationToken cancellationToken) where TBody : class where THeader : class + CancellationToken cancellationToken, + HttpMethod method) where TBody : class where THeader : class { AzureOperationResponse responseWithResource = await client.GetRawAsync( pollingState.LocationHeaderLink, @@ -404,73 +440,24 @@ public static class AzureClientExtensions pollingState.Status = AzureAsyncOperation.InProgressStatus; } else if (statusCode == HttpStatusCode.OK || - statusCode == HttpStatusCode.Created) + (statusCode == HttpStatusCode.Created && method == HttpMethod.Put) || + (statusCode == HttpStatusCode.NoContent && (method == HttpMethod.Delete || method == HttpMethod.Post))) { - if (responseWithResource.Body == null) - { - throw new CloudException(Resources.NoBody); - } - - // In 202 pattern on PUT ProvisioningState may not be present in - // the response. In that case the assumption is the status is Succeeded. - var resource = responseWithResource.Body; - if (resource["properties"] != null && resource["properties"]["provisioningState"] != null) - { - pollingState.Status = (string)resource["properties"]["provisioningState"]; - } - else - { - pollingState.Status = AzureAsyncOperation.SuccessStatus; - } + pollingState.Status = AzureAsyncOperation.SuccessStatus; pollingState.Error = new CloudError() { Code = pollingState.Status, Message = string.Format(Resources.LongRunningOperationFailed, pollingState.Status) }; - pollingState.Resource = responseWithResource.Body.ToObject(JsonSerializer + pollingState.Resource = responseWithResource.Body == null ? null : responseWithResource.Body.ToObject(JsonSerializer .Create(client.DeserializationSettings)); pollingState.ResourceHeaders = responseWithResource.Headers.ToObject(JsonSerializer .Create(client.DeserializationSettings)); } - } - - /// - /// Updates PollingState from Location header on Post or Delete operations. - /// - /// Type of the resource body. - /// Type of the resource header. - /// IAzureClient - /// Current polling state. - /// Headers that will be added to request - /// Cancellation token - /// Task. - private static async Task UpdateStateFromLocationHeaderOnPostOrDelete( - IAzureClient client, - PollingState pollingState, - Dictionary> customHeaders, - CancellationToken cancellationToken) where TBody : class where THeader : class - { - AzureOperationResponse responseWithResource = await client.GetAsync( - pollingState.LocationHeaderLink, - customHeaders, - cancellationToken).ConfigureAwait(false); - - pollingState.Response = responseWithResource.Response; - pollingState.Request = responseWithResource.Request; - pollingState.ResourceHeaders = responseWithResource.Headers; - - var statusCode = responseWithResource.Response.StatusCode; - if (statusCode == HttpStatusCode.Accepted) - { - pollingState.Status = AzureAsyncOperation.InProgressStatus; - } - else if (statusCode == HttpStatusCode.OK || - statusCode == HttpStatusCode.Created || - statusCode == HttpStatusCode.NoContent) + else { - pollingState.Status = AzureAsyncOperation.SuccessStatus; - pollingState.Resource = responseWithResource.Body; + throw new CloudException("The response from long running operation does not have a valid status code."); } } @@ -482,14 +469,12 @@ public static class AzureClientExtensions /// IAzureClient /// Current polling state. /// Headers that will be added to request - /// Headers that will be added to request /// Cancellation token /// Task. private static async Task UpdateStateFromAzureAsyncOperationHeader( IAzureClient client, PollingState pollingState, - Dictionary> customHeaders, - bool postOrDelete, + Dictionary> customHeaders, CancellationToken cancellationToken) where TBody : class where THeader : class { AzureOperationResponse asyncOperationResponse = @@ -508,21 +493,18 @@ public static class AzureClientExtensions pollingState.Response = asyncOperationResponse.Response; pollingState.Request = asyncOperationResponse.Request; pollingState.Resource = null; - if (postOrDelete) - { - //Try to de-serialize to the response model. (Not required for "PutOrPatch" - //which has the fallback of invoking generic "resource get".) - string responseContent = await pollingState.Response.Content.ReadAsStringAsync(); - var responseHeaders = pollingState.Response.Headers.ToJson(); - try - { - pollingState.Resource = JObject.Parse(responseContent) - .ToObject(JsonSerializer.Create(client.DeserializationSettings)); - pollingState.ResourceHeaders = - responseHeaders.ToObject(JsonSerializer.Create(client.DeserializationSettings)); - } - catch { }; + //Try to de-serialize to the response model. (Not required for "PutOrPatch" + //which has the fallback of invoking generic "resource get".) + string responseContent = await pollingState.Response.Content.ReadAsStringAsync(); + var responseHeaders = pollingState.Response.Headers.ToJson(); + try + { + pollingState.Resource = JObject.Parse(responseContent) + .ToObject(JsonSerializer.Create(client.DeserializationSettings)); + pollingState.ResourceHeaders = + responseHeaders.ToObject(JsonSerializer.Create(client.DeserializationSettings)); } + catch { }; } /// diff --git a/ClientRuntimes/CSharp/Microsoft.Rest.ClientRuntime.Azure/PollingState.cs b/ClientRuntimes/CSharp/Microsoft.Rest.ClientRuntime.Azure/PollingState.cs index 26681f0f7e59..17c38105b0b3 100644 --- a/ClientRuntimes/CSharp/Microsoft.Rest.ClientRuntime.Azure/PollingState.cs +++ b/ClientRuntimes/CSharp/Microsoft.Rest.ClientRuntime.Azure/PollingState.cs @@ -46,29 +46,39 @@ public PollingState(HttpOperationResponse response, int? retryTi } } - if (resource != null && resource["properties"] != null && - resource["properties"]["provisioningState"] != null) + switch (Response.StatusCode) { - Status = (string)resource["properties"]["provisioningState"]; - } - else - { - switch (Response.StatusCode) - { - case HttpStatusCode.Accepted: - Status = AzureAsyncOperation.InProgressStatus; - break; - - case HttpStatusCode.NoContent: - case HttpStatusCode.Created: - case HttpStatusCode.OK: + case HttpStatusCode.Accepted: + Status = AzureAsyncOperation.InProgressStatus; + break; + case HttpStatusCode.OK: + if (resource != null && resource["properties"] != null && + resource["properties"]["provisioningState"] != null) + { + Status = (string)resource["properties"]["provisioningState"]; + } + else + { Status = AzureAsyncOperation.SuccessStatus; - break; - - default: - Status = AzureAsyncOperation.FailedStatus; - break; - } + } + break; + case HttpStatusCode.Created: + if (resource != null && resource["properties"] != null && + resource["properties"]["provisioningState"] != null) + { + Status = (string) resource["properties"]["provisioningState"]; + } + else + { + Status = AzureAsyncOperation.InProgressStatus; + } + break; + case HttpStatusCode.NoContent: + Status = AzureAsyncOperation.SuccessStatus; + break; + default: + Status = AzureAsyncOperation.FailedStatus; + break; } } diff --git a/ClientRuntimes/CSharp/Microsoft.Rest.ClientRuntime.Azure/Properties/Resources.Designer.cs b/ClientRuntimes/CSharp/Microsoft.Rest.ClientRuntime.Azure/Properties/Resources.Designer.cs index fb148df5bdf7..04e84d7a82b8 100644 --- a/ClientRuntimes/CSharp/Microsoft.Rest.ClientRuntime.Azure/Properties/Resources.Designer.cs +++ b/ClientRuntimes/CSharp/Microsoft.Rest.ClientRuntime.Azure/Properties/Resources.Designer.cs @@ -304,7 +304,7 @@ internal class Resources { } /// - /// Looks up a localized string similar to Unexpected polling status code from long running operation '{0}'.. + /// Looks up a localized string similar to Unexpected polling status code from long running operation '{0}' for method '{1}'.. /// internal static string UnexpectedPollingStatus { get { diff --git a/ClientRuntimes/CSharp/Microsoft.Rest.ClientRuntime.Azure/Properties/Resources.resx b/ClientRuntimes/CSharp/Microsoft.Rest.ClientRuntime.Azure/Properties/Resources.resx index 496a6c2483ad..8fb340da09bb 100644 --- a/ClientRuntimes/CSharp/Microsoft.Rest.ClientRuntime.Azure/Properties/Resources.resx +++ b/ClientRuntimes/CSharp/Microsoft.Rest.ClientRuntime.Azure/Properties/Resources.resx @@ -200,6 +200,6 @@ Parameter name: {0} The specified argument '{0}' must return a scheduled task (also known as "hot" task) when invoked. - Unexpected polling status code from long running operation '{0}'. + Unexpected polling status code from long running operation '{0}' for method '{1}'. \ No newline at end of file