diff --git a/abstractions/dotnet/src/IRequestAdapter.cs b/abstractions/dotnet/src/IRequestAdapter.cs index 61da556f49..b7980f6b10 100644 --- a/abstractions/dotnet/src/IRequestAdapter.cs +++ b/abstractions/dotnet/src/IRequestAdapter.cs @@ -2,6 +2,7 @@ // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. // ------------------------------------------------------------------------------ +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -29,41 +30,46 @@ public interface IRequestAdapter /// /// The RequestInformation object to use for the HTTP request. /// The response handler to use for the HTTP request instead of the default handler. + /// The error factories mapping to use in case of a failed request. /// The to use for cancelling the requests. /// The deserialized response model. - Task SendAsync(RequestInformation requestInfo, IResponseHandler responseHandler = default, CancellationToken cancellationToken = default) where ModelType : IParsable; + Task SendAsync(RequestInformation requestInfo, IResponseHandler responseHandler = default, Dictionary> errorMapping = default, CancellationToken cancellationToken = default) where ModelType : IParsable; /// /// Executes the HTTP request specified by the given RequestInformation and returns the deserialized response model collection. /// /// The RequestInformation object to use for the HTTP request. /// The response handler to use for the HTTP request instead of the default handler. + /// The error factories mapping to use in case of a failed request. /// The to use for cancelling the requests. /// The deserialized response model collection. - Task> SendCollectionAsync(RequestInformation requestInfo, IResponseHandler responseHandler = default, CancellationToken cancellationToken = default) where ModelType : IParsable; + Task> SendCollectionAsync(RequestInformation requestInfo, IResponseHandler responseHandler = default, Dictionary> errorMapping = default, CancellationToken cancellationToken = default) where ModelType : IParsable; /// /// Executes the HTTP request specified by the given RequestInformation and returns the deserialized primitive response model. /// /// The RequestInformation object to use for the HTTP request. /// The response handler to use for the HTTP request instead of the default handler. + /// The error factories mapping to use in case of a failed request. /// The to use for cancelling the requests. /// The deserialized primitive response model. - Task SendPrimitiveAsync(RequestInformation requestInfo, IResponseHandler responseHandler = default, CancellationToken cancellationToken = default); + Task SendPrimitiveAsync(RequestInformation requestInfo, IResponseHandler responseHandler = default, Dictionary> errorMapping = default, CancellationToken cancellationToken = default); /// /// Executes the HTTP request specified by the given RequestInformation and returns the deserialized primitive response model collection. /// /// The RequestInformation object to use for the HTTP request. /// The response handler to use for the HTTP request instead of the default handler. + /// The error factories mapping to use in case of a failed request. /// The to use for cancelling the requests. /// The deserialized primitive response model collection. - Task> SendPrimitiveCollectionAsync(RequestInformation requestInfo, IResponseHandler responseHandler = default, CancellationToken cancellationToken = default); + Task> SendPrimitiveCollectionAsync(RequestInformation requestInfo, IResponseHandler responseHandler = default, Dictionary> errorMapping = default, CancellationToken cancellationToken = default); /// /// Executes the HTTP request specified by the given RequestInformation with no return content. /// /// The RequestInformation object to use for the HTTP request. /// The response handler to use for the HTTP request instead of the default handler. + /// The error factories mapping to use in case of a failed request. /// The to use for cancelling the requests. /// A Task to await completion. - Task SendNoContentAsync(RequestInformation requestInfo, IResponseHandler responseHandler = default, CancellationToken cancellationToken = default); + Task SendNoContentAsync(RequestInformation requestInfo, IResponseHandler responseHandler = default, Dictionary> errorMapping = default, CancellationToken cancellationToken = default); /// /// The base url for every request. /// diff --git a/abstractions/dotnet/src/serialization/IParseNode.cs b/abstractions/dotnet/src/serialization/IParseNode.cs index 314c49f8ae..e96a2db892 100644 --- a/abstractions/dotnet/src/serialization/IParseNode.cs +++ b/abstractions/dotnet/src/serialization/IParseNode.cs @@ -99,6 +99,12 @@ public interface IParseNode /// The model object value of the node. T GetObjectValue() where T : IParsable; /// + /// Gets the resulting error from the node. + /// + /// The error object value of the node. + /// The error factory. + IParsable GetErrorValue(Func factory); + /// /// Callback called before the node is deserialized. /// Action OnBeforeAssignFieldValues { get; set; } diff --git a/http/dotnet/httpclient/src/HttpClientRequestAdapter.cs b/http/dotnet/httpclient/src/HttpClientRequestAdapter.cs index 95a82600cb..4d72652755 100644 --- a/http/dotnet/httpclient/src/HttpClientRequestAdapter.cs +++ b/http/dotnet/httpclient/src/HttpClientRequestAdapter.cs @@ -58,13 +58,15 @@ public ISerializationWriterFactory SerializationWriterFactory /// /// The instance to send /// The to use with the response + /// The error factories mapping to use in case of a failed request. /// The to use for cancelling the request. - public async Task> SendCollectionAsync(RequestInformation requestInfo, IResponseHandler responseHandler = default, CancellationToken cancellationToken = default) where ModelType : IParsable + public async Task> SendCollectionAsync(RequestInformation requestInfo, IResponseHandler responseHandler = default, Dictionary> errorMapping = default, CancellationToken cancellationToken = default) where ModelType : IParsable { var response = await GetHttpResponseMessage(requestInfo, cancellationToken); requestInfo.Content?.Dispose(); if(responseHandler == null) { + await ThrowFailedResponse(response, errorMapping); var rootNode = await GetRootParseNode(response); var result = rootNode.GetCollectionOfObjectValues(); return result; @@ -77,13 +79,15 @@ public async Task> SendCollectionAsync(Request /// /// The RequestInformation object to use for the HTTP request. /// The response handler to use for the HTTP request instead of the default handler. + /// The error factories mapping to use in case of a failed request. /// The to use for cancelling the request. /// The deserialized primitive response model collection. - public async Task> SendPrimitiveCollectionAsync(RequestInformation requestInfo, IResponseHandler responseHandler = default, CancellationToken cancellationToken = default) { + public async Task> SendPrimitiveCollectionAsync(RequestInformation requestInfo, IResponseHandler responseHandler = default, Dictionary> errorMapping = default, CancellationToken cancellationToken = default) { var response = await GetHttpResponseMessage(requestInfo, cancellationToken); requestInfo.Content?.Dispose(); if(responseHandler == null) { + await ThrowFailedResponse(response, errorMapping); var rootNode = await GetRootParseNode(response); var result = rootNode.GetCollectionOfPrimitiveValues(); return result; @@ -96,14 +100,16 @@ public async Task> SendPrimitiveCollectionAsync /// The instance to send /// The to use with the response + /// The error factories mapping to use in case of a failed request. /// The to use for cancelling the request. /// The deserialized response model. - public async Task SendAsync(RequestInformation requestInfo, IResponseHandler responseHandler = null, CancellationToken cancellationToken = default) where ModelType : IParsable + public async Task SendAsync(RequestInformation requestInfo, IResponseHandler responseHandler = null, Dictionary> errorMapping = default, CancellationToken cancellationToken = default) where ModelType : IParsable { var response = await GetHttpResponseMessage(requestInfo, cancellationToken); requestInfo.Content?.Dispose(); if(responseHandler == null) { + await ThrowFailedResponse(response, errorMapping); var rootNode = await GetRootParseNode(response); var result = rootNode.GetObjectValue(); return result; @@ -116,9 +122,10 @@ public async Task SendAsync(RequestInformation requestInfo /// /// The instance to send /// The to use with the response + /// The error factories mapping to use in case of a failed request. /// The to use for cancelling the request. /// The deserialized primitive response model. - public async Task SendPrimitiveAsync(RequestInformation requestInfo, IResponseHandler responseHandler = default, CancellationToken cancellationToken = default) + public async Task SendPrimitiveAsync(RequestInformation requestInfo, IResponseHandler responseHandler = default, Dictionary> errorMapping = default, CancellationToken cancellationToken = default) { var response = await GetHttpResponseMessage(requestInfo, cancellationToken); requestInfo.Content?.Dispose(); @@ -131,6 +138,7 @@ public async Task SendPrimitiveAsync(RequestInformation re } else { + await ThrowFailedResponse(response, errorMapping); var rootNode = await GetRootParseNode(response); object result; if(modelType == typeof(bool)) @@ -173,9 +181,10 @@ public async Task SendPrimitiveAsync(RequestInformation re /// /// The instance to send /// The to use with the response + /// The error factories mapping to use in case of a failed request. /// The to use for cancelling the request. /// - public async Task SendNoContentAsync(RequestInformation requestInfo, IResponseHandler responseHandler = null, CancellationToken cancellationToken = default) + public async Task SendNoContentAsync(RequestInformation requestInfo, IResponseHandler responseHandler = null, Dictionary> errorMapping = default, CancellationToken cancellationToken = default) { var response = await GetHttpResponseMessage(requestInfo, cancellationToken); requestInfo.Content?.Dispose(); @@ -184,6 +193,25 @@ public async Task SendNoContentAsync(RequestInformation requestInfo, IResponseHa else await responseHandler.HandleResponseAsync(response); } + private async Task ThrowFailedResponse(HttpResponseMessage response, Dictionary> errorMapping) + { + if(response.IsSuccessStatusCode) return; + + var statusCodeAsInt = (int)response.StatusCode; + var statusCodeAsString = statusCodeAsInt.ToString(); + Func errorFactory; + if(errorMapping == null || + !errorMapping.TryGetValue(statusCodeAsString, out errorFactory) && + !(statusCodeAsInt >= 400 && statusCodeAsInt < 500 && errorMapping.TryGetValue("4XX", out errorFactory)) && + !(statusCodeAsInt >= 500 && statusCodeAsInt < 600 && errorMapping.TryGetValue("5XX", out errorFactory))) + throw new HttpRequestException($"The server returned an unexpected status code and no error factory is registered for this code: {statusCodeAsString}"); + + var rootNode = await GetRootParseNode(response); + var result = rootNode.GetErrorValue(errorFactory); + if(result is not Exception ex) + throw new HttpRequestException($"The server returned an unexpected status code and the error registered for this code failed to deserialize: {statusCodeAsString}"); + else throw ex; + } private async Task GetRootParseNode(HttpResponseMessage response) { var responseContentType = response.Content.Headers?.ContentType?.MediaType?.ToLowerInvariant(); diff --git a/serialization/dotnet/json/src/JsonParseNode.cs b/serialization/dotnet/json/src/JsonParseNode.cs index ec44a7228c..378196da94 100644 --- a/serialization/dotnet/json/src/JsonParseNode.cs +++ b/serialization/dotnet/json/src/JsonParseNode.cs @@ -256,12 +256,30 @@ public IEnumerable GetCollectionOfPrimitiveValues() public T GetObjectValue() where T : IParsable { var item = (T)(typeof(T).GetConstructor(new Type[] { }).Invoke(new object[] { })); + return GetObjectValueInternal(item); + } + private T GetObjectValueInternal(T item) where T : IParsable + { var fieldDeserializers = item.GetFieldDeserializers(); OnBeforeAssignFieldValues?.Invoke(item); AssignFieldValues(item, fieldDeserializers); OnAfterAssignFieldValues?.Invoke(item); return item; } + /// + /// Gets the resulting error from the node. + /// + /// The error object value of the node. + /// The error factory. + public IParsable GetErrorValue(Func factory) + { + if (factory == null) throw new ArgumentNullException(nameof(factory)); + + var instance = factory.Invoke(); + if (instance == null) throw new InvalidOperationException("factory returned null"); + + return GetObjectValueInternal(instance); + } private void AssignFieldValues(T item, IDictionary> fieldDeserializers) where T : IParsable { if(_jsonNode.ValueKind != JsonValueKind.Object) return;